"""complyform assess command — compliance assessment against frameworks."""

from __future__ import annotations

import json
import sys
import time
from concurrent.futures import Future, ThreadPoolExecutor, as_completed
from typing import TYPE_CHECKING, Annotated, Any

import typer

from complyform.cli.exit_codes import EXIT_COMPLIANCE_FAILURE, EXIT_SUCCESS

if TYPE_CHECKING:
    from complyform.core.cloud_intelligence import CloudIntelligenceResult
    from complyform.core.scanner.landing_zone import LandingZoneContext
    from complyform.reporting.json_report import AssessmentMetadata
from complyform.cli.main import AppContext, app
from complyform.core.assessor.control_matcher import ControlMatcher, Finding
from complyform.core.assessor.coverage import ResourceCoverageClassifier
from complyform.core.assessor.scoring import compute_compliance_score
from complyform.core.cache import ScanCache
from complyform.core.errors import NoCachedAssessmentError, UnknownFrameworkError
from complyform.core.profiles.cross_mappings import get_all as get_all_cross_mappings
from complyform.core.profiles.mapping_loader import load_mappings
from complyform.core.profiles.models import CrossMappingIndex
from complyform.core.profiles.registry import get_framework

_BF = "Brownfield (Existing Infrastructure)"

CLOUD_INTELLIGENCE_FEATURE_MAP: dict[str, str] = {
    "iam_analysis": "iam_analysis",
    "cross_validate": "cross_validation",
    "estimate_cost": "cost_impact",
}


@app.command(name="assess", rich_help_panel=_BF)
def assess_cmd(  # noqa: PLR0913
    ctx: typer.Context,
    frameworks: Annotated[
        str,
        typer.Option(help="Comma-separated framework IDs to assess against"),
    ] = "",
    format: Annotated[
        str,
        typer.Option("--format", help="Output format: json, terminal, html, sarif"),
    ] = "terminal",
    severity: Annotated[
        str,
        typer.Option(help="Filter findings: critical, high, all"),
    ] = "all",
    attest: Annotated[
        bool,
        typer.Option(
            "--attest", help="Produce in-toto v1 attestation alongside output"
        ),
    ] = False,
    attest_output: Annotated[
        str,
        typer.Option("--attest-output", help="Attestation output path"),
    ] = "./attestation.json",
    explain: Annotated[
        bool,
        typer.Option("--explain", help="Append explanations for FAIL findings"),
    ] = False,
    no_upgrade_hints: Annotated[
        bool,
        typer.Option("--no-upgrade-hints", help="Suppress framework overlap upsell"),
    ] = False,
    cross_validate: Annotated[
        bool,
        typer.Option(
            "--cross-validate", help="Team+: Cross-validate with cloud-native CSPM"
        ),
    ] = False,
    iam_analysis: Annotated[
        bool,
        typer.Option(
            "--iam-analysis", help="Team+: Include IAM least-privilege analysis"
        ),
    ] = False,
    estimate_cost: Annotated[
        bool,
        typer.Option(
            "--estimate-cost", help="Team+: Compliance cost impact estimation"
        ),
    ] = False,
) -> None:
    """Assess compliance posture against selected frameworks."""
    from complyform.core.auth.license import LicenseManager
    from complyform.core.auth.tier_enforcement import (
        check_feature,
        check_framework_access,
        enforce_with_refresh,
    )

    license_mgr = LicenseManager()
    tier_info = license_mgr.resolve_tier_info()

    fw_ids = [f.strip() for f in frameworks.split(",") if f.strip()]
    if fw_ids:
        tier_info = enforce_with_refresh(
            license_mgr,
            tier_info,
            check_framework_access,
            fw_ids,
            "assess",
            tier_info,
        )

    if attest:
        import contextlib

        with contextlib.suppress(Exception):
            tier_info = enforce_with_refresh(
                license_mgr,
                tier_info,
                check_feature,
                "signed_attestations",
                tier_info,
            )

    # Tier-gate each requested CI feature
    for flag_name, feature_key in CLOUD_INTELLIGENCE_FEATURE_MAP.items():
        if locals()[flag_name]:
            tier_info = enforce_with_refresh(
                license_mgr,
                tier_info,
                check_feature,
                feature_key,
                tier_info,
            )

    _run_assess(
        ctx=ctx,
        frameworks_str=frameworks,
        format=format,
        severity=severity,
        attest=attest,
        attest_output=attest_output,
        no_upgrade_hints=no_upgrade_hints,
        explain=explain,
        iam_analysis=iam_analysis,
        cross_validate=cross_validate,
        estimate_cost=estimate_cost,
    )


