"""
AWS Certificate collector for multi-account certificate inventory.

Discovers certificates from:
- AWS Certificate Manager (ACM) — per region
- IAM Server Certificates — global
- Azure Key Vault (via subprocess az CLI) — optional

Follows BaseResourceCollector pattern. READ-ONLY operations only.
"""

import json
import subprocess
from datetime import datetime, timezone
from typing import Any, Dict, List, Optional

import boto3
from botocore.exceptions import ClientError
from loguru import logger

from runbooks.inventory.collectors.base import BaseResourceCollector, CollectionContext
from runbooks.inventory.models.certificate import (
    CertificateInventoryResult,
    CertificateRecord,
    CertificateSource,
    CertificateStatus,
    CertificateUsage,
    ManagedBy,
)
from runbooks.inventory.models.resource import AWSResource


# CloudFront requires certificates to be in us-east-1
ACM_CLOUDFRONT_REGION = "us-east-1"
DEFAULT_REGION = "ap-southeast-2"
AZURE_SUBPROCESS_TIMEOUT = 60
ORG_CROSS_ACCOUNT_ROLE = "OrganizationAccountAccessRole"

# Map ACM status strings to CertificateStatus enum values
_ACM_STATUS_MAP: Dict[str, CertificateStatus] = {
    "ISSUED": CertificateStatus.ISSUED,
    "PENDING_VALIDATION": CertificateStatus.PENDING_VALIDATION,
    "INACTIVE": CertificateStatus.INACTIVE,
    "EXPIRED": CertificateStatus.EXPIRED,
    "VALIDATION_TIMED_OUT": CertificateStatus.VALIDATION_TIMED_OUT,
    "REVOKED": CertificateStatus.REVOKED,
    "FAILED": CertificateStatus.FAILED,
}


