"""Sanicode CLI — entry point for all subcommands."""

from __future__ import annotations

import json
from collections import Counter
from pathlib import Path

import typer
from rich.console import Console
from rich.panel import Panel
from rich.table import Table

from sanicode import __version__

app = typer.Typer(
    name="sanicode",
    help="AI-assisted code sanitization scanner with compliance mapping.",
    no_args_is_help=True,
)
console = Console()


def _version_callback(value: bool) -> None:
    if value:
        console.print(f"sanicode {__version__}")
        raise typer.Exit()


@app.callback()
def main_callback(
    version: bool | None = typer.Option(
        None,
        "--version",
        "-V",
        callback=_version_callback,
        is_eager=True,
        help="Show version and exit.",
    ),
) -> None:
    """Sanicode — AI-assisted code sanitization scanner."""


@app.command()
def config(
    show: bool = typer.Option(False, "--show", help="Print the resolved configuration."),
    init: bool = typer.Option(
        False, "--init", help="Create a starter sanicode.toml in the current directory."
    ),
    config_file: Path | None = typer.Option(
        None, "--config", "-c", help="Path to a sanicode.toml config file."
    ),
) -> None:
    """Configure the LLM backend and scanner settings."""
    from sanicode.config import DEFAULT_CONFIG_TEMPLATE, load_config

    if init:
        dest = Path("sanicode.toml")
        if dest.exists():
            console.print(
                "[red]Error:[/red] sanicode.toml already exists in the current directory."
            )
            raise typer.Exit(code=1)
        dest.write_text(DEFAULT_CONFIG_TEMPLATE, encoding="utf-8")
        console.print(f"[green]Created[/green] {dest.resolve()}")
        console.print(
            "Edit the file to configure your LLM endpoints,"
            " then run [bold]sanicode scan[/bold]."
        )
        return

    try:
        cfg = load_config(config_file)
    except FileNotFoundError as exc:
        console.print(f"[red]Error:[/red] {exc}")
        raise typer.Exit(code=1) from exc

    source_label = (
        str(cfg._source_path) if cfg._source_path else "defaults (no config file found)"
    )
    console.print(Panel(
        f"[bold]Sanicode Configuration[/bold]\nSource: {source_label}",
        expand=False,
    ))

    # LLM tiers
    llm_table = Table(title="LLM Tiers", show_lines=False)
    llm_table.add_column("Tier", style="bold")
    llm_table.add_column("Endpoint")
    llm_table.add_column("Model")
    for tier_name in ("fast", "analysis", "reasoning"):
        tier = getattr(cfg.llm, tier_name)
        if tier.endpoint:
            llm_table.add_row(tier_name, tier.endpoint, tier.model or "(none)")
        else:
            llm_table.add_row(tier_name, "[dim]not configured[/dim]", "[dim]—[/dim]")
    console.print(llm_table)

    # Scan
    scan_table = Table(title="Scan", show_lines=False)
    scan_table.add_column("Setting", style="bold")
    scan_table.add_column("Value")
    scan_table.add_row("Extensions", ", ".join(cfg.scan.include_extensions))
    scan_table.add_row("Max file size", f"{cfg.scan.max_file_size_kb} KB")
    excl = ", ".join(cfg.scan.exclude_patterns) if cfg.scan.exclude_patterns else "(none)"
    scan_table.add_row("Exclusions", excl)
    console.print(scan_table)

    # Output
    out_table = Table(title="Output", show_lines=False)
    out_table.add_column("Setting", style="bold")
    out_table.add_column("Value")
    out_table.add_row("Formats", ", ".join(cfg.output.formats))
    out_table.add_row("Output dir", cfg.output.output_dir)
    console.print(out_table)

    # Compliance
    comp_table = Table(title="Compliance", show_lines=False)
    comp_table.add_column("Setting", style="bold")
    comp_table.add_column("Value")
    comp_table.add_row("Profiles", ", ".join(cfg.compliance.profiles))
    console.print(comp_table)