def _run_assess(  # noqa: PLR0913
    *,
    ctx: typer.Context,
    frameworks_str: str,
    format: str,
    severity: str,
    attest: bool,
    attest_output: str,
    no_upgrade_hints: bool,
    explain: bool = False,
    iam_analysis: bool = False,
    cross_validate: bool = False,
    estimate_cost: bool = False,
) -> None:
    """Core assess logic shared by assess command and scan --assess."""
    from rich.console import Console

    app_ctx: AppContext = ctx.obj
    start_time = time.monotonic()

    fw_ids = [f.strip() for f in frameworks_str.split(",") if f.strip()]
    if not fw_ids:
        raise UnknownFrameworkError(["(none)"], _get_valid_ids())

    for fw_id in fw_ids:
        get_framework(fw_id)

    cache = ScanCache()
    cached = cache.latest()
    if cached is None:
        raise NoCachedAssessmentError(", ".join(fw_ids))

    resources, cache_meta = cached
    cloud: str = cache_meta.get("cloud", "gcp")
    state_hash: str = cache_meta.get("state_hash", "")
    provider_versions: dict[str, str] = cache_meta.get("provider_versions", {})
    credentials_flag: str | None = cache_meta.get("credentials")
    project_id: str = cache_meta.get("project_id", "")

    all_cross = get_all_cross_mappings()
    cross_index = CrossMappingIndex(schema_version=1, mappings=all_cross)

    matcher = ControlMatcher()
    all_findings: list[Finding] = []

    for fw_id in fw_ids:
        mapping_file = load_mappings(fw_id, cloud)
        findings = matcher.match(resources, mapping_file, cross_index)

        applicable_types = frozenset(
            rt for rm in mapping_file.mappings for rt in rm.resource_types
        )
        not_applicable_types: frozenset[str] = frozenset()
        classifier = ResourceCoverageClassifier(applicable_types, not_applicable_types)
        not_assessed = classifier.generate_not_assessed_findings(resources, fw_id)

        all_findings.extend(findings)
        all_findings.extend(not_assessed)

    # --- Landing zone context ---
    landing_zone_context: LandingZoneContext | None = None
    lz_raw = cache_meta.get("landing_zone")
    if lz_raw is not None:
        from complyform.core.scanner.landing_zone import LandingZoneContext as LZCtx

        landing_zone_context = LZCtx(**lz_raw) if isinstance(lz_raw, dict) else None

    console_obj = app_ctx.console
    if landing_zone_context and landing_zone_context.detected:
        from complyform.core.cloud_intelligence.assessment_modifier import (
            apply_landing_zone_context,
        )

        all_findings, controls_skipped = apply_landing_zone_context(
            all_findings, landing_zone_context
        )
        if controls_skipped > 0 and isinstance(console_obj, Console):
            Console(stderr=True).print(
                f"[dim]{controls_skipped} controls satisfied by landing zone"
                f" ({landing_zone_context.type})[/dim]",
            )

    # --- Cloud intelligence orchestration ---
    ci_results: dict[str, CloudIntelligenceResult] = {}

    if iam_analysis or cross_validate or estimate_cost:
        ci_results = _run_cloud_intelligence(
            cloud=cloud,
            project_id=project_id,
            credentials_flag=credentials_flag,
            iam_analysis=iam_analysis,
            cross_validate=cross_validate,
            estimate_cost=estimate_cost,
            findings=all_findings,
            console=console_obj,
        )

    # --- Merge IAM findings ---
    if "iam_analysis" in ci_results and ci_results["iam_analysis"].success:
        from complyform.core.cloud_intelligence.iam_analyzer import (
            IAMAnalysisResult,
            iam_finding_to_findings,
        )

        iam_result = ci_results["iam_analysis"]
        if isinstance(iam_result, IAMAnalysisResult):
            for iam_finding in iam_result.findings:
                all_findings.extend(iam_finding_to_findings(iam_finding, fw_ids))

    # Recompute score after IAM merge
    score = compute_compliance_score(all_findings)
    elapsed_ms = int((time.monotonic() - start_time) * 1000)

    _cache_findings(all_findings, cache)
    _cache_cloud_intelligence(cache, ci_results, landing_zone_context)

    summary = _build_summary(all_findings, score)
    overlap_hints = None
    if not no_upgrade_hints and format == "terminal" and not app_ctx.quiet:
        from complyform.core.assessor.overlap_hints import compute_overlap_hints
        from complyform.core.profiles.registry import list_frameworks

        all_fw_ids = frozenset(f.id for f in list_frameworks())
        overlap_hints = compute_overlap_hints(
            all_findings, cross_index, frozenset(fw_ids), all_fw_ids
        )

    explanations = None
    if explain:
        from complyform.cli.explain_cmd import explain_findings_inline

        explanations = explain_findings_inline(all_findings)

    if format == "json":
        _output_json(
            all_findings,
            summary,
            cloud=cloud,
            state_hash=state_hash,
            frameworks=fw_ids,
            elapsed_ms=elapsed_ms,
            tier=app_ctx.config.tier,
            resource_count=len(resources),
            provider_versions=provider_versions,
            upgrade_hints=overlap_hints,
            explanations=explanations,
            ci_results=ci_results if ci_results else None,
            landing_zone=landing_zone_context,
        )
    elif format == "sarif":
        _output_sarif(all_findings, cloud=cloud, frameworks=fw_ids)
    else:
        _output_terminal(
            app_ctx,
            all_findings,
            summary,
            frameworks=fw_ids,
            cloud=cloud,
            resource_count=len(resources),
            overlap_hints=overlap_hints,
            explanations=explanations,
        )

    if attest:
        _write_attestation(
            all_findings,
            summary,
            cloud=cloud,
            state_hash=state_hash,
            frameworks=fw_ids,
            elapsed_ms=elapsed_ms,
            tier=app_ctx.config.tier,
            resource_count=len(resources),
            provider_versions=provider_versions,
            attest_output=attest_output,
        )

    has_failures = _has_relevant_failures(all_findings, severity)
    raise typer.Exit(code=EXIT_COMPLIANCE_FAILURE if has_failures else EXIT_SUCCESS)


