from __future__ import annotations

import json
from pathlib import Path
from typing import List, Optional, Set, Tuple

import typer
import yaml

from vibegate.checks import tool_exists, tool_version
from vibegate.config import ConfigError, VibeGateConfig, default_contract, load_config
from vibegate.runner import run_check, run_fixpack

app = typer.Typer(help="VibeGate CLI")


@app.command()
def check(
    repo_root: Path = typer.Argument(
        Path("."),
        exists=True,
        file_okay=False,
        dir_okay=True,
    ),
) -> None:
    """Run VibeGate checks and emit placeholder artifacts."""
    repo_root = repo_root.resolve()
    try:
        config = load_config(repo_root)
    except ConfigError as exc:
        typer.secho(str(exc), fg=typer.colors.RED)
        raise typer.Exit(code=2) from exc

    artifacts, status = run_check(config, repo_root)
    _print_outputs(config, artifacts)
    _print_summary(status, config)
    if status == "FAIL":
        raise typer.Exit(code=1)


@app.command()
def verify(
    repo_root: Path = typer.Argument(
        Path("."),
        exists=True,
        file_okay=False,
        dir_okay=True,
    ),
) -> None:
    """Alias for check."""
    check(repo_root)


@app.command()
def fixpack(
    repo_root: Path = typer.Argument(
        Path("."),
        exists=True,
        file_okay=False,
        dir_okay=True,
    ),
) -> None:
    """Emit placeholder fix pack artifacts."""
    repo_root = repo_root.resolve()
    try:
        config = load_config(repo_root)
    except ConfigError as exc:
        typer.secho(str(exc), fg=typer.colors.RED)
        raise typer.Exit(code=2) from exc

    artifacts, status = run_fixpack(config, repo_root)
    _print_outputs(config, artifacts)
    if status == "FAIL":
        raise typer.Exit(code=1)


@app.command()
def init(
    repo_root: Path = typer.Argument(
        Path("."),
        exists=True,
        file_okay=False,
        dir_okay=True,
    ),
    force: bool = typer.Option(
        False,
        "--force",
        help="Overwrite vibegate.yaml if it already exists.",
    ),
) -> None:
    """Generate a starter vibegate.yaml and supporting directories."""
    repo_root = repo_root.resolve()
    if not isinstance(force, bool):
        force = False
    created: List[Path] = []

    contract_path = repo_root / "vibegate.yaml"
    if contract_path.exists() and not force:
        typer.secho(
            "vibegate.yaml already exists. Use --force to overwrite.",
            fg=typer.colors.YELLOW,
        )
    else:
        contract = default_contract(repo_root)
        packaging_tool = _detect_packaging_tool(
            contract["packaging"].get(
                "tool_detection_order", ["uv", "poetry", "pdm", "pip-tools", "pip"]
            )
        )
        contract["packaging"]["tool"] = packaging_tool
        contract_path.write_text(
            yaml.safe_dump(contract, sort_keys=False), encoding="utf-8"
        )
        created.append(contract_path)

    for dirname in ("artifacts", "evidence", ".vibegate"):
        directory = repo_root / dirname
        if not directory.exists():
            directory.mkdir(parents=True, exist_ok=True)
            created.append(directory)

    suppressions_path = repo_root / ".vibegate" / "suppressions.yaml"
    if not suppressions_path.exists() or force:
        suppressions_path.parent.mkdir(parents=True, exist_ok=True)
        suppressions_path.write_text(_suppressions_template(), encoding="utf-8")
        if suppressions_path not in created:
            created.append(suppressions_path)

    typer.echo("Created:")
    if created:
        for path in created:
            typer.echo(f"- {path.relative_to(repo_root)}")
    else:
        typer.echo("- (nothing)")

    typer.echo("Next steps:")
    typer.echo("- vibegate doctor")
    typer.echo("- vibegate check")


