from __future__ import annotations

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

import typer
import yaml

from vibegate import __version__
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.callback(invoke_without_command=True)
def main(
    ctx: typer.Context,
    version: bool = typer.Option(
        False,
        "--version",
        help="Show the VibeGate version and exit.",
    ),
) -> None:
    if version:
        typer.echo(__version__)
        raise typer.Exit()
    if ctx.invoked_subcommand is None:
        typer.echo(ctx.get_help())
        raise typer.Exit()


@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()

    # Friendly error if config doesn't exist
    config_path = repo_root / "vibegate.yaml"
    if not config_path.exists():
        typer.secho(
            "No configuration found. VibeGate requires a vibegate.yaml file.",
            fg=typer.colors.RED,
        )
        typer.echo("\nTo get started, run:")
        typer.secho("  vibegate init .", fg=typer.colors.GREEN, bold=True)
        raise typer.Exit(code=2)

    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("fixpack-list")
def fixpack_list(
    path: Path = typer.Option(
        Path("artifacts/fixpack.json"),
        "--path",
        help="Path to the fixpack JSON file.",
    ),
) -> None:
    """Print a TSV summary of fixpack tasks."""
    if not path.exists():
        typer.secho(f"Fixpack not found at {path}", fg=typer.colors.RED)
        raise typer.Exit(code=2)
    with path.open("r", encoding="utf-8") as handle:
        fixpack = json.load(handle)
    groups = fixpack.get("groups", [])
    if not isinstance(groups, list):
        typer.secho("Fixpack groups are invalid.", fg=typer.colors.RED)
        raise typer.Exit(code=2)
    rows: List[Tuple[int, int, str, str, str]] = []
    for group in groups:
        group_order = group.get("order")
        if not isinstance(group_order, int):
            group_order = 0
        tasks = group.get("tasks", [])
        if not isinstance(tasks, list):
            continue
        for task in tasks:
            task_id = task.get("id", "")
            title = task.get("title", "")
            severity = task.get("severity", "unknown")
            task_order = task.get("order")
            if not isinstance(task_order, int):
                task_order = 0
            rows.append(
                (
                    group_order,
                    task_order,
                    str(task_id),
                    str(title),
                    str(severity),
                )
            )
    for _, _, task_id, title, severity in sorted(
        rows, key=lambda item: (item[0], item[1], item[2])
    ):
        typer.echo(f"{task_id}\t{title}\t{severity}")