@app.command()
def scan(
    path: Path = typer.Argument(
        Path("."), help="Path to the codebase to scan."
    ),
    config_file: Path | None = typer.Option(
        None, "--config", "-c", help="Path to a sanicode.toml config file."
    ),
    output_format: list[str] = typer.Option(
        ["markdown"], "--format", "-f", help="Output format(s): markdown, json, sarif."
    ),
) -> None:
    """Scan a codebase, build a knowledge graph, and identify findings."""
    from sanicode.config import load_config
    from sanicode.scanner.executor import run_scan

    target = path.resolve()
    try:
        cfg = load_config(config_file)
    except FileNotFoundError as exc:
        console.print(f"[red]Error:[/red] {exc}")
        raise typer.Exit(code=1) from exc

    console.print(f"Scanning [bold]{target}[/bold] ...")

    try:
        output = run_scan(target, cfg)
    except FileNotFoundError as exc:
        console.print(f"[red]Error:[/red] {exc}")
        raise typer.Exit(code=1) from exc

    for err in output.parse_errors:
        console.print(f"[yellow]Warning:[/yellow] Skipping {err}")

    # Write report files.
    from sanicode.report.json_report import generate_json
    from sanicode.report.markdown import generate_markdown
    from sanicode.report.persist import save_scan_result
    from sanicode.report.sarif import generate_sarif

    output_dir = Path(cfg.output.output_dir)
    output_dir.mkdir(parents=True, exist_ok=True)
    save_scan_result(output.result, output_dir)

    report_files: list[str] = []
    for fmt in output_format:
        if fmt == "sarif":
            sarif_path = output_dir / "report.sarif"
            content = json.dumps(generate_sarif(output.result), indent=2)
            sarif_path.write_text(content, encoding="utf-8")
            report_files.append("report.sarif")
        elif fmt == "json":
            json_path = output_dir / "report.json"
            content = json.dumps(generate_json(output.result), indent=2)
            json_path.write_text(content, encoding="utf-8")
            report_files.append("report.json")
        elif fmt == "markdown":
            md_path = output_dir / "report.md"
            md_path.write_text(generate_markdown(output.result), encoding="utf-8")
            report_files.append("report.md")

    written = ["scan-result.json"] + report_files
    console.print(f"\n[green]Reports written to[/green] [bold]{output_dir}/[/bold]")
    for fname in written:
        console.print(f"  {fname}")

    # Print summary.
    summary = output.result.summary
    console.print()
    console.print("[bold]Knowledge Graph[/bold]")
    console.print(f"  Nodes: {summary.get('graph_nodes', 0)}  "
                  f"(entry points: {summary.get('graph_entry_points', 0)}, "
                  f"sinks: {summary.get('graph_sinks', 0)}, "
                  f"sanitizers: {summary.get('graph_sanitizers', 0)})")
    console.print(f"  Edges: {summary.get('graph_edges', 0)}")

    enriched = output.enriched_findings
    console.print()
    console.print(f"[bold]Findings:[/bold] {len(enriched)}")

    if enriched:
        # Per-finding detail table.
        detail_table = Table(title="Findings", show_lines=False)
        detail_table.add_column("File", style="cyan", no_wrap=True)
        detail_table.add_column("Line", justify="right")
        detail_table.add_column("Severity")
        detail_table.add_column("Rule", style="bold")
        detail_table.add_column("CWE", style="bold")
        detail_table.add_column("Message")

        for ef in enriched:
            sev = ef.derived_severity or ef.severity
            if sev in ("critical", "high"):
                sev_style = f"[red]{sev}[/red]"
            elif sev == "medium":
                sev_style = f"[yellow]{sev}[/yellow]"
            else:
                sev_style = f"[dim]{sev}[/dim]"

            rel_file = str(ef.file)
            try:
                rel_file = str(ef.file.relative_to(target))
            except ValueError:
                pass

            cwe_label = f"CWE-{ef.cwe_id}"
            if ef.cwe_name:
                cwe_label += f" ({ef.cwe_name})"

            detail_table.add_row(
                rel_file, str(ef.line), sev_style, ef.rule_id, cwe_label, ef.message
            )

        console.print(detail_table)
        console.print()

        # Top CWEs table.
        cwe_counts: Counter[str] = Counter()
        for ef in enriched:
            label = f"CWE-{ef.cwe_id}"
            if ef.cwe_name:
                label += f" ({ef.cwe_name})"
            cwe_counts[label] += 1

        table = Table(title="Top CWEs", show_lines=False)
        table.add_column("CWE", style="bold")
        table.add_column("Count", justify="right")
        for cwe_label, count in cwe_counts.most_common(10):
            table.add_row(cwe_label, str(count))
        console.print(table)