@app.command()
def doctor(
    repo_root: Path = typer.Argument(
        Path("."),
        exists=True,
        file_okay=False,
        dir_okay=True,
    ),
) -> None:
    """Verify required tools and versions for enabled checks."""
    repo_root = repo_root.resolve()
    try:
        config = load_config(repo_root)
    except ConfigError as exc:
        typer.secho(str(exc), fg=typer.colors.RED)
        raise typer.Exit(code=2) from exc

    required_tools = _required_tools(config)
    missing = [tool for tool in sorted(required_tools) if not tool_exists(tool)]

    mismatches: List[Tuple[str, str, str]] = []
    for tool, expected in config.determinism.tool_versions.items():
        if not tool_exists(tool):
            if tool not in missing:
                missing.append(tool)
            continue
        observed_raw = tool_version(tool)
        observed = _extract_version(observed_raw) or observed_raw
        if observed != expected:
            mismatches.append((tool, expected, observed_raw))

    if missing:
        typer.secho("Missing required tools:", fg=typer.colors.RED)
        for tool in sorted(set(missing)):
            typer.echo(f"- {tool}")

    if mismatches:
        typer.secho("Version mismatches:", fg=typer.colors.RED)
        for tool, expected, observed in mismatches:
            typer.echo(f"- {tool}: expected {expected}, observed {observed}")

    if not missing and not mismatches:
        typer.secho("All required tools are available.", fg=typer.colors.GREEN)

    if missing or mismatches:
        typer.echo("Install guidance:")
        guidance_tools = sorted(set(missing + [item[0] for item in mismatches]))
        for tool in guidance_tools:
            expected = config.determinism.tool_versions.get(tool)
            typer.echo(f"- {tool}: {_install_guidance(tool, expected)}")
        raise typer.Exit(code=1)


@app.command()
def prompt() -> None:
    """Stub command for future prompt-based workflows."""
    typer.echo("not implemented")


def _print_outputs(config, artifacts: List) -> None:
    typer.echo("Created files:")
    for artifact in artifacts:
        typer.echo(f"- {artifact.path}")
    typer.echo(f"- {config.outputs.evidence_jsonl}")


def _load_run_summary(evidence_path: Path) -> dict | None:
    if not evidence_path.exists():
        return None
    summary = None
    with evidence_path.open("r", encoding="utf-8") as handle:
        for line in handle:
            line = line.strip()
            if not line:
                continue
            try:
                payload = json.loads(line)
            except json.JSONDecodeError:
                continue
            if payload.get("event_type") == "run_summary":
                summary = payload
    return summary


def _print_summary(status: str, config: VibeGateConfig) -> None:
    summary = _load_run_summary(config.outputs.evidence_jsonl)
    unsuppressed = "unknown"
    if summary and isinstance(summary.get("counts"), dict):
        count = summary["counts"].get("findings_unsuppressed")
        if isinstance(count, int):
            unsuppressed = str(count)

    typer.echo("Summary:")
    typer.echo(f"- Result: {status}")
    typer.echo(f"- Unsuppressed findings: {unsuppressed}")
    typer.echo(f"- Report: {config.outputs.report_markdown}")
    typer.echo(f"- Evidence: {config.outputs.evidence_jsonl}")


def _detect_packaging_tool(tool_detection_order: List[str]) -> str:
    for tool in tool_detection_order:
        if tool_exists(tool):
            return tool
    return "pip"


def _required_tools(config: VibeGateConfig) -> Set[str]:
    required: Set[str] = set()
    if config.checks.formatting.enabled or config.checks.lint.enabled:
        required.add("ruff")
    if config.checks.typecheck.enabled:
        required.add("pyright")
    if config.checks.tests.enabled:
        required.add("pytest")
    if config.checks.sast.enabled:
        required.add("bandit")
    if config.checks.secrets.enabled:
        required.add("gitleaks")
    if config.checks.vulnerability.enabled:
        required.add("osv-scanner")
    return required


def _extract_version(raw: str) -> Optional[str]:
    for token in raw.split():
        if token[0].isdigit():
            return token.strip()
    return None


def _install_guidance(tool: str, expected: Optional[str]) -> str:
    version = f"=={expected}" if expected else ""
    pipx_tools = {"ruff"}
    pip_tools = {"pyright", "pytest", "bandit"}
    if tool in pipx_tools:
        return f"pipx install {tool}{version}"
    if tool in pip_tools:
        return f"pip install {tool}{version}"
    if tool == "gitleaks":
        return (
            "Install via your system package manager or go install (unsure best fit)."
        )
    if tool == "osv-scanner":
        return "Install from the official OSV scanner releases (unsure best fit)."
    return "Install via your preferred package manager."


def _suppressions_template() -> str:
    return """# VibeGate suppressions
schema_version: v1alpha1
suppressions: []
  # - fingerprint: "<stable-fingerprint>"
  #   rule_id: "RULE_ID"  # Optional: match a specific rule
  #   justification: "Why this finding is acceptable"
  #   expires_at: "2099-12-31T00:00:00Z"
  #   actor: "your-name-or-team"
"""


if __name__ == "__main__":
    app()