@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.",
    ),
    yes: bool = typer.Option(
        False,
        "--yes",
        "-y",
        help="Skip interactive prompts and use recommended defaults.",
    ),
) -> None:
    """Generate a starter vibegate.yaml and supporting directories."""
    import sys
    repo_root = repo_root.resolve()
    if not isinstance(force, bool):
        force = False
    created: List[Path] = []

    # Interactive prompts (only if not --yes and running in TTY)
    use_interactive = not yes and sys.stdin.isatty()
    create_labels = True  # default

    if use_interactive:
        typer.echo("Initializing VibeGate configuration...\n")
        create_labels_input = typer.prompt(
            "Create labels.yaml for tracking false positives?",
            default="yes",
            type=str,
        )
        create_labels = create_labels_input.lower() in ("y", "yes", "")

    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.get("packaging", {}).get(
                "tool_detection_order", ["uv", "poetry", "pdm", "pip-tools", "pip"]
            )
        )
        packaging = contract.get("packaging", {})
        packaging["tool"] = packaging_tool
        contract["packaging"] = packaging
        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)

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

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

    typer.echo("\nNext steps:")
    typer.echo("  1. vibegate doctor .")
    typer.echo("  2. 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:")
        typer.echo(
            '- Recommended tooling: python -m pip install "vibegate[tools]" '
            '(or pipx install "vibegate[tools]")'
        )
        guidance_tools = sorted(set(missing + [tool for tool, _, _ 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")


@app.command()
def label(
    fingerprint: str = typer.Argument(..., help="Finding fingerprint to label"),
    false_positive: bool = typer.Option(
        False, "--false-positive", help="Mark as false positive"
    ),
    true_positive: bool = typer.Option(
        False, "--true-positive", help="Mark as true positive"
    ),
    acceptable_risk: bool = typer.Option(
        False, "--acceptable-risk", help="Mark as acceptable risk"
    ),
    reason: str = typer.Option(..., "--reason", help="Short reason tag"),
    note: str = typer.Option("", "--note", help="Optional detailed note"),
    repo_root: Path = typer.Option(
        Path("."),
        "--repo",
        exists=True,
        file_okay=False,
        dir_okay=True,
        help="Repository root directory",
    ),
) -> None:
    """Label a finding for false positive tracking.

    Labels help track finding quality but do NOT affect CI/CD behavior.
    Use suppressions.yaml to actually suppress findings in CI/CD.
    """
    from datetime import datetime, timezone

    repo_root = repo_root.resolve()

    # Validate exactly one label type
    label_flags = [false_positive, true_positive, acceptable_risk]
    if sum(label_flags) != 1:
        typer.secho(
            "Error: Specify exactly one of --false-positive, --true-positive, or --acceptable-risk",
            fg=typer.colors.RED,
        )
        raise typer.Exit(code=2)

    if false_positive:
        label_type = "false_positive"
    elif true_positive:
        label_type = "true_positive"
    else:
        label_type = "acceptable_risk"

    labels_path = repo_root / ".vibegate" / "labels.yaml"

    # Load existing labels or create new structure
    if labels_path.exists():
        with labels_path.open("r", encoding="utf-8") as f:
            data = yaml.safe_load(f) or {}
    else:
        data = {"version": 1, "labels": []}

    labels_list = data.get("labels", [])
    if not isinstance(labels_list, list):
        labels_list = []

    # Find or create entry for this fingerprint
    existing_entry = None
    for entry in labels_list:
        if entry.get("fingerprint") == fingerprint:
            existing_entry = entry
            break

    now_iso = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")

    if existing_entry:
        # Update existing entry
        existing_entry["label"] = label_type
        existing_entry["reason"] = reason
        existing_entry["note"] = note
        existing_entry["created_at"] = now_iso
        typer.echo(f"Updated label for fingerprint: {fingerprint[:12]}...")
    else:
        # Create new entry
        new_entry = {
            "fingerprint": fingerprint,
            "rule_id": "",  # Could be populated from evidence if available
            "label": label_type,
            "reason": reason,
            "note": note,
            "created_at": now_iso,
        }
        labels_list.append(new_entry)
        typer.echo(f"Added label for fingerprint: {fingerprint[:12]}...")

    data["labels"] = labels_list

    # Write back to file
    labels_path.parent.mkdir(parents=True, exist_ok=True)
    with labels_path.open("w", encoding="utf-8") as f:
        yaml.safe_dump(data, f, sort_keys=False, default_flow_style=False)

    typer.secho(f"Label '{label_type}' saved to {labels_path}", fg=typer.colors.GREEN)
    typer.echo(f"Reason: {reason}")
    if note:
        typer.echo(f"Note: {note}")


@app.command()
def version() -> None:
    """Print the VibeGate version."""
    typer.echo(__version__)


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):
        counts = summary.get("counts", {})
        count = 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 and 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 (
            "macOS: brew install gitleaks; other OS: "
            "see https://github.com/gitleaks/gitleaks#installation"
        )
    if tool == "osv-scanner":
        return (
            "macOS: brew install osv-scanner; other OS: "
            "see https://google.github.io/osv-scanner/"
        )
    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"
"""


def _labels_template() -> str:
    return """# VibeGate labels (false positive feedback)
# Labels are for tracking finding quality and informing future rule evolution.
# They do NOT affect CI/CD behavior (use suppressions.yaml for that).
version: 1
labels: []
  # - fingerprint: "<finding-fingerprint>"
  #   rule_id: "<rule_id>"
  #   label: "false_positive"  # or "true_positive" or "acceptable_risk"
  #   reason: "<short-tag>"
  #   note: "<optional-details>"
  #   created_at: "<iso-timestamp>"
"""


if __name__ == "__main__":
    app()
