#!/usr/bin/env python3
"""
Apple Certificate and Provisioning Profile Manager

This script manages Apple Developer certificates and provisioning profiles through
the App Store Connect API. It can delete distribution certificates and provisioning
profiles to help maintain a clean development environment.
"""

import jwt
import requests
import time
import os
import logging
import colorlog
import sys
from typing import Dict, List, Optional, Any, Union
import argparse
import time
from datetime import datetime

# pip3 install pyjwt requests colorlog

# Configuration
KEY_ID = os.getenv('APPLE_KEY_ID')
ISSUER_ID = os.getenv('APPLE_ISSUER_ID')
KEY_FILEPATH = os.getenv('APPLE_KEY_PATH')
BASE_URL = 'https://api.appstoreconnect.apple.com/v1'

# Set up colorful logging
handler = colorlog.StreamHandler()
handler.setFormatter(colorlog.ColoredFormatter(
    '%(log_color)s%(asctime)s - %(levelname)s - %(message)s',
    log_colors={
        'DEBUG': 'cyan',
        'INFO': 'green',
        'WARNING': 'yellow',
        'ERROR': 'red',
        'CRITICAL': 'bold_red'
    }
))
logger = colorlog.getLogger()
logger.addHandler(handler)
logger.setLevel(logging.INFO)

# Load private key
def load_private_key(filepath: str) -> Optional[str]:
    """Load the private key from the specified file."""
    try:
        with open(filepath, 'r') as key_file:
            return key_file.read()
    except FileNotFoundError:
        logger.error(f"Private key file not found: {filepath}")
        return None
    except Exception as e:
        logger.error(f"Error loading private key: {e}")
        return None

# Generate JWT
def generate_jwt(private_key: str) -> Optional[str]:
    """Generate a JWT token for API authentication."""
    headers = {'kid': KEY_ID, 'alg': 'ES256'}
    payload = {
        'iss': ISSUER_ID,
        'iat': int(time.time()),
        'exp': int(time.time()) + 20 * 60,  # Token valid for 20 minutes
        'aud': 'appstoreconnect-v1'
    }
    try:
        return jwt.encode(payload=payload, key=private_key, algorithm='ES256', headers=headers)
    except Exception as e:
        logger.error(f"Error generating JWT: {e}")
        return None

# Make a request to App Store Connect API
def api_request(method: str, endpoint: str, token: str, data: Optional[Dict] = None) -> Optional[Dict]:
    """Make a request to the App Store Connect API."""
    url = f'{BASE_URL}/{endpoint}'
    headers = {
        'Authorization': f'Bearer {token}',
        'Content-Type': 'application/json'
    }

    try:
        response = requests.request(method=method, url=url, headers=headers, json=data, timeout=30)

        # Handle rate limiting
        if response.status_code == 429:
            retry_after = int(response.headers.get('Retry-After', 60))
            logger.warning(f"Rate limited. Waiting {retry_after} seconds before retrying...")
            time.sleep(retry_after)
            return api_request(method=method, endpoint=endpoint, token=token, data=data)

        response.raise_for_status()
        return response.json() if response.status_code != 204 else {}
    except requests.exceptions.HTTPError as http_err:
        logger.error(f"HTTP error occurred: {http_err}")
        return None
    except Exception as err:
        logger.error(f"Error occurred: {err}")
        return None

# Check if provisioning profile exists
def check_profile_exists(token: str, profile_name: str) -> Optional[str]:
    """Check if a provisioning profile with the given name exists."""
    profiles = api_request(method='GET', endpoint='profiles', token=token)
    if not profiles:
        return None

    for profile in profiles.get('data', []):
        if profile['attributes']['name'] == profile_name:
            logger.info(f"Provisioning profile '{profile_name}' exists.")
            return profile['id']

    logger.warning(f"Provisioning profile '{profile_name}' does not exist.")
    return None

# Delete provisioning profile
def delete_profile(token: str, profile_id: str) -> bool:
    """Delete a provisioning profile by ID."""
    result = api_request(method='DELETE', endpoint=f'profiles/{profile_id}', token=token)
    if result is not None or result == {}:
        logger.info(f"Provisioning profile '{profile_id}' deleted successfully.")
        return True
    else:
        logger.error(f"Failed to delete provisioning profile '{profile_id}'.")
        return False

