"""complyform remediate command — generate compliance patches."""

from __future__ import annotations

import json
import sys
from pathlib import Path  # noqa: TC003 — Typer needs Path at runtime
from typing import TYPE_CHECKING, Annotated

import typer

from complyform.cli.exit_codes import (
    EXIT_COMPLIANCE_FAILURE,
    EXIT_PARTIAL_FAILURE,
    EXIT_SUCCESS,
)
from complyform.cli.main import AppContext, app
from complyform.core.cache import ScanCache
from complyform.core.errors import (
    InteractiveNotTTYError,
    InteractivePRConflictError,
    NoCachedAssessmentError,
    NoFindingsToRemediate,
    PRFlagConflictError,
    PRNoPatchesError,
    PRTokenMissingError,
    RemediationTierError,
    emit_provider_version_warning,
)

if TYPE_CHECKING:
    from rich.console import Console

    from complyform.core.assessor.control_matcher import Finding
    from complyform.core.profiles.models import ResourceMapping
    from complyform.core.remediation.github_pr import PRResult
    from complyform.core.remediator.patch_generator import PatchFile
    from complyform.core.scanner.state_parser import ResourceRecord

_BF = "Brownfield (Existing Infrastructure)"

_CI_FEATURE_MAP: dict[str, str] = {
    "iam_analysis": "iam_analysis",
    "estimate_cost": "cost_impact",
}


