"""Multi-cloud certificate inventory collector (READONLY).

Discovers SSL/TLS certificates across:
- AWS ACM (single-account or org-wide via Config Aggregator)
- AWS IAM Server Certificates (legacy)
- Azure Key Vault Certificates
"""

import csv
import json
import logging
import shlex
import subprocess
import time
from collections import Counter
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, List, Optional

import boto3
from botocore.config import Config as BotoConfig
from botocore.exceptions import ClientError
from rich.table import Table

from runbooks.common.rich_utils import console
from runbooks.security.cert_types import CertificateRecord, RiskLevel

logger = logging.getLogger(__name__)


class CertificateInventoryCollector:
    """Multi-cloud certificate inventory (READONLY only)."""

    def discover_aws_acm_org_wide(
        self, session: boto3.Session, region: str = "ap-southeast-2"
    ) -> List[CertificateRecord]:
        """Discover ACM certificates org-wide via Config Aggregator."""
        config_client = session.client(
            "config",
            region_name=region,
            config=BotoConfig(retries={"max_attempts": 3, "mode": "adaptive"}),
        )

        try:
            resp = config_client.describe_configuration_aggregators()
            aggregators = resp.get("ConfigurationAggregators", [])
            if not aggregators:
                logger.warning("No Config Aggregator found — falling back to single-account")
                return []
            aggregator_name = aggregators[0]["ConfigurationAggregatorName"]
        except ClientError as e:
            logger.warning(f"Config Aggregator detection failed: {e}")
            return []

        expression = (
            "SELECT resourceId, accountId, awsRegion, configuration WHERE resourceType = 'AWS::ACM::Certificate'"
        )
        certs = []
        next_token = None

        while True:
            kwargs: Dict[str, Any] = {
                "ConfigurationAggregatorName": aggregator_name,
                "Expression": expression,
            }
            if next_token:
                kwargs["NextToken"] = next_token

            try:
                resp = config_client.select_aggregate_resource_config(**kwargs)
            except ClientError as e:
                logger.error(f"Config Aggregator query failed: {e}")
                break

            for result_str in resp.get("Results", []):
                try:
                    r = json.loads(result_str)
                    config = r.get("configuration", {})
                    not_after_str = config.get("notAfter", "")
                    not_after = None
                    if not_after_str:
                        try:
                            not_after = datetime.fromisoformat(not_after_str.replace("Z", "+00:00"))
                        except ValueError:
                            pass

                    in_use_by = config.get("inUseBy", [])
                    renewal = config.get("renewalSummary", {})

                    certs.append(
                        CertificateRecord(
                            domain=config.get("domainName", "N/A"),
                            identifier=r.get("resourceId", ""),
                            cloud="aws",
                            source="acm",
                            account_or_subscription=r.get("accountId", ""),
                            region=r.get("awsRegion", ""),
                            status=config.get("status", "UNKNOWN"),
                            cert_type=config.get("type", "UNKNOWN"),
                            issuer=config.get("issuer", "N/A"),
                            not_after=not_after,
                            in_use=len(in_use_by) > 0,
                            used_by=in_use_by,
                            renewal_status=renewal.get("renewalStatus", "N/A"),
                            key_algorithm=config.get("keyAlgorithm", "N/A"),
                        )
                    )
                except (json.JSONDecodeError, KeyError) as e:
                    logger.debug(f"Failed to parse certificate record: {e}")

            next_token = resp.get("NextToken")
            if not next_token:
                break

        return certs

    def discover_aws_acm_single(
        self, session: boto3.Session, region: str = "ap-southeast-2"
    ) -> List[CertificateRecord]:
        """Discover ACM certificates in a single account."""
        _retry = BotoConfig(retries={"max_attempts": 3, "mode": "adaptive"})
        acm_client = session.client("acm", region_name=region, config=_retry)
        sts_client = session.client("sts", region_name=region, config=_retry)

        try:
            account_id = sts_client.get_caller_identity()["Account"]
        except ClientError:
            account_id = "unknown"

        key_types = [
            "RSA_1024",
            "RSA_2048",
            "RSA_3072",
            "RSA_4096",
            "EC_prime256v1",
            "EC_secp384r1",
            "EC_secp521r1",
        ]
        certs = []
        paginator = acm_client.get_paginator("list_certificates")
        for page in paginator.paginate(Includes={"keyTypes": key_types}):
            for summary in page.get("CertificateSummaryList", []):
                not_after = summary.get("NotAfter")
                in_use_by: List[str] = []
                try:
                    detail = acm_client.describe_certificate(CertificateArn=summary["CertificateArn"])
                    in_use_by = detail["Certificate"].get("InUseBy", [])
                except ClientError:
                    pass

                issuer = "Amazon" if summary.get("Type") == "AMAZON_ISSUED" else "Imported"
                certs.append(
                    CertificateRecord(
                        domain=summary.get("DomainName", "N/A"),
                        identifier=summary["CertificateArn"],
                        cloud="aws",
                        source="acm",
                        account_or_subscription=account_id,
                        region=region,
                        status=summary.get("Status", "UNKNOWN"),
                        cert_type=summary.get("Type", "UNKNOWN"),
                        issuer=issuer,
                        not_after=not_after,
                        in_use=summary.get("InUse", False),
                        used_by=in_use_by,
                        renewal_status=summary.get("RenewalEligibility", "N/A"),
                        key_algorithm=summary.get("KeyAlgorithm", "N/A"),
                    )
                )
        return certs

    def discover_aws_iam_server_certs(self, session: boto3.Session) -> List[CertificateRecord]:
        """Discover legacy IAM server certificates."""
        _retry = BotoConfig(retries={"max_attempts": 3, "mode": "adaptive"})
        iam_client = session.client("iam", config=_retry)
        sts_client = session.client("sts", config=_retry)

        try:
            account_id = sts_client.get_caller_identity()["Account"]
        except ClientError:
            account_id = "unknown"

        certs = []
        try:
            paginator = iam_client.get_paginator("list_server_certificates")
            for resp in paginator.paginate():
                for meta in resp.get("ServerCertificateMetadataList", []):
                    not_after = meta.get("Expiration")
                    now = datetime.now(timezone.utc)
                    expired = not_after and not_after.replace(tzinfo=timezone.utc) < now if not_after else False
                    certs.append(
                        CertificateRecord(
                            domain=meta.get("ServerCertificateName", "N/A"),
                            identifier=meta.get("Arn", ""),
                            cloud="aws",
                            source="iam-server-cert",
                            account_or_subscription=account_id,
                            region="global",
                            status="EXPIRED" if expired else "ACTIVE",
                            cert_type="IAM_SERVER",
                            issuer="N/A",
                            not_after=not_after,
                            in_use=False,
                        )
                    )
        except ClientError as e:
            logger.warning(f"IAM server certificate listing failed: {e}")
        return certs

    def discover_azure_keyvault(self, subscription_id: Optional[str] = None) -> List[CertificateRecord]:
        """Discover Azure Key Vault certificates via az CLI."""
        sub_arg = f"--subscription {subscription_id}" if subscription_id else ""

        vaults_result = self._run_az_cli(f"keyvault list {sub_arg}")
        if not vaults_result["success"] or not isinstance(vaults_result["data"], list):
            return []

        certs = []
        access_denied = []

        for vault in vaults_result["data"]:
            vault_name = vault.get("name", "")
            cert_result = self._run_az_cli(f"keyvault certificate list --vault-name {vault_name}")
            if not cert_result["success"]:
                err = cert_result.get("error") or ""
                if "Forbidden" in err or "403" in err:
                    access_denied.append(vault_name)
                continue

            for cert_data in cert_result["data"] or []:
                cert_name = cert_data.get("name", "N/A")
                detail = self._run_az_cli(f"keyvault certificate show --vault-name {vault_name} --name {cert_name}")
                if not detail["success"]:
                    continue

                d = detail.get("data") or {}
                attrs = d.get("attributes", {})
                policy = d.get("policy", {})
                x509 = policy.get("x509CertificateProperties", {})

                expires_str = attrs.get("expires", "")
                not_after = None
                if expires_str:
                    try:
                        not_after = datetime.fromisoformat(expires_str.replace("Z", "+00:00"))
                    except ValueError:
                        pass

                now = datetime.now(timezone.utc)
                expired = not_after and not_after < now if not_after else False
                certs.append(
                    CertificateRecord(
                        domain=x509.get("subject", cert_name),
                        identifier=d.get("id", ""),
                        cloud="azure",
                        source="key-vault",
                        account_or_subscription=subscription_id or "default",
                        region=vault.get("location", "N/A"),
                        status="EXPIRED" if expired else "ACTIVE",
                        cert_type="KEY_VAULT",
                        issuer=policy.get("issuerParameters", {}).get("name", "N/A"),
                        not_after=not_after,
                        in_use=False,
                        used_by=[],
                        key_algorithm=policy.get("keyProperties", {}).get("keyType", "N/A"),
                        tags=d.get("tags", {}),
                    )
                )

        if access_denied:
            console.print(
                f"[yellow]Access denied to {len(access_denied)} vault(s): {', '.join(access_denied)}[/yellow]"
            )

        return certs

    @staticmethod
    def _run_az_cli(command: str, timeout: int = 120, max_attempts: int = 3) -> Dict[str, Any]:
        """Execute Azure CLI command with retry for transient failures."""
        full_command = shlex.split(f"az {command} -o json")
        last_error = None
        for attempt in range(1, max_attempts + 1):
            try:
                result = subprocess.run(full_command, capture_output=True, text=True, timeout=timeout)
                if result.returncode == 0:
                    data = json.loads(result.stdout) if result.stdout.strip() else {}
                    return {"success": True, "data": data, "error": None}
                last_error = result.stderr
                if attempt < max_attempts:
                    time.sleep(attempt)
                    continue
            except subprocess.TimeoutExpired:
                return {"success": False, "data": None, "error": f"Timed out after {timeout}s"}
            except json.JSONDecodeError as e:
                return {"success": False, "data": None, "error": f"JSON parse error: {e}"}
            except Exception as e:
                return {"success": False, "data": None, "error": str(e)}
        return {"success": False, "data": None, "error": last_error}

    @staticmethod
    def render_table(certs: List[CertificateRecord], threshold_days: int = 90) -> None:
        """Render certificate inventory as Rich table, sorted by risk."""
        table = Table(title=f"Certificate Inventory ({len(certs)} certificates)", show_lines=True)
        table.add_column("#", style="dim", width=4)
        table.add_column("Domain", style="cyan", max_width=50)
        table.add_column("Account/Sub", style="dim", width=14)
        table.add_column("Source", width=10)
        table.add_column("Status", width=10)
        table.add_column("Expiry", width=12)
        table.add_column("Days", justify="right", width=6)
        table.add_column("InUse", width=5)
        table.add_column("Renewal", width=12)
        table.add_column("Risk", width=10)

        risk_style = {
            RiskLevel.CRITICAL.value: "bold red",
            RiskLevel.HIGH.value: "red",
            RiskLevel.MEDIUM.value: "yellow",
            RiskLevel.LOW.value: "green",
            RiskLevel.OK.value: "dim green",
        }
        risk_order = {
            RiskLevel.CRITICAL.value: 0,
            RiskLevel.HIGH.value: 1,
            RiskLevel.MEDIUM.value: 2,
            RiskLevel.LOW.value: 3,
            RiskLevel.OK.value: 4,
        }

        sorted_certs = sorted(
            certs,
            key=lambda c: (risk_order.get(c.risk_level, 5), c.days_until_expiry or 9999),
        )

        row_num = 0
        for c in sorted_certs:
            if (
                c.days_until_expiry is not None
                and c.days_until_expiry > threshold_days
                and c.risk_level == RiskLevel.OK.value
            ):
                continue
            row_num += 1
            style = risk_style.get(c.risk_level, "")
            days_str = str(c.days_until_expiry) if c.days_until_expiry is not None else "N/A"
            expiry_str = c.not_after.strftime("%Y-%m-%d") if c.not_after else "N/A"
            table.add_row(
                str(row_num),
                c.domain,
                c.account_or_subscription[:14],
                c.source,
                c.status,
                expiry_str,
                days_str,
                "Yes" if c.in_use else "No",
                c.renewal_status,
                f"[{style}]{c.risk_level}[/{style}]",
            )

        console.print(table)

        risks = Counter(c.risk_level for c in certs)
        console.print(
            f"\n[bold]Summary:[/bold] {len(certs)} certificates | "
            f"[bold red]{risks.get(RiskLevel.CRITICAL.value, 0)} CRITICAL[/bold red] | "
            f"[red]{risks.get(RiskLevel.HIGH.value, 0)} HIGH[/red] | "
            f"[yellow]{risks.get(RiskLevel.MEDIUM.value, 0)} MEDIUM[/yellow] | "
            f"[green]"
            f"{risks.get(RiskLevel.LOW.value, 0) + risks.get(RiskLevel.OK.value, 0)} OK"
            f"[/green]"
        )

    @staticmethod
    def export_csv(certs: List[CertificateRecord], path: str) -> None:
        """Export certificate inventory to CSV."""
        Path(path).parent.mkdir(parents=True, exist_ok=True)
        with open(path, "w", newline="") as f:
            writer = csv.writer(f)
            writer.writerow(
                [
                    "Domain",
                    "Cloud",
                    "Source",
                    "Account",
                    "Region",
                    "Status",
                    "Type",
                    "Issuer",
                    "Expiry",
                    "DaysLeft",
                    "InUse",
                    "Renewal",
                    "Risk",
                    "KeyAlgo",
                    "Identifier",
                ]
            )
            for c in certs:
                writer.writerow(
                    [
                        c.domain,
                        c.cloud,
                        c.source,
                        c.account_or_subscription,
                        c.region,
                        c.status,
                        c.cert_type,
                        c.issuer,
                        c.not_after.strftime("%Y-%m-%d") if c.not_after else "",
                        c.days_until_expiry,
                        c.in_use,
                        c.renewal_status,
                        c.risk_level,
                        c.key_algorithm,
                        c.identifier,
                    ]
                )
        console.print(f"[green]CSV exported: {path}[/green]")