@app.command()
def report(
    scan_result_path: Path = typer.Argument(
        ..., help="Path to a scan-result.json file."
    ),
    output_format: list[str] = typer.Option(
        ["markdown", "sarif"], "--format", "-f", help="Output format(s): markdown, json, sarif."
    ),
    output_dir: Path | None = typer.Option(
        None, "--output-dir", "-o",
        help="Output directory (default: same as scan-result.json)."
    ),
    severity_filter: str | None = typer.Option(
        None, "--severity", "-s", help="Minimum severity threshold: critical, high, medium, low."
    ),
    cwe_filter: list[int] | None = typer.Option(
        None, "--cwe", help="Filter to specific CWE IDs (repeatable)."
    ),
) -> None:
    """Generate reports from a saved scan-result.json file."""
    from sanicode.report.json_report import generate_json
    from sanicode.report.markdown import generate_markdown
    from sanicode.report.persist import ScanResult, load_scan_result
    from sanicode.report.sarif import generate_sarif

    try:
        result = load_scan_result(scan_result_path)
    except FileNotFoundError as exc:
        console.print(f"[red]Error:[/red] {exc}")
        raise typer.Exit(code=1) from exc
    except ValueError as exc:
        console.print(f"[red]Error:[/red] Invalid scan result file: {exc}")
        raise typer.Exit(code=1) from exc

    # Apply filters to findings
    filtered = result.findings

    if severity_filter:
        severity_levels = {"critical": 4, "high": 3, "medium": 2, "low": 1, "info": 0}
        threshold = severity_levels.get(severity_filter.lower())
        if threshold is None:
            console.print(f"[red]Error:[/red] Unknown severity: {severity_filter}")
            raise typer.Exit(code=1)
        filtered = [
            f for f in filtered
            if severity_levels.get(
                f.get("derived_severity") or f.get("severity", "info"), 0
            ) >= threshold
        ]

    if cwe_filter:
        cwe_set = set(cwe_filter)
        filtered = [f for f in filtered if f.get("cwe_id") in cwe_set]

    # Build a filtered ScanResult (update summary counts)
    by_severity: dict[str, int] = {}
    by_cwe: dict[str, int] = {}
    for f in filtered:
        sev = f.get("derived_severity") or f.get("severity", "info")
        by_severity[sev] = by_severity.get(sev, 0) + 1
        cwe_key = str(f.get("cwe_id", ""))
        by_cwe[cwe_key] = by_cwe.get(cwe_key, 0) + 1

    filtered_summary = dict(result.summary)
    filtered_summary["total_findings"] = len(filtered)
    filtered_summary["by_severity"] = by_severity
    filtered_summary["by_cwe"] = by_cwe

    filtered_result = ScanResult(
        sanicode_version=result.sanicode_version,
        scan_id=result.scan_id,
        scanned_path=result.scanned_path,
        scan_timestamp=result.scan_timestamp,
        summary=filtered_summary,
        findings=filtered,
    )

    # Determine output directory
    dest = output_dir if output_dir else scan_result_path.parent
    dest.mkdir(parents=True, exist_ok=True)

    # Generate reports
    report_files: list[str] = []
    for fmt in output_format:
        if fmt == "sarif":
            out = dest / "report.sarif"
            out.write_text(json.dumps(generate_sarif(filtered_result), indent=2), encoding="utf-8")
            report_files.append("report.sarif")
        elif fmt == "json":
            out = dest / "report.json"
            out.write_text(json.dumps(generate_json(filtered_result), indent=2), encoding="utf-8")
            report_files.append("report.json")
        elif fmt == "markdown":
            out = dest / "report.md"
            out.write_text(generate_markdown(filtered_result), encoding="utf-8")
            report_files.append("report.md")

    console.print(f"[green]Reports written to[/green] [bold]{dest}/[/bold]")
    for fname in report_files:
        console.print(f"  {fname}")


@app.command()
def recommend(
    scan_id: str | None = typer.Argument(None, help="Scan ID to generate recommendations for."),
    top_n: int = typer.Option(10, "--top", "-n", help="Number of top recommendations to show."),
) -> None:
    """Output prioritized remediation recommendations from a scan."""
    typer.echo("Not yet implemented: recommend")


@app.command()
def graph(
    path: Path = typer.Argument(
        Path("."), help="Path to the codebase to build a graph from."
    ),
    export: Path | None = typer.Option(
        None, "--export", "-e", help="Export graph to this file (JSON). Omit for stdout."
    ),
) -> None:
    """Build and inspect or export the knowledge graph."""
    from sanicode.graph.builder import KnowledgeGraph

    target = path.resolve()
    if not target.exists():
        console.print(f"[red]Error:[/red] Path does not exist: {target}")
        raise typer.Exit(code=1)

    kg = KnowledgeGraph.from_source(target)
    data = kg.to_dict()

    if export:
        export.write_text(json.dumps(data, indent=2, default=str), encoding="utf-8")
        console.print(f"Graph exported to [bold]{export}[/bold] "
                      f"({kg.node_count} nodes, {kg.edge_count} edges)")
    else:
        console.print_json(json.dumps(data, default=str))


@app.command()
def serve(
    host: str = typer.Option("0.0.0.0", "--host", help="Host to bind the API server to."),
    port: int = typer.Option(8080, "--port", "-p", help="Port to listen on."),
    config_file: Path | None = typer.Option(
        None, "--config", "-c", help="Path to a sanicode.toml config file."
    ),
) -> None:
    """Start the Sanicode FastAPI server for remote/hybrid scan mode."""
    import uvicorn

    from sanicode.config import load_config
    from sanicode.server.app import app as fastapi_app
    from sanicode.server.app import configure

    try:
        cfg = load_config(config_file)
    except FileNotFoundError as exc:
        console.print(f"[red]Error:[/red] {exc}")
        raise typer.Exit(code=1) from exc

    configure(cfg)
    console.print(f"Starting Sanicode API server on {host}:{port}")
    console.print(f"  Docs: http://{host}:{port}/api/v1/docs")
    console.print(f"  Metrics: http://{host}:{port}/metrics")
    uvicorn.run(fastapi_app, host=host, port=port)


def main() -> None:
    """Entrypoint wrapper required by the pyproject.toml script declaration."""
    app()