def _get_valid_ids() -> list[str]:
    from complyform.core.profiles.registry import list_frameworks

    return [f.id for f in list_frameworks()]


def _run_cloud_intelligence(  # noqa: PLR0913
    *,
    cloud: str,
    project_id: str,
    credentials_flag: str | None,
    iam_analysis: bool,
    cross_validate: bool,
    estimate_cost: bool,
    findings: list[Finding],
    console: object,
) -> dict[str, CloudIntelligenceResult]:
    """Run enabled CI features concurrently via ThreadPoolExecutor."""
    from rich.console import Console

    from complyform.core.cloud_intelligence import resolve_cloud_credentials
    from complyform.core.cloud_intelligence.iam_aws import AWSIAMAnalyzer
    from complyform.core.cloud_intelligence.iam_azure import AzureIAMAnalyzer
    from complyform.core.cloud_intelligence.iam_gcp import GCPIAMAnalyzer
    from complyform.core.cloud_intelligence.xval_aws import AWSCrossValidator
    from complyform.core.cloud_intelligence.xval_azure import AzureCrossValidator
    from complyform.core.cloud_intelligence.xval_gcp import GCPCrossValidator

    IAM_ANALYZER_MAP: dict[str, type] = {
        "gcp": GCPIAMAnalyzer,
        "aws": AWSIAMAnalyzer,
        "azure": AzureIAMAnalyzer,
    }
    CROSS_VALIDATOR_MAP: dict[str, type] = {
        "gcp": GCPCrossValidator,
        "aws": AWSCrossValidator,
        "azure": AzureCrossValidator,
    }

    credentials = resolve_cloud_credentials(cloud, credentials_flag)
    ci_results: dict[str, CloudIntelligenceResult] = {}
    tasks: dict[Future[CloudIntelligenceResult], str] = {}

    with ThreadPoolExecutor(max_workers=3) as executor:
        if iam_analysis and cloud in IAM_ANALYZER_MAP:
            analyzer = IAM_ANALYZER_MAP[cloud](cloud, project_id, credentials)
            tasks[executor.submit(analyzer.analyze)] = "iam_analysis"

        if cross_validate and cloud in CROSS_VALIDATOR_MAP:
            validator = CROSS_VALIDATOR_MAP[cloud](
                cloud, project_id, credentials, complyform_findings=findings
            )
            tasks[executor.submit(validator.analyze)] = "cross_validation"

        if estimate_cost:
            from complyform.core.cloud_intelligence.cost_estimator import (
                estimate_cost_impact,
            )

            tasks[executor.submit(estimate_cost_impact, cloud)] = "cost_impact"

        for future in as_completed(tasks, timeout=120):
            feature = tasks[future]
            try:
                result = future.result()
            except Exception:
                if isinstance(console, Console):
                    Console(stderr=True).print(
                        f"[yellow]\u26a0 {feature} timed out."
                        " Continuing without.[/yellow]",
                    )
                continue

            ci_results[feature] = result
            if not result.success and isinstance(console, Console):
                Console(stderr=True).print(
                    f"[yellow]\u26a0 {feature} unavailable: {result.error}."
                    " Continuing without.[/yellow]",
                )

    return ci_results