@app.command(name="remediate", rich_help_panel=_BF)
def remediate_cmd(  # noqa: PLR0913
    ctx: typer.Context,
    frameworks: Annotated[
        str,
        typer.Option(help="Comma-separated framework IDs to remediate"),
    ] = "",
    controls: Annotated[
        str,
        typer.Option(help="Comma-separated control IDs to remediate"),
    ] = "",
    output: Annotated[
        str,
        typer.Option(help="Output directory for .tf patch files"),
    ] = "./patches",
    dry_run: Annotated[
        bool,
        typer.Option(
            "--dry-run",
            help="Preview patches without writing files",
        ),
    ] = False,
    format: Annotated[
        str,
        typer.Option("--format", help="Output format: terminal, json"),
    ] = "terminal",
    estimate_cost: Annotated[
        bool,
        typer.Option(
            "--estimate-cost",
            help="Team+: Cost impact of remediation",
        ),
    ] = False,
    iam_analysis: Annotated[
        bool,
        typer.Option("--iam-analysis", help="Team+: Include IAM remediation patches"),
    ] = False,
    open_pr: Annotated[
        bool,
        typer.Option(
            "--open-pr",
            help="Open a pull request with patches",
        ),
    ] = False,
    pr_branch: Annotated[
        str | None,
        typer.Option("--pr-branch", help="PR branch name"),
    ] = None,
    pr_repo: Annotated[
        str | None,
        typer.Option("--pr-repo", help="PR target repo"),
    ] = None,
    pr_base: Annotated[
        str | None,
        typer.Option("--pr-base", help="PR base branch"),
    ] = None,
    pr_title: Annotated[
        str | None,
        typer.Option("--pr-title", help="PR title"),
    ] = None,
    pr_draft: Annotated[
        bool,
        typer.Option("--pr-draft", help="Create as draft PR"),
    ] = False,
    interactive: Annotated[
        bool,
        typer.Option(
            "--interactive",
            help="Interactive review mode",
        ),
    ] = False,
    batch: Annotated[
        Path | None,
        typer.Option("--batch", help="Agency tier: batch remediate from manifest"),
    ] = None,
) -> None:
    """Generate remediation patches for failed checks."""
    from complyform.core.auth.license import LicenseManager
    from complyform.core.auth.tier_enforcement import (
        check_feature,
        check_framework_access,
        enforce_with_refresh,
    )

    # ── Batch mode ──────────────────────────────────────────────────────
    if batch is not None:
        from pathlib import Path as _Path

        from complyform.core.batch.executor import run_batch_operation
        from complyform.core.batch.manifest import load_batch_manifest

        license_mgr = LicenseManager()
        tier_info = license_mgr.resolve_tier_info()
        enforce_with_refresh(
            license_mgr, tier_info, check_feature, "batch_remediate", tier_info
        )

        manifest = load_batch_manifest(batch)
        output_root = _Path(output)

        def _per_project(proj: object) -> dict[str, object]:
            from complyform.core.batch.manifest import BatchProject

            assert isinstance(proj, BatchProject)  # noqa: S101
            cache = ScanCache()
            cached = (
                cache.read_by_label(proj.label)
                if hasattr(cache, "read_by_label")
                else cache.latest()
            )
            if cached is None:
                return {
                    "status": "skipped",
                    "reason": "no cached assessment",
                }
            proj_dir = output_root / proj.label
            proj_dir.mkdir(parents=True, exist_ok=True)
            fw_str = ",".join(proj.frameworks) if proj.frameworks else frameworks
            patches, _findings, _cloud = _run_remediate(
                ctx=ctx,
                frameworks_str=fw_str,
                controls_str=controls,
                output_dir=str(proj_dir),
                dry_run=dry_run,
                format=format,
                interactive=False,
                iam_analysis=iam_analysis,
                estimate_cost=estimate_cost,
            )
            return {
                "status": "success",
                "patches": len(patches),
                "output_dir": str(proj_dir),
            }

        batch_result = run_batch_operation(
            manifest, "remediate", _per_project, output_root
        )
        exit_code = EXIT_SUCCESS if batch_result.failed == 0 else EXIT_PARTIAL_FAILURE
        raise typer.Exit(code=exit_code)

    # Mutual exclusivity checks first
    if interactive and open_pr:
        raise InteractivePRConflictError()
    if interactive and format == "json":
        raise InteractivePRConflictError(
            "Cannot combine --interactive with --format=json."
        )
    if interactive and not sys.stdin.isatty():
        raise InteractiveNotTTYError()

    app_ctx: AppContext = ctx.obj
    license_mgr = LicenseManager()
    tier_info = license_mgr.resolve_tier_info()

    # Tier gate: framework access for remediation
    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,
            "remediate",
            tier_info,
        )

    # Multi-state remediate tier gate
    cache = ScanCache()
    cached = cache.latest()
    if cached is not None:
        _resources, _meta = cached
        source_states = {r.source_state for r in _resources}
        if len(source_states) > 1:
            from complyform.core.auth.tier_enforcement import check_feature

            tier_info = enforce_with_refresh(
                license_mgr,
                tier_info,
                check_feature,
                "multi_state_remediate",
                tier_info,
            )

    # Tier-gate each requested CI feature
    for flag_name, feature_key in _CI_FEATURE_MAP.items():
        if locals()[flag_name]:
            from complyform.core.auth.tier_enforcement import check_feature

            tier_info = enforce_with_refresh(
                license_mgr,
                tier_info,
                check_feature,
                feature_key,
                tier_info,
            )

    # --pr-* flags require --open-pr
    for pr_flag, pr_val in (
        ("pr-branch", pr_branch),
        ("pr-repo", pr_repo),
        ("pr-base", pr_base),
        ("pr-title", pr_title),
        ("pr-draft", pr_draft),
    ):
        if pr_val and not open_pr:
            raise PRFlagConflictError(pr_flag)

    # --open-pr tier gate
    _github_token = ""
    if open_pr:
        from complyform.core.auth.tier_enforcement import (
            check_feature,
        )

        tier_info = enforce_with_refresh(
            license_mgr,
            tier_info,
            check_feature,
            "pr_remediation",
            tier_info,
        )

        # Check for GitHub token
        _github_token = app_ctx.config.github_token or ""
        if not _github_token:
            raise PRTokenMissingError()

    patches, findings_for_pr, cloud_for_pr = _run_remediate(
        ctx=ctx,
        frameworks_str=frameworks,
        controls_str=controls,
        output_dir=output,
        dry_run=dry_run,
        format=format,
        interactive=interactive,
        iam_analysis=iam_analysis,
        estimate_cost=estimate_cost,
    )

    if open_pr:
        _open_pr(
            ctx=ctx,
            patches=patches,
            findings=findings_for_pr,
            cloud=cloud_for_pr,
            frameworks_str=frameworks,
            github_token=_github_token,
            pr_branch=pr_branch,
            pr_repo=pr_repo,
            pr_base=pr_base,
            pr_title=pr_title,
            pr_draft=pr_draft,
            format=format,
        )
    else:
        raise typer.Exit(code=EXIT_SUCCESS if patches else EXIT_COMPLIANCE_FAILURE)


def _run_remediate(  # noqa: PLR0913
    *,
    ctx: typer.Context,
    frameworks_str: str,
    controls_str: str,
    output_dir: str,
    dry_run: bool,
    format: str,
    interactive: bool,
    iam_analysis: bool = False,
    estimate_cost: bool = False,
) -> tuple[list[PatchFile], list[Finding], str]:
    """Core remediate logic. Returns (patches, findings, cloud)."""
    from pathlib import Path

    from rich.console import Console

    from complyform.core.profiles.mapping_loader import (
        load_mappings,
    )
    from complyform.core.remediator.dependency_resolver import (
        DependencyResolver,
        build_dependency_report,
    )
    from complyform.core.remediator.patch_generator import (
        PatchGenerator,
    )

    app_ctx: AppContext = ctx.obj
    console: Console = app_ctx.console

    # 1. Load cached assessment
    cache = ScanCache()
    cached = cache.latest()
    if cached is None:
        raise NoCachedAssessmentError("")

    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", {})

    # Re-run assessment to get findings
    all_findings = _load_findings(resources, cloud)

    # 2. Filter to FAIL only
    fail_findings = [f for f in all_findings if f.status == "FAIL"]

    # 3. Apply --frameworks filter
    fw_ids = [f.strip() for f in frameworks_str.split(",") if f.strip()]
    if fw_ids:
        fail_findings = [f for f in fail_findings if f.framework in fw_ids]

    # 4. Apply --controls filter
    ctrl_ids = [c.strip() for c in controls_str.split(",") if c.strip()]
    if ctrl_ids:
        fail_findings = [f for f in fail_findings if f.control_id in ctrl_ids]

    # 5. No findings?
    assessed_fws = fw_ids or list({f.framework for f in all_findings})
    if not fail_findings:
        raise NoFindingsToRemediate(assessed_fws)

    # 6-7. Load mappings and build lookup
    frameworks_in_findings = {f.framework for f in fail_findings}
    mappings_lookup: dict[str, ResourceMapping] = {}
    gated_frameworks: list[tuple[str, str]] = []

    for fw_id in frameworks_in_findings:
        try:
            mapping_file = load_mappings(fw_id, cloud)
        except Exception:
            continue
        for rm in mapping_file.mappings:
            for rt in rm.resource_types:
                key = f"{rm.control_id}:{rt}"
                mappings_lookup[key] = rm

    # 8. Check required_config presence
    for f in fail_findings:
        key = f"{f.control_id}:{f.resource_type}"
        maybe_rm = mappings_lookup.get(key)
        if maybe_rm is not None and maybe_rm.required_config is None:
            gated_frameworks.append((f.framework, "pro"))

    if gated_frameworks:
        unique = {fw for fw, _ in gated_frameworks}
        fw_list = ", ".join(sorted(unique))
        raise RemediationTierError(fw_list, "pro")

    # 9. Provider version pre-check
    if provider_versions:
        from packaging.version import InvalidVersion, Version

        for fw_id in frameworks_in_findings:
            try:
                mf = load_mappings(fw_id, cloud)
            except Exception:
                continue
            if mf.min_provider_version is None:
                continue
            detected = provider_versions.get(mf.provider or cloud)
            if detected is None:
                continue
            try:
                if Version(detected) < Version(mf.min_provider_version):
                    emit_provider_version_warning(
                        console,
                        provider=mf.provider or cloud,
                        detected_version=detected,
                        min_version=mf.min_provider_version,
                        cloud=cloud,
                    )
            except InvalidVersion:
                pass

    # 10-11. Generate patches
    generator = PatchGenerator()
    patches = generator.generate_patches(
        fail_findings, mappings_lookup, resources, cloud
    )

    # Resolve dependencies
    resolver = DependencyResolver()
    patches = resolver.resolve(patches)

    # IAM patches
    iam_patch_count = 0
    if iam_analysis:
        from complyform.core.cloud_intelligence.iam_analyzer import (
            IAMAnalysisResult,
            generate_iam_patches,
        )

        # Try loading IAM analysis from cache, else run fresh
        iam_findings_data = cache_meta.get("iam_analysis")
        iam_findings_list = []
        if iam_findings_data and isinstance(iam_findings_data, dict):
            iam_result_success = iam_findings_data.get("success", False)
            if iam_result_success:
                from complyform.core.cloud_intelligence.iam_analyzer import IAMFinding

                for f_data in iam_findings_data.get("findings", []):
                    iam_findings_list.append(IAMFinding(**f_data))
        else:
            # Run IAM analyzer fresh
            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

            _IAM_MAP: dict[str, type] = {
                "gcp": GCPIAMAnalyzer,
                "aws": AWSIAMAnalyzer,
                "azure": AzureIAMAnalyzer,
            }
            if cloud in _IAM_MAP:
                try:
                    creds = resolve_cloud_credentials(
                        cloud, cache_meta.get("credentials")
                    )
                    analyzer_cls = _IAM_MAP[cloud]
                    analyzer = analyzer_cls(
                        cloud, cache_meta.get("project_id", ""), creds
                    )
                    iam_result = analyzer.analyze()
                    if isinstance(iam_result, IAMAnalysisResult) and iam_result.success:
                        iam_findings_list = iam_result.findings
                except Exception:
                    pass

        if iam_findings_list:
            iam_patches = generate_iam_patches(iam_findings_list, cloud)
            iam_patch_count = len(iam_patches)
            patches.extend(iam_patches)

    # 12. Interactive review
    if interactive:
        from complyform.cli.interactive_review import (
            run_interactive_review,
        )

        patches = run_interactive_review(patches, fail_findings, console)

    # 13. Write files
    output_path = Path(output_dir)
    if not dry_run and patches:
        output_path.mkdir(parents=True, exist_ok=True)
        for patch in patches:
            filepath = output_path / patch.filename
            filepath.write_text(patch.content, encoding="utf-8")

    # Cost estimation (runs after patch generation)
    cost_result = None
    if estimate_cost:
        from complyform.core.cloud_intelligence.cost_estimator import (
            estimate_cost_impact,
        )

        tf = output_dir if not dry_run else None
        cost_result = estimate_cost_impact(cloud, tf_path=tf)

    # Output
    if format == "json":
        _output_json(patches, fail_findings, cloud=cloud)
    elif not app_ctx.quiet:
        # 14. Dependency report
        dep_report = build_dependency_report(patches, fail_findings)
        if dep_report and isinstance(console, Console):
            _display_dependency_report(console, dep_report)

        # 15. Patch file tree
        if isinstance(console, Console) and patches:
            _display_patch_tree(console, patches, output_dir, dry_run)

            # 16. Summary
            resource_patch_count = len(patches) - iam_patch_count
            if iam_patch_count > 0:
                console.print(
                    f"\n[bold green]Generated {resource_patch_count} resource"
                    f" patch(es) + {iam_patch_count} IAM patch(es).[/bold green]"
                )
            else:
                console.print(
                    f"\n[bold green]{len(patches)} patch(es) generated.[/bold green]"
                )
            if dry_run:
                console.print("[dim]Dry run — no files written.[/dim]")

            # Cost impact section
            if cost_result is not None and cost_result.success:
                console.print(
                    f"\n[bold]Cost Impact[/bold] ({cost_result.confidence} confidence):"
                )
                console.print(
                    f"  Monthly delta: ${cost_result.total_monthly_delta:,.2f}"
                )
                console.print(
                    f"  Annual delta:  ${cost_result.total_annual_delta:,.2f}"
                )
                if cost_result.breakdown:
                    for item in cost_result.breakdown:
                        console.print(
                            f"    {item.category}: ${item.monthly_cost:,.2f}/mo"
                            f" ({item.description})"
                        )

            console.print(
                "\n[dim]Run: complyform scan"
                f" --state=<path> --assess"
                f" --frameworks="
                f"{','.join(frameworks_in_findings)}"
                f" to verify fixes[/dim]"
            )

    # TODO §A.6.1: emit_corpus_triple() call site

    # 17. Clear cache
    if not dry_run:
        hash_prefix = state_hash.replace("sha256:", "")[:16]
        if hash_prefix:
            cache.clear(hash_prefix)

    return patches, fail_findings, cloud


def _load_findings(
    resources: list[ResourceRecord],
    cloud: str,
) -> list[Finding]:
    """Re-run assessment to get findings."""
    from complyform.core.assessor.control_matcher import (
        ControlMatcher,
    )
    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 (
        list_frameworks,
    )
    from complyform.core.scanner.state_parser import (
        ResourceRecord,
    )

    typed_resources: list[ResourceRecord] = []
    for r in resources:
        if isinstance(r, ResourceRecord):
            typed_resources.append(r)

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

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

    community_fws = [f.id for f in list_frameworks(tier="community")]

    for fw_id in community_fws:
        try:
            mapping_file = load_mappings(fw_id, cloud)
        except Exception:
            continue
        findings = matcher.match(typed_resources, mapping_file, cross_index)
        all_findings.extend(findings)

    return all_findings


def _output_json(
    patches: list[PatchFile],
    findings: list[Finding],
    *,
    cloud: str,
    pr_result: PRResult | None = None,
) -> None:
    """Output JSON envelope to stdout."""
    from datetime import UTC, datetime

    from complyform import __version__

    pr_data = None
    if pr_result is not None:
        pr_data = pr_result.model_dump(mode="json")

    envelope = {
        "schema_version": 1,
        "tool": "complyform",
        "tool_version": __version__,
        "command": "remediate",
        "timestamp": datetime.now(UTC).isoformat(),
        "cloud": cloud,
        "findings": [f.model_dump(mode="json") for f in findings],
        "patches": [
            {
                "resource_address": p.resource_address,
                "control_id": p.control_id,
                "patch_file": f"patches/{p.filename}",
                "diff": p.diff,
            }
            for p in patches
        ],
        "pr": pr_data,
        "cloud_intelligence": None,
    }
    sys.stdout.write(json.dumps(envelope, indent=2) + "\n")


