from __future__ import annotations

import importlib.metadata
import json
import shutil
import subprocess
import tempfile
from pathlib import Path
from typing import Annotated

import typer
from rich.console import Console
from rich.json import JSON

from terraform_guardrail.api.app import create_app as create_api_app
from terraform_guardrail.generator import generate_snippet
from terraform_guardrail.mcp.server import run_stdio
from terraform_guardrail.policy_registry import (
    PolicyRegistryError,
    download_bundle,
    get_policy_bundle,
    list_policy_bundles,
)
from terraform_guardrail.registry_api import create_registry_app
from terraform_guardrail.scanner.rules import RULES
from terraform_guardrail.scanner.scan import scan_path
from terraform_guardrail.web.app import create_app

app = typer.Typer(add_completion=False)
policy_app = typer.Typer(help="Policy registry commands.")
rules_app = typer.Typer(help="Rule catalog commands.")
app.add_typer(policy_app, name="policy")
app.add_typer(rules_app, name="rules")
console = Console()


def _version_callback(value: bool) -> None:
    if value:
        typer.echo(importlib.metadata.version("terraform-guardrail"))
        raise typer.Exit()


@app.callback()
def _root(
    version: Annotated[
        bool,
        typer.Option(
            "--version",
            callback=_version_callback,
            is_eager=True,
            help="Show version and exit.",
        ),
    ] = False,
) -> None:
    """Terraform Guardrail MCP CLI."""


@app.command()
def scan(
    path: Annotated[Path, typer.Argument(help="Path to a Terraform file or directory.")],
    state: Annotated[Path | None, typer.Option(help="Optional path to a .tfstate file.")] = None,
    format: Annotated[str, typer.Option(help="pretty or json")] = "pretty",
    schema: Annotated[bool, typer.Option(help="Enable schema-aware validation")] = False,
    policy_bundle: Annotated[str | None, typer.Option(help="Policy bundle ID to evaluate")] = None,
    policy_bundle_path: Annotated[
        Path | None,
        typer.Option(help="Local policy bundle path (.tar.gz or directory)"),
    ] = None,
    policy_layers: Annotated[
        list[str] | None,
        typer.Option(help="Ordered policy bundles for layering (repeatable)"),
    ] = None,
    policy_base: Annotated[str | None, typer.Option(help="Base policy bundle ID")] = None,
    policy_env: Annotated[str | None, typer.Option(help="Environment policy bundle ID")] = None,
    policy_app: Annotated[str | None, typer.Option(help="Application policy bundle ID")] = None,
    policy_registry: Annotated[str | None, typer.Option(help="Policy registry URL")] = None,
    policy_query: Annotated[str | None, typer.Option(help="OPA query override")] = None,
    fail_on: Annotated[
        str | None,
        typer.Option(help="Fail if findings at/above severity: low, medium, high"),
    ] = None,
) -> None:
    try:
        report = scan_path(
            path=path,
            state_path=state,
            use_schema=schema,
            policy_bundle=policy_bundle,
            policy_bundle_path=policy_bundle_path,
            policy_layers=policy_layers,
            policy_base=policy_base,
            policy_env=policy_env,
            policy_app=policy_app,
            policy_registry=policy_registry,
            policy_query=policy_query,
        )
    except Exception as exc:  # noqa: BLE001
        console.print(f"Scan failed: {exc}")
        raise typer.Exit(code=1) from exc
    if format == "json":
        console.print(JSON(json.dumps(report.model_dump(), indent=2)))
    else:
        console.print(f"Scanned: {report.scanned_path}")
        console.print(f"Findings: {report.summary.findings}")
        console.print(
            "High: "
            f"{report.summary.high} Medium: {report.summary.medium} Low: {report.summary.low}"
        )
        for finding in report.findings:
            console.print(
                f"- [{finding.severity}] {finding.rule_id} {finding.message} ({finding.path})"
            )

    if fail_on:
        _maybe_fail(report.summary, fail_on.lower())


@app.command()
def generate(
    provider: Annotated[str, typer.Argument(help="Provider: aws or azure")],
    resource: Annotated[str, typer.Argument(help="Resource type, e.g. aws_s3_bucket")],
    name: Annotated[str, typer.Option(help="Resource name")] = "example",
) -> None:
    try:
        snippet = generate_snippet(provider, resource, name)
    except Exception as exc:  # noqa: BLE001
        console.print(f"Generation failed: {exc}")
        raise typer.Exit(code=1) from exc
    console.print(snippet.content.strip())


@app.command()
def mcp() -> None:
    console.print("Starting MCP server on stdio...")
    run_stdio()


@app.command()
def web(
    host: Annotated[str, typer.Option(help="Bind host")] = "127.0.0.1",
    port: Annotated[int, typer.Option(help="Bind port")] = 8000,
) -> None:
    import uvicorn

    uvicorn.run(create_app(), host=host, port=port)