def _cache_cloud_intelligence(
    cache: ScanCache,
    ci_results: dict[str, CloudIntelligenceResult],
    landing_zone_context: LandingZoneContext | None,
) -> None:
    """Persist CI results to the latest cache entry metadata."""
    import dataclasses

    if not ci_results and not landing_zone_context:
        return

    if not cache._cache_dir.exists():
        return

    best_dir = None
    best_time = ""
    for entry_dir in cache._cache_dir.iterdir():
        if not entry_dir.is_dir():
            continue
        meta_path = entry_dir / "metadata.json"
        if not meta_path.exists():
            continue
        try:
            meta = json.loads(meta_path.read_text(encoding="utf-8"))
            scanned_at = meta.get("scanned_at", "")
            if scanned_at > best_time:
                best_time = scanned_at
                best_dir = entry_dir
        except ValueError, KeyError:
            continue

    if best_dir is None:
        return

    meta_path = best_dir / "metadata.json"
    meta = json.loads(meta_path.read_text(encoding="utf-8"))

    for feature_key, result in ci_results.items():
        meta[feature_key] = dataclasses.asdict(result)

    if landing_zone_context is not None:
        meta["landing_zone"] = dataclasses.asdict(landing_zone_context)

    meta_path.write_text(json.dumps(meta, indent=2), encoding="utf-8")


def _cache_findings(findings: list[Finding], cache: ScanCache) -> None:
    """Persist findings to the latest cache entry for explain command."""
    if not cache._cache_dir.exists():
        return

    best_dir = None
    best_time = ""
    for entry_dir in cache._cache_dir.iterdir():
        if not entry_dir.is_dir():
            continue
        meta_path = entry_dir / "metadata.json"
        if not meta_path.exists():
            continue
        try:
            meta = json.loads(meta_path.read_text(encoding="utf-8"))
            scanned_at = meta.get("scanned_at", "")
            if scanned_at > best_time:
                best_time = scanned_at
                best_dir = entry_dir
        except ValueError, KeyError:
            continue

    if best_dir is not None:
        findings_data = [f.model_dump(mode="json") for f in findings]
        (best_dir / "findings.json").write_text(
            json.dumps(findings_data, indent=2), encoding="utf-8"
        )


def _build_summary(findings: list[Finding], score: float) -> dict[str, object]:
    passed = sum(1 for f in findings if f.status == "PASS")
    failed = sum(1 for f in findings if f.status == "FAIL")
    manual = sum(1 for f in findings if f.status == "MANUAL_REVIEW")
    not_assessed = sum(1 for f in findings if f.status == "NOT_ASSESSED")

    severity_counts: dict[str, int] = {}
    for f in findings:
        severity_counts[f.severity] = severity_counts.get(f.severity, 0) + 1

    return {
        "total_controls": len(findings),
        "passed": passed,
        "failed": failed,
        "manual_review": manual,
        "not_assessed": not_assessed,
        "score": score,
        "severity_counts": severity_counts,
    }


def _has_relevant_failures(findings: list[Finding], severity_filter: str) -> bool:
    for f in findings:
        if f.status != "FAIL":
            continue
        if severity_filter == "all":
            return True
        if severity_filter == "critical" and f.severity == "CRITICAL":
            return True
        if severity_filter == "high" and f.severity in ("CRITICAL", "HIGH"):
            return True
    return False


def _build_metadata(
    resource_count: int,
    elapsed_ms: int,
    tier: str,
    provider_versions: dict[str, str],
) -> AssessmentMetadata:
    from complyform.reporting.json_report import AssessmentMetadata

    return AssessmentMetadata(
        resource_count=resource_count,
        assessment_duration_ms=elapsed_ms,
        tier=tier,
        cache_hit=True,
        provider_versions=provider_versions,
    )