# Fetch and return a distribution certificate ID for deletion
def get_distribution_certificate_for_deletion(token: str, min_certs: int = 2) -> Optional[str]:
    """
    Fetch distribution certificates and return one for deletion if there are
    greater than or equal to the specified minimum number (default 2).
    Will select any certificate for deletion, not specifically the oldest one.
    """
    certificates = api_request(method='GET', endpoint='certificates', token=token)

    if not certificates:
        return None
    # Filter for distribution certificates only
    current_time = time.strftime('%Y-%m-%dT%H:%M:%S.000+00:00')
    distribution_certs = [
        {
            'ID': cert['id'],
            'Name': cert['attributes']['name'],
            'Type': cert['attributes']['certificateType'],
            'ExpirationDate': cert['attributes'].get('expirationDate', 'Unknown')
        }
        for cert in certificates.get('data', [])
        if cert['attributes']['certificateType'] == 'DISTRIBUTION' and 
           cert['attributes'].get('expirationDate', '') > current_time
    ]

    # Count the number of distribution certificates
    distribution_count = len(distribution_certs)
    logger.info(f"Number of DISTRIBUTION certificates: {distribution_count}")

    # Check if there are greater than or equal to the minimum required certificates
    if distribution_count >= min_certs:
        # Log the certificates
        for cert in distribution_certs:
            logger.info(f"Distribution Certificate: {cert['Name']} (ID: {cert['ID']}, Expires: {cert['ExpirationDate']})")

        # Simply select the first certificate for deletion
        selected_cert = distribution_certs[0]
        logger.info(f"Selected for deletion: {selected_cert['Name']} (ID: {selected_cert['ID']}, Expires: {selected_cert['ExpirationDate']})")
        return selected_cert['ID']
    else:
        # Log that there are not enough distribution certificates for deletion
        logger.info(f"Only {distribution_count} distribution certificates found. Minimum is {min_certs}. No deletion needed.")
        return None

# Delete certificate
def delete_certificate(token: str, certificate_id: str) -> bool:
    """Delete a certificate by ID."""
    result = api_request(method='DELETE', endpoint=f'certificates/{certificate_id}', token=token)
    if result is not None or result == {}:
        logger.info(f"Certificate {certificate_id} deleted successfully.")
        return True
    else:
        logger.error(f"Failed to delete certificate {certificate_id}.")
        return False

# Main function
def main(identifier: str, key_id: str, issuer_id: str, key_filepath: str, info_only: bool = False) -> int:
    """Main function to run the script."""
    # Set global variables
    global KEY_ID, ISSUER_ID, KEY_FILEPATH
    KEY_ID = key_id
    ISSUER_ID = issuer_id
    KEY_FILEPATH = key_filepath

    private_key = load_private_key(filepath=KEY_FILEPATH)
    if not private_key:
        logger.error("Failed to load private key. Exiting.")
        return 1

    token = generate_jwt(private_key=private_key)
    if not token:
        logger.error("Failed to generate JWT token. Exiting.")
        return 1

    # Step 1: Check for certificates - if there are 2 or more distribution certificates, delete any one
    distribution_cert_id = get_distribution_certificate_for_deletion(token=token)
    if distribution_cert_id:
        delete_certificate(token=token, certificate_id=distribution_cert_id)
    else:
        logger.info("No distribution certificates available for deletion. We are good to go.")

    # Step 2: Check for provisioning profiles
    profile_name = f'match AppStore {identifier}'
    profile_id = check_profile_exists(token=token, profile_name=profile_name)

    if profile_id:
        delete_profile(token=token, profile_id=profile_id)
    else:
        logger.info(f"Provisioning profile '{profile_name}' not found. We are good to go.")

    logger.info("Operations completed successfully.")
    return 0

if __name__ == '__main__':
    # Use argparse for named command-line arguments
    parser = argparse.ArgumentParser(description='Apple Certificate and Provisioning Profile Manager')
    parser.add_argument('--identifier', '-i', required=True, help='App identifier (e.g., com.example.app)')
    parser.add_argument('--key-id', '-k', required=True, help='Apple Developer Key ID')
    parser.add_argument('--issuer-id', '-s', required=True, help='Apple Developer Issuer ID')
    parser.add_argument('--key-filepath', '-f', required=True, help='Path to the private key file')
    parser.add_argument('--info-only', action='store_true', help='Information only mode, no deletions')

    # Show usage example in the help text
    parser.epilog = 'Example: python3 delete_apple_certificates.py --identifier com.example.app --key-id 77HLG2C29P --issuer-id a1ead579-73d7-4227-b9d6-1aeccf17edb4 --key-filepath AuthKey_77HLG2C29P.p8'

    args = parser.parse_args()

    sys.exit(main(
        identifier=args.identifier,
        key_id=args.key_id,
        issuer_id=args.issuer_id,
        key_filepath=args.key_filepath,
        info_only=args.info_only
    ))