"""Sentinel CLI — entry point for all commands."""

from __future__ import annotations

import json
import shutil
import time
from pathlib import Path
from typing import TYPE_CHECKING, Annotated

if TYPE_CHECKING:
    from sentinel.reporters.base import BaseReporter

import typer
from rich import print as rprint
from rich.console import Console
from rich.panel import Panel
from rich.progress import Progress, SpinnerColumn, TextColumn, TimeElapsedColumn
from rich.table import Table

from sentinel.models.config import SentinelConfig
from sentinel.models.findings import AggregatedReport, ScanResult, Severity

app = typer.Typer(
    name="sentinel",
    help="[bold red]Sentinel[/] — DevSecOps audit toolkit by Cephalon Labs",
    rich_markup_mode="rich",
    no_args_is_help=True,
)
console = Console(stderr=True)

VERSION = "0.1.0"

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _load_config(config_path: Path | None, repo_path: Path) -> SentinelConfig:
    """Load config from explicit path, repo-local sentinel.yml, or defaults."""
    if config_path and config_path.exists():
        return SentinelConfig.from_file(config_path)
    auto = repo_path / "sentinel.yml"
    if auto.exists():
        return SentinelConfig.from_file(auto)
    return SentinelConfig.defaults()


def _resolve_modules(
    module_flags: list[str],
    config: SentinelConfig,
) -> list[str]:
    allowed = {"cicd", "scm", "secrets", "deps"}
    if module_flags:
        invalid = set(module_flags) - allowed
        if invalid:
            console.print(f"[red]Unknown module(s): {', '.join(invalid)}[/]")
            raise typer.Exit(1)
        return list(module_flags)
    return list(config.scan.modules)


def _build_aggregated_report(
    results: list[ScanResult],
    config: SentinelConfig,
    repo_path: Path,
) -> AggregatedReport:
    return AggregatedReport(
        client_name=config.client.name,
        engagement_date=config.client.engagement_date,
        target_path=str(repo_path.resolve()),
        results=results,
    )


def _print_summary(report: AggregatedReport) -> None:
    """Print a Rich summary table to stderr."""
    grade = report.risk_grade.value
    grade_colour = {
        "A": "bold green", "B": "green", "C": "yellow",
        "D": "red", "F": "bold red",
    }.get(grade, "white")

    console.print()
    console.print(Panel(
        f"[{grade_colour}]Risk Grade: {grade}[/]  |  "
        f"Critical: [bold red]{report.critical_count}[/]  "
        f"High: [red]{report.high_count}[/]  "
        f"Total findings: [cyan]{len(report.all_findings)}[/]",
        title="[bold]Sentinel Audit Summary[/]",
        border_style="dim",
    ))

    if not report.all_findings:
        console.print("[green]No findings. Clean bill of health.[/]")
        return

    t = Table(title="Findings by Module", show_lines=True)
    t.add_column("Module", style="bold")
    for sev in Severity:
        t.add_column(sev.value.capitalize(), style=sev.colour)

    for module, findings in sorted(report.findings_by_module.items()):
        counts = {s: 0 for s in Severity}
        for f in findings:
            counts[f.severity] += 1
        t.add_row(
            module,
            *[str(counts[s]) if counts[s] else "-" for s in Severity],
        )
    console.print(t)


def _save_artifacts(results: list[ScanResult], artifacts_dir: Path) -> None:
    artifacts_dir.mkdir(parents=True, exist_ok=True)
    for result in results:
        out = artifacts_dir / f"{result.module}.json"
        out.write_text(json.dumps(result.model_dump(), indent=2, default=str))


def _purge_artifacts(artifacts_dir: Path) -> None:
    if artifacts_dir.exists():
        shutil.rmtree(artifacts_dir)


# ---------------------------------------------------------------------------
# Commands
# ---------------------------------------------------------------------------

@app.command()
def audit(
    path: Annotated[Path, typer.Argument(help="Path to the repository to audit")] = Path("."),
    format: Annotated[
        list[str] | None,
        typer.Option("--format", "-f", help="Output format(s): json, pdf, html, markdown"),
    ] = None,
    module: Annotated[
        list[str] | None,
        typer.Option("--module", "-m", help="Run specific module(s): cicd, scm, secrets, deps"),
    ] = None,
    config: Annotated[
        Path | None,
        typer.Option("--config", "-c", help="Path to sentinel.yml"),
    ] = None,
    output: Annotated[
        Path | None,
        typer.Option("--output", "-o", help="Output directory for reports"),
    ] = None,
    deep_include: Annotated[
        bool,
        typer.Option("--deep-include", help="Recursively follow CI include: local: directives"),
    ] = False,
    keep_artifacts: Annotated[
        bool,
        typer.Option("--keep-artifacts", help="Retain raw JSON artifacts after reporting"),
    ] = False,
) -> None:
    """Run the Sentinel security audit against a local repository."""

    repo_path = path.resolve()
    if not repo_path.is_dir():
        console.print(f"[red]Path does not exist or is not a directory: {repo_path}[/]")
        raise typer.Exit(1)

    cfg = _load_config(config, repo_path)

    # CLI flags override config
    if deep_include:
        cfg.cicd.deep_include = True

    modules_to_run = _resolve_modules(module or [], cfg)
    formats_to_use: list[str] = list(format or cfg.reporting.formats)
    output_dir = output or Path(cfg.reporting.output_dir)
    artifacts_dir = repo_path / ".sentinel" / "collected"

    console.print(f"\n[bold red]Sentinel[/] v{VERSION} — [dim]{repo_path}[/]")
    console.print(f"Modules: [cyan]{', '.join(modules_to_run)}[/]")
    console.print(f"Formats: [cyan]{', '.join(formats_to_use)}[/]\n")

    results: list[ScanResult] = []
    total_start = time.monotonic()

    with Progress(
        SpinnerColumn(),
        TextColumn("[progress.description]{task.description}"),
        TimeElapsedColumn(),
        console=console,
        transient=True,
    ) as progress:
        for mod_name in modules_to_run:
            task = progress.add_task(f"Running [bold]{mod_name}[/] module...", total=None)
            t0 = time.monotonic()
            try:
                result = _run_module(mod_name, repo_path, cfg)
                result.scan_time_ms = int((time.monotonic() - t0) * 1000)
                results.append(result)
            except Exception as exc:  # noqa: BLE001
                console.print(f"[yellow]Warning:[/] {mod_name} module error: {exc}")
                results.append(ScanResult(
                    module=mod_name, target_path=str(repo_path), errors=[str(exc)]
                ))
            finally:
                progress.remove_task(task)

    report = _build_aggregated_report(results, cfg, repo_path)

    # Save raw artifacts
    _save_artifacts(results, artifacts_dir)

    # Render reports
    output_paths: list[Path] = []
    for fmt in formats_to_use:
        try:
            reporter = _get_reporter(fmt, cfg)
            out_path = reporter.render(report, output_dir)
            output_paths.append(out_path)
        except Exception as exc:  # noqa: BLE001
            console.print(f"[yellow]Warning:[/] {fmt} reporter failed: {exc}")

    elapsed = time.monotonic() - total_start
    _print_summary(report)

    console.print(f"\n[dim]Scan completed in {elapsed:.1f}s[/]")
    for p in output_paths:
        console.print(f"  [green]→[/] {p}")

    if not keep_artifacts:
        _purge_artifacts(artifacts_dir)

    # Exit with non-zero if critical findings exist
    if report.critical_count > 0:
        raise typer.Exit(2)


@app.command()
def init(
    output: Annotated[
        Path, typer.Argument(help="Where to write the example config")
    ] = Path("sentinel.yml"),
) -> None:
    """Generate an example sentinel.yml configuration file."""
    try:
        src = Path(__file__).parent.parent.parent / "sentinel.example.yml"
        if src.exists():
            shutil.copy(src, output)
        else:
            _write_inline_example(output)
        rprint(f"[green]Created[/] {output}")
    except Exception as exc:  # noqa: BLE001
        console.print(f"[red]Failed to write config: {exc}[/]")
        raise typer.Exit(1)


@app.command()
def version() -> None:
    """Print the Sentinel version."""
    rprint(f"sentinel-audit [bold]{VERSION}[/]")


# ---------------------------------------------------------------------------
# Module dispatch
# ---------------------------------------------------------------------------

def _run_module(name: str, repo_path: Path, cfg: SentinelConfig) -> ScanResult:
    if name == "cicd":
        from sentinel.analyzers.cicd_analyzer import CicdAnalyzer
        from sentinel.collectors.cicd.collector import CicdCollector
        data = CicdCollector(repo_path, cfg.cicd).collect()
        return CicdAnalyzer(repo_path).analyze(data)

    if name == "scm":
        from sentinel.analyzers.scm_analyzer import ScmAnalyzer
        from sentinel.collectors.scm.collector import ScmCollector
        data = ScmCollector(repo_path).collect()
        return ScmAnalyzer(repo_path).analyze(data)

    if name == "secrets":
        from sentinel.analyzers.secrets_analyzer import SecretsAnalyzer
        from sentinel.collectors.secrets.collector import SecretsCollector
        data = SecretsCollector(repo_path, cfg.secrets).collect()
        return SecretsAnalyzer(repo_path).analyze(data)

    if name == "deps":
        from sentinel.analyzers.deps_analyzer import DepsAnalyzer
        from sentinel.collectors.deps.collector import DepsCollector
        data = DepsCollector(repo_path, cfg.deps).collect()
        return DepsAnalyzer(repo_path, cfg.deps).analyze(data)

    raise ValueError(f"Unknown module: {name}")


def _get_reporter(fmt: str, cfg: SentinelConfig) -> BaseReporter:

    if fmt == "json":
        from sentinel.reporters.json_reporter import JsonReporter
        return JsonReporter()
    if fmt == "markdown":
        from sentinel.reporters.markdown_reporter import MarkdownReporter
        return MarkdownReporter()
    if fmt == "html":
        from sentinel.reporters.html_reporter import HtmlReporter
        return HtmlReporter(cfg.reporting.branding)
    if fmt == "pdf":
        from sentinel.reporters.pdf_reporter import PdfReporter
        return PdfReporter(cfg.reporting.branding)

    raise ValueError(f"Unknown format: {fmt}")


def _write_inline_example(path: Path) -> None:
    content = """\
version: "1.0"

client:
  name: "Acme Corp"
  engagement_date: "2026-02-24"

scan:
  path: "."
  modules: [cicd, scm, secrets, deps]

cicd:
  deep_include: false
  required_stages:
    - sast
    - secret_detection
    - dependency_scanning
    - dast
    - container_scanning

deps:
  ecosystems: [npm, pip, go, cargo, ruby]
  max_severity: critical
  ignore_cves: []

secrets:
  scan_history: false
  history_depth: 100
  allowlist:
    - "tests/fixtures/*"

reporting:
  formats: [json, pdf]
  output_dir: ./sentinel-reports
  branding:
    company: "Cephalon Labs"
    logo_path: ./assets/logo.png
"""
    path.write_text(content)


if __name__ == "__main__":
    app()