def _output_json(  # noqa: PLR0913
    findings: list[Finding],
    summary: dict[str, object],
    *,
    cloud: str,
    state_hash: str,
    frameworks: list[str],
    elapsed_ms: int,
    tier: str,
    resource_count: int,
    provider_versions: dict[str, str],
    upgrade_hints: list[dict[str, Any]] | None,
    explanations: list[Any] | None = None,
    ci_results: dict[str, CloudIntelligenceResult] | None = None,
    landing_zone: LandingZoneContext | None = None,
) -> None:
    import dataclasses

    from complyform.reporting.json_report import (
        AssessmentSummary,
        format_json_envelope,
    )

    summary_model = AssessmentSummary.model_validate(summary)
    metadata = _build_metadata(resource_count, elapsed_ms, tier, provider_versions)

    envelope = format_json_envelope(
        findings,
        summary_model,
        metadata,
        cloud=cloud,
        state_hash=state_hash,
        frameworks=frameworks,
        upgrade_hints=upgrade_hints,
    )
    if explanations is not None:
        envelope["explanations"] = [e.model_dump(mode="json") for e in explanations]

    # Cloud intelligence section
    ci_payload: dict[str, Any] = {}
    if ci_results:
        for feature_key, result in ci_results.items():
            ci_payload[feature_key] = dataclasses.asdict(result)
    if landing_zone is not None and landing_zone.detected:
        ci_payload["landing_zone"] = dataclasses.asdict(landing_zone)
    if ci_payload:
        envelope["cloud_intelligence"] = ci_payload

    sys.stdout.write(json.dumps(envelope, indent=2) + "\n")


def _output_sarif(
    findings: list[Finding],
    *,
    cloud: str,
    frameworks: list[str],
) -> None:
    from complyform.reporting.sarif_report import format_sarif_report

    sarif = format_sarif_report(findings, cloud=cloud, frameworks=frameworks)
    sys.stdout.write(json.dumps(sarif, indent=2) + "\n")


def _output_terminal(
    app_ctx: AppContext,
    findings: list[Finding],
    summary: dict[str, object],
    *,
    frameworks: list[str],
    cloud: str,
    resource_count: int,
    overlap_hints: list[dict[str, Any]] | None,
    explanations: list[Any] | None = None,
) -> None:
    from rich.console import Console
    from rich.panel import Panel

    from complyform.reporting.json_report import AssessmentSummary
    from complyform.reporting.terminal_report import format_terminal_report

    console = app_ctx.console
    if app_ctx.quiet or not isinstance(console, Console):
        return

    summary_model = AssessmentSummary.model_validate(summary)
    format_terminal_report(
        console,
        findings,
        summary_model,
        frameworks=frameworks,
        cloud=cloud,
        resource_count=resource_count,
        overlap_hints=overlap_hints,
    )

    if explanations:
        console.print()
        for expl in explanations:
            body = (
                f"[bold]Summary:[/bold] {expl.summary}\n"
                "[bold]Why It Matters:[/bold]"
                f" {expl.why_it_matters}\n"
                "[bold]Remediation Hint:[/bold]"
                f" {expl.remediation_hint}"
            )
            console.print(
                Panel(
                    body,
                    title=(f"Explain: {expl.control_id} ({expl.framework})"),
                    style="red",
                )
            )


def _write_attestation(
    findings: list[Finding],
    summary: dict[str, object],
    *,
    cloud: str,
    state_hash: str,
    frameworks: list[str],
    elapsed_ms: int,
    tier: str,
    resource_count: int,
    provider_versions: dict[str, str],
    attest_output: str,
) -> None:
    from pathlib import Path

    from complyform.core.attestation.builder import build_attestation
    from complyform.reporting.json_report import (
        AssessmentSummary,
        format_json_envelope,
    )

    summary_model = AssessmentSummary.model_validate(summary)
    metadata = _build_metadata(resource_count, elapsed_ms, tier, provider_versions)

    envelope = format_json_envelope(
        findings,
        summary_model,
        metadata,
        cloud=cloud,
        state_hash=state_hash,
        frameworks=frameworks,
    )

    attestation = build_attestation(envelope, attest_output)
    Path(attest_output).write_text(json.dumps(attestation, indent=2), encoding="utf-8")