@app.command()
def api(
    host: Annotated[str, typer.Option(help="Bind host")] = "127.0.0.1",
    port: Annotated[int, typer.Option(help="Bind port")] = 8080,
) -> None:
    import uvicorn

    uvicorn.run(create_api_app(), host=host, port=port)


@app.command("registry-api")
def registry_api(
    host: Annotated[str, typer.Option(help="Bind host")] = "127.0.0.1",
    port: Annotated[int, typer.Option(help="Bind port")] = 8090,
) -> None:
    import uvicorn

    uvicorn.run(create_registry_app(), host=host, port=port)


@policy_app.command("list")
def list_policies(
    registry: Annotated[str | None, typer.Option(help="Policy registry URL")] = None,
) -> None:
    try:
        bundles = list_policy_bundles(registry)
    except PolicyRegistryError as exc:
        console.print(f"Policy registry error: {exc}")
        raise typer.Exit(code=1) from exc
    for bundle in bundles:
        console.print(f"- {bundle.bundle_id} ({bundle.version or 'unknown'}) {bundle.title}")


@policy_app.command("fetch")
def fetch_policy(
    bundle_id: Annotated[str, typer.Argument(help="Bundle ID to download")],
    destination: Annotated[
        Path, typer.Option(help="Destination directory for the bundle")
    ] = Path("./policies"),
    registry: Annotated[str | None, typer.Option(help="Policy registry URL")] = None,
) -> None:
    try:
        bundle = get_policy_bundle(bundle_id, registry)
        bundle_path = download_bundle(bundle, destination)
    except PolicyRegistryError as exc:
        console.print(f"Policy registry error: {exc}")
        raise typer.Exit(code=1) from exc
    console.print(f"Bundle downloaded to {bundle_path}")


@policy_app.command("init")
def init_policy_bundle(
    destination: Annotated[
        Path, typer.Option(help="Destination directory for the new bundle")
    ] = Path("./policy-bundle"),
    bundle_name: Annotated[str, typer.Option(help="Policy package name")] = "guardrail",
) -> None:
    bundle_dir = destination
    policies_dir = bundle_dir / "policies"
    policies_dir.mkdir(parents=True, exist_ok=True)
    (bundle_dir / ".manifest").write_text(
        '{\n  "revision": "0.1.0",\n  "roots": ["guardrail"]\n}\n',
        encoding="utf-8",
    )
    (bundle_dir / "data.json").write_text(
        "\n".join(
            [
                "{",
                '  "guardrail": {',
                '    "allowed_regions": [],',
                '    "allowed_instance_types": []',
                "  }",
                "}",
                "",
            ]
        ),
        encoding="utf-8",
    )
    (policies_dir / "guardrail.rego").write_text(
        "\n".join(
            [
                f"package {bundle_name}",
                "",
                "import rego.v1",
                "",
                "default allow = true",
                "",
                "deny contains output if {",
                "  false",
                "",
                "  output := {",
                "    \"message\": \"Example policy violation\",",
                "    \"severity\": \"medium\",",
                "    \"rule_id\": \"CUSTOM001\",",
                "    \"path\": \"policy\",",
                "  }",
                "}",
                "",
            ]
        ),
        encoding="utf-8",
    )
    console.print(f"Policy bundle scaffold created at {bundle_dir}")


@policy_app.command("validate")
def validate_policy_bundle(
    bundle_path: Annotated[Path, typer.Argument(help="Bundle path (.tar.gz or directory)")],
) -> None:
    if not bundle_path.exists():
        console.print(f"Bundle not found: {bundle_path}")
        raise typer.Exit(code=1)
    opa_path = shutil.which("opa")
    if not opa_path:
        console.print("OPA CLI not found. Install OPA to validate bundles.")
        raise typer.Exit(code=1)
    with tempfile.TemporaryDirectory() as tmp_dir:
        tmp_path = Path(tmp_dir) / "validated.tar.gz"
        cmd = [
            opa_path,
            "build",
            "--bundle",
            str(bundle_path),
            "--output",
            str(tmp_path),
        ]
        result = subprocess.run(cmd, capture_output=True, text=True, check=False)
        if result.returncode != 0:
            console.print(f"Bundle validation failed: {result.stderr.strip()}")
            raise typer.Exit(code=1)
    console.print("Bundle validation succeeded.")


@rules_app.command("list")
def list_rules() -> None:
    for rule_id, message in sorted(RULES.items()):
        console.print(f"- {rule_id}: {message}")


def main() -> None:
    app()


def _maybe_fail(summary, level: str) -> None:
    if level not in {"low", "medium", "high"}:
        console.print("Invalid fail-on severity. Use low, medium, or high.")
        raise typer.Exit(code=2)
    if level == "high" and summary.high > 0:
        raise typer.Exit(code=1)
    if level == "medium" and (summary.high + summary.medium) > 0:
        raise typer.Exit(code=1)
    if level == "low" and summary.findings > 0:
        raise typer.Exit(code=1)


if __name__ == "__main__":
    main()