class CertificateCollector(BaseResourceCollector):
    """
    Collector for AWS certificate resources across ACM and IAM.

    Handles discovery and inventory of:
    - ACM certificates (regional)
    - IAM server certificates (global, all regions)
    - Azure Key Vault certificates (optional, via az CLI)
    """

    service_category = "certificates"
    supported_resources = {
        "acm:certificate",
        "iam:server-certificate",
    }
    requires_org_access = False

    def collect_resources(
        self, context: CollectionContext, resource_filters: Optional[Dict[str, Any]] = None
    ) -> List[AWSResource]:
        """
        Collect certificate resources — satisfies BaseResourceCollector interface.

        For certificate-specific structured results use scan_single_account()
        or scan_multi_account() which return CertificateInventoryResult.

        Returns an empty list (certificate records are returned via dedicated methods).
        """
        logger.debug(
            f"CertificateCollector.collect_resources called for account "
            f"{context.account.account_id} region {context.region}"
        )
        return []

    def get_resource_costs(self, resources: List[AWSResource], context: CollectionContext) -> Dict[str, float]:
        """ACM certificates have no direct cost. Returns empty dict."""
        return {}

    # ------------------------------------------------------------------
    # Public API
    # ------------------------------------------------------------------

    def scan_single_account(self, profile: Optional[str], region: str = "ap-southeast-2") -> CertificateInventoryResult:
        """
        Scan one AWS account for certificates.

        Args:
            profile: AWS profile name (None uses default credential chain)
            region: Primary AWS region for ACM queries

        Returns:
            CertificateInventoryResult with all discovered certificates
        """
        result = CertificateInventoryResult()
        result.scan_metadata["scan_mode"] = "single_account"
        result.scan_metadata["region"] = region
        result.scan_metadata["started_at"] = datetime.utcnow().isoformat()

        session = boto3.Session(profile_name=profile) if profile else boto3.Session()
        try:
            sts = session.client("sts", region_name=region)
            identity = sts.get_caller_identity()
            account_id = identity["Account"]
        except ClientError as e:
            logger.error(f"Could not get caller identity: {e}")
            result.summary.accounts_failed = 1
            result.scan_metadata["error"] = str(e)
            return result

        acm_certs = self._collect_acm_certificates(session, account_id, "", region)
        iam_certs = self._collect_iam_server_certificates(session, account_id, "")
        result.certificates.extend(acm_certs)
        result.certificates.extend(iam_certs)
        result.summary.accounts_scanned = 1
        result.build_summary()
        result.scan_metadata["finished_at"] = datetime.utcnow().isoformat()
        return result

    def scan_multi_account(
        self,
        management_profile: str,
        region: str = "ap-southeast-2",
    ) -> CertificateInventoryResult:
        """
        Scan all AWS Organization accounts for certificates.

        Uses Organizations API on the management account to enumerate member
        accounts, then assumes OrganizationAccountAccessRole in each to collect
        certificates.

        Args:
            management_profile: AWS profile with Organizations read access
            region: Primary AWS region for ACM queries

        Returns:
            CertificateInventoryResult aggregated across all accounts
        """
        result = CertificateInventoryResult()
        result.scan_metadata["scan_mode"] = "multi_account"
        result.scan_metadata["region"] = region
        result.scan_metadata["started_at"] = datetime.utcnow().isoformat()

        mgmt_session = boto3.Session(profile_name=management_profile)

        # Enumerate accounts via Organizations
        try:
            org_client = mgmt_session.client("organizations", region_name="us-east-1")
            paginator = org_client.get_paginator("list_accounts")
            accounts = []
            for page in paginator.paginate():
                for acct in page["Accounts"]:
                    if acct["Status"] == "ACTIVE":
                        accounts.append(acct)
            logger.info(f"Discovered {len(accounts)} active accounts in organization")
        except ClientError as e:
            logger.error(f"Failed to list organization accounts: {e}")
            result.summary.accounts_failed = 1
            result.scan_metadata["error"] = str(e)
            return result

        sts_client = mgmt_session.client("sts", region_name=region)

        for acct in accounts:
            account_id = acct["Id"]
            account_name = acct.get("Name", "")
            try:
                assumed = sts_client.assume_role(
                    RoleArn=f"arn:aws:iam::{account_id}:role/OrganizationAccountAccessRole",
                    RoleSessionName="CertificateInventory",
                )
                creds = assumed["Credentials"]
                session = boto3.Session(
                    aws_access_key_id=creds["AccessKeyId"],
                    aws_secret_access_key=creds["SecretAccessKey"],
                    aws_session_token=creds["SessionToken"],
                )
                acm_certs = self._collect_acm_certificates(session, account_id, account_name, region)
                iam_certs = self._collect_iam_server_certificates(session, account_id, account_name)
                result.certificates.extend(acm_certs)
                result.certificates.extend(iam_certs)
                result.summary.accounts_scanned += 1
                logger.info(
                    f"Scanned account {account_id} ({account_name}): {len(acm_certs)} ACM + {len(iam_certs)} IAM certs"
                )
            except ClientError as e:
                logger.warning(f"Failed to scan account {account_id} ({account_name}): {e}")
                result.summary.accounts_failed += 1
                continue

        result.build_summary()
        result.scan_metadata["finished_at"] = datetime.utcnow().isoformat()
        return result

    def scan_azure_keyvault(self, subscription_id: str) -> List[CertificateRecord]:
        """
        Discover certificates from Azure Key Vault via az CLI.

        Requires `az` CLI authenticated and subscription_id accessible.
        Returns empty list (with warning) if az CLI is unavailable or fails.

        Args:
            subscription_id: Azure subscription ID to scan

        Returns:
            List of CertificateRecord objects with source=AZURE_KEY_VAULT
        """
        certs: List[CertificateRecord] = []

        # List all key vaults in the subscription
        try:
            result = subprocess.run(
                ["az", "keyvault", "list", "--subscription", subscription_id, "--output", "json"],
                capture_output=True,
                text=True,
                timeout=AZURE_SUBPROCESS_TIMEOUT,
            )
            if result.returncode != 0:
                logger.warning(f"az keyvault list failed: {result.stderr}")
                return certs
            vaults = json.loads(result.stdout)
        except (subprocess.TimeoutExpired, FileNotFoundError, json.JSONDecodeError) as e:
            logger.warning(f"Azure Key Vault scan skipped: {e}")
            return certs

        for vault in vaults:
            vault_name = vault.get("name", "")
            vault_uri = vault.get("properties", {}).get("vaultUri", "")
            try:
                list_result = subprocess.run(
                    [
                        "az",
                        "keyvault",
                        "certificate",
                        "list",
                        "--vault-name",
                        vault_name,
                        "--subscription",
                        subscription_id,
                        "--output",
                        "json",
                    ],
                    capture_output=True,
                    text=True,
                    timeout=AZURE_SUBPROCESS_TIMEOUT,
                )
                if list_result.returncode != 0:
                    logger.warning(f"az keyvault certificate list failed for {vault_name}: {list_result.stderr}")
                    continue
                vault_certs = json.loads(list_result.stdout)
            except (subprocess.TimeoutExpired, json.JSONDecodeError) as e:
                logger.warning(f"Failed to list certs in vault {vault_name}: {e}")
                continue

            for vc in vault_certs:
                cert_id = vc.get("id", "")
                cert_name = vc.get("name", cert_id)
                enabled = vc.get("attributes", {}).get("enabled", True)
                expires_raw = vc.get("attributes", {}).get("expires")
                not_before_raw = vc.get("attributes", {}).get("notBefore")

                not_after: Optional[datetime] = None
                not_before: Optional[datetime] = None
                if expires_raw:
                    try:
                        not_after = datetime.utcfromtimestamp(expires_raw)
                    except (TypeError, ValueError):
                        pass
                if not_before_raw:
                    try:
                        not_before = datetime.utcfromtimestamp(not_before_raw)
                    except (TypeError, ValueError):
                        pass

                status = CertificateStatus.ISSUED if enabled else CertificateStatus.INACTIVE

                record = CertificateRecord(
                    certificate_arn=cert_id,
                    domain_name=cert_name,
                    account_id=subscription_id,
                    account_name=vault_name,
                    region="azure",
                    source=CertificateSource.AZURE_KEY_VAULT,
                    status=status,
                    not_before=not_before,
                    not_after=not_after,
                    tags=vc.get("tags") or {},
                    managed_by=ManagedBy.UNKNOWN,
                )
                certs.append(record)

        logger.info(f"Azure Key Vault scan complete: {len(certs)} certificates across {len(vaults)} vaults")
        return certs

    # ------------------------------------------------------------------
    # Internal collection helpers
    # ------------------------------------------------------------------

    def _collect_acm_certificates(
        self,
        session: boto3.Session,
        account_id: str,
        account_name: str,
        region: str,
    ) -> List[CertificateRecord]:
        """Paginate ACM list_certificates and describe each one."""
        certs: List[CertificateRecord] = []
        acm = self.get_client("acm", region)

        try:
            paginator = acm.get_paginator("list_certificates")
            arns: List[str] = []
            for page in paginator.paginate():
                for item in page.get("CertificateSummaryList", []):
                    arns.append(item["CertificateArn"])
        except ClientError as e:
            logger.warning(f"ACM list_certificates failed in {region} account {account_id}: {e}")
            return certs

        for arn in arns:
            try:
                detail = acm.describe_certificate(CertificateArn=arn)["Certificate"]
            except ClientError as e:
                logger.warning(f"ACM describe_certificate failed for {arn}: {e}")
                continue

            tags = self._get_acm_tags(acm, arn)
            managed_by = self._determine_managed_by(
                tags,
                detail.get("Type", ""),
                detail.get("Issuer", ""),
            )

            # Determine in-use status and usage list
            in_use_arns: List[str] = detail.get("InUseBy", []) or []
            in_use = len(in_use_arns) > 0
            usage = self._map_certificate_usage(arn, region, in_use_arns, session)

            # Parse timestamps
            not_before = detail.get("NotBefore")
            not_after = detail.get("NotAfter")

            # Auto-renewal: ACM AMAZON_ISSUED certs with ISSUED status are eligible
            cert_type = detail.get("Type", "")
            status_str = detail.get("Status", "ISSUED")
            auto_renewal = cert_type == "AMAZON_ISSUED" and status_str == "ISSUED"

            # RenewalSummary is present in describe_certificate response at no extra cost
            renewal_summary = detail.get("RenewalSummary", {})
            renewal_status = renewal_summary.get("RenewalStatus", "") if renewal_summary else ""

            record = CertificateRecord(
                certificate_arn=arn,
                domain_name=detail.get("DomainName", ""),
                subject_alternative_names=detail.get("SubjectAlternativeNames") or [],
                account_id=account_id,
                account_name=account_name,
                region=region,
                source=CertificateSource.ACM,
                status=_ACM_STATUS_MAP.get(status_str, CertificateStatus.INACTIVE),
                issuer=detail.get("Issuer", ""),
                key_algorithm=detail.get("KeyAlgorithm", ""),
                certificate_type=cert_type,
                not_before=not_before,
                not_after=not_after,
                in_use=in_use,
                usage=usage,
                auto_renewal_eligible=auto_renewal,
                renewal_status=renewal_status,
                owner_team=tags.get("Owner", tags.get("Team", "UNKNOWN")),
                managed_by=managed_by,
                tags=tags,
            )
            certs.append(record)

        return certs

    def _collect_iam_server_certificates(
        self,
        session: boto3.Session,
        account_id: str,
        account_name: str,
    ) -> List[CertificateRecord]:
        """List IAM server certificates (global service, no region needed)."""
        certs: List[CertificateRecord] = []
        iam = session.client("iam")

        try:
            paginator = iam.get_paginator("list_server_certificates")
            for page in paginator.paginate():
                for meta in page.get("ServerCertificateMetadataList", []):
                    arn = meta.get("Arn", "")
                    not_after = meta.get("Expiration")

                    tags = self._get_iam_cert_tags(iam, meta.get("ServerCertificateName", ""))
                    managed_by = self._determine_managed_by(tags, "IMPORTED", "")

                    # Determine status from expiry date (IAM API has no status field)
                    iam_status = CertificateStatus.ISSUED
                    if not_after is not None:
                        try:
                            now_utc = datetime.now(tz=timezone.utc)
                            exp_naive = (
                                not_after.replace(tzinfo=None)
                                if hasattr(not_after, "tzinfo") and not_after.tzinfo
                                else not_after
                            )
                            now_naive = now_utc.replace(tzinfo=None)
                            if exp_naive < now_naive:
                                iam_status = CertificateStatus.EXPIRED
                        except (AttributeError, TypeError):
                            pass  # Cannot parse expiry — default to ISSUED

                    record = CertificateRecord(
                        certificate_arn=arn,
                        domain_name=meta.get("ServerCertificateName", ""),
                        account_id=account_id,
                        account_name=account_name,
                        region="global",
                        source=CertificateSource.IAM_SERVER_CERT,
                        status=iam_status,
                        issuer="",
                        key_algorithm="",
                        certificate_type="IMPORTED",
                        not_before=meta.get("UploadDate"),
                        not_after=not_after,
                        in_use=False,  # IAM server certs usage is not easily queryable
                        usage=[],
                        auto_renewal_eligible=False,
                        owner_team=tags.get("Owner", tags.get("Team", "UNKNOWN")),
                        managed_by=managed_by,
                        tags=tags,
                    )
                    certs.append(record)
        except ClientError as e:
            logger.warning(f"IAM list_server_certificates failed for account {account_id}: {e}")

        return certs

    def _map_certificate_usage(
        self,
        cert_arn: str,
        region: str,
        in_use_arns: List[str],
        session: boto3.Session,
    ) -> List[CertificateUsage]:
        """
        Cross-reference certificate usage across ELBv2, Classic ELB, CloudFront, API GW.

        Skips on error (lightweight read-only scan, not fail-safe like acm_remediation).
        CloudFront certificates must be queried from us-east-1.
        """
        usage: List[CertificateUsage] = []

        # ELBv2 (ALB / NLB)
        try:
            elbv2 = self.get_client("elbv2", region)
            lb_paginator = elbv2.get_paginator("describe_load_balancers")
            for page in lb_paginator.paginate():
                for lb in page.get("LoadBalancers", []):
                    lb_arn = lb["LoadBalancerArn"]
                    lb_name = lb.get("LoadBalancerName", "")
                    try:
                        listeners_resp = elbv2.describe_listeners(LoadBalancerArn=lb_arn)
                        for listener in listeners_resp.get("Listeners", []):
                            for cert_ref in listener.get("Certificates", []):
                                if cert_ref.get("CertificateArn") == cert_arn:
                                    usage.append(
                                        CertificateUsage(
                                            resource_type="ALB/NLB",
                                            resource_arn=lb_arn,
                                            resource_name=lb_name,
                                            listener_arn=listener.get("ListenerArn"),
                                        )
                                    )
                    except ClientError as e:
                        logger.warning(f"Could not describe listeners for {lb_arn}: {e}")
                        continue
        except ClientError as e:
            logger.warning(f"ELBv2 usage check skipped for {cert_arn}: {e}")

        # Classic ELB
        try:
            elb = self.get_client("elb", region)
            classic_resp = elb.describe_load_balancers()
            for lb in classic_resp.get("LoadBalancerDescriptions", []):
                lb_name = lb.get("LoadBalancerName", "")
                for ld in lb.get("ListenerDescriptions", []):
                    if ld.get("Listener", {}).get("SSLCertificateId") == cert_arn:
                        usage.append(
                            CertificateUsage(
                                resource_type="Classic ELB",
                                resource_arn="",
                                resource_name=lb_name,
                            )
                        )
        except ClientError as e:
            logger.warning(f"Classic ELB usage check skipped for {cert_arn}: {e}")

        # CloudFront — certificates must be in us-east-1 to attach to distributions
        try:
            cf = self.get_client("cloudfront", ACM_CLOUDFRONT_REGION)
            cf_paginator = cf.get_paginator("list_distributions")
            for page in cf_paginator.paginate():
                dist_list = page.get("DistributionList", {})
                for dist in dist_list.get("Items", []):
                    viewer_cert = dist.get("ViewerCertificate", {})
                    if viewer_cert.get("ACMCertificateArn") == cert_arn:
                        usage.append(
                            CertificateUsage(
                                resource_type="CloudFront",
                                resource_arn=dist.get("ARN", ""),
                                resource_name=dist.get("DomainName", dist.get("Id", "")),
                            )
                        )
        except ClientError as e:
            logger.warning(f"CloudFront usage check skipped for {cert_arn}: {e}")

        # API Gateway (REST APIs)
        try:
            apigw = self.get_client("apigateway", region)
            domains_resp = apigw.get_domain_names()
            for domain in domains_resp.get("items", []):
                if domain.get("certificateArn") == cert_arn or domain.get("regionalCertificateArn") == cert_arn:
                    usage.append(
                        CertificateUsage(
                            resource_type="API Gateway",
                            resource_arn="",
                            resource_name=domain.get("domainName", ""),
                        )
                    )
        except ClientError as e:
            logger.warning(f"API Gateway usage check skipped for {cert_arn}: {e}")

        return usage

    def _determine_managed_by(self, tags: Dict[str, str], cert_type: str, issuer: str) -> ManagedBy:
        """
        Heuristic determination of who manages this certificate.

        Priority order:
        1. ManagedBy tag if present
        2. Let's Encrypt issuer
        3. AWS auto-renew (AMAZON_ISSUED)
        4. Known team tags (Owner, Team)
        5. UNKNOWN
        """
        # Explicit tag takes highest priority
        managed_tag = tags.get("ManagedBy", tags.get("managed-by", "")).upper()
        if "DATACOM" in managed_tag:
            return ManagedBy.DATACOM
        if "BLUECURRENT" in managed_tag:
            return ManagedBy.BLUECURRENT
        if "LETS_ENCRYPT" in managed_tag or "LETSENCRYPT" in managed_tag:
            return ManagedBy.LETS_ENCRYPT
        if "AWS" in managed_tag:
            return ManagedBy.AWS_AUTO_RENEW

        # Issuer-based detection
        if "Let's Encrypt" in issuer or "letsencrypt" in issuer.lower():
            return ManagedBy.LETS_ENCRYPT

        # AWS-issued certificates are managed by AWS auto-renew
        if cert_type == "AMAZON_ISSUED":
            return ManagedBy.AWS_AUTO_RENEW

        # Owner / Team tag fallback
        owner = tags.get("Owner", tags.get("Team", "")).upper()
        if "DATACOM" in owner:
            return ManagedBy.DATACOM
        if "BLUECURRENT" in owner:
            return ManagedBy.BLUECURRENT

        return ManagedBy.UNKNOWN

    def _get_acm_tags(self, acm_client: Any, cert_arn: str) -> Dict[str, str]:
        """Fetch ACM certificate tags, returning empty dict on error."""
        try:
            resp = acm_client.list_tags_for_certificate(CertificateArn=cert_arn)
            return {t["Key"]: t["Value"] for t in resp.get("Tags", [])}
        except ClientError as e:
            logger.warning(f"Could not fetch tags for {cert_arn}: {e}")
            return {}

    def _get_iam_cert_tags(self, iam_client: Any, cert_name: str) -> Dict[str, str]:
        """Fetch IAM server certificate tags, returning empty dict on error."""
        try:
            resp = iam_client.list_server_certificate_tags(ServerCertificateName=cert_name)
            return {t["Key"]: t["Value"] for t in resp.get("Tags", [])}
        except ClientError as e:
            logger.warning(f"Could not fetch IAM cert tags for {cert_name}: {e}")
            return {}