def _open_pr(
    *,
    ctx: typer.Context,
    patches: list[PatchFile],
    findings: list[Finding],
    cloud: str,
    frameworks_str: str,
    github_token: str,
    pr_branch: str | None,
    pr_repo: str | None,
    pr_base: str | None,
    pr_title: str | None,
    pr_draft: bool,
    format: str,
) -> None:
    """Create a GitHub PR with the generated patches."""
    from complyform import __version__
    from complyform.core.remediation.github_pr import (
        GitHubPRClient,
        build_commit_message,
        build_pr_body,
        default_branch_name,
        detect_github_repo,
    )
    from complyform.core.remediation.github_pr import (
        PatchFile as PRPatchFile,
    )

    app_ctx: AppContext = ctx.obj

    if not patches:
        raise PRNoPatchesError()

    # Detect or parse repo
    if pr_repo:
        parts = pr_repo.split("/", 1)
        owner, repo = parts[0], parts[1]
    else:
        owner, repo = detect_github_repo()

    client = GitHubPRClient(github_token, owner, repo)
    client.validate_token()

    # Resolve base branch
    base = pr_base or client.get_default_branch()
    base_sha = client.get_branch_sha(base)

    # Resolve branch name
    fw = frameworks_str.split(",")[0].strip() if frameworks_str else "mixed"
    branch = pr_branch or default_branch_name(fw)

    # Create branch
    client.create_branch(branch, base_sha)

    # Build tree + commit
    pr_patches = [
        PRPatchFile(
            path=f"patches/{p.filename}",
            content=p.content,
        )
        for p in patches
    ]
    control_count = len({f.control_id for f in findings})
    commit_msg = build_commit_message(fw, control_count)
    tree_sha = client.create_tree(base_sha, pr_patches)
    commit_sha = client.create_commit(tree_sha, base_sha, commit_msg)
    client.update_branch_ref(branch, commit_sha)

    # Build PR body
    patch_info = [
        (f"patches/{p.filename}", p.control_id, p.resource_address) for p in patches
    ]
    title = pr_title or (
        f"fix(compliance): {fw} remediation — {control_count} controls"
    )
    body = build_pr_body(
        framework=fw,
        control_count=control_count,
        cloud=cloud,
        patches=patch_info,
        version=__version__,
    )

    # Create PR
    result = client.create_pull_request(
        title=title,
        body=body,
        head=branch,
        base=base,
        draft=pr_draft,
    )
    result = result.model_copy(
        update={"files_committed": len(pr_patches), "commit_sha": commit_sha}
    )

    if format == "json":
        _output_json(patches, findings, cloud=cloud, pr_result=result)
    else:
        console = app_ctx.console
        console.print(f"\n[bold green]PR created:[/bold green] {result.url}")
        console.print(f"  Branch: {result.branch} → {result.base}")
        console.print(f"  Files: {result.files_committed}  Draft: {result.draft}")

    raise typer.Exit(code=EXIT_SUCCESS)


def _display_dependency_report(
    console: Console,
    report: object,
) -> None:
    """Display dependency report."""
    from rich.panel import Panel

    from complyform.core.remediator.dependency_resolver import (
        DependencyReport,
    )

    if not isinstance(report, DependencyReport):
        return

    lines: list[str] = []
    for dep in report.new_resources:
        risk_style = "green"
        if dep.risk_level == "medium":
            risk_style = "yellow"
        elif dep.risk_level == "high":
            risk_style = "bold red"
        lines.append(
            f"  [{risk_style}]{dep.risk_level.upper()}"
            f"[/{risk_style}]"
            f" {dep.resource_type}.{dep.resource_name}"
            f" (required by: "
            f"{', '.join(dep.required_by_controls)})"
        )
    console.print(
        Panel(
            "\n".join(lines),
            title="New Dependency Resources",
            style="cyan",
        )
    )


def _display_patch_tree(
    console: Console,
    patches: list[PatchFile],
    output_dir: str,
    dry_run: bool,
) -> None:
    """Display patch file tree."""
    from rich.tree import Tree

    label = f"{'[dim](dry run)[/dim] ' if dry_run else ''}{output_dir}/"
    tree = Tree(label)
    for p in patches:
        tree.add(f"[green]{p.filename}[/green]")
    console.print(tree)
