"""review subcommand — execute review workflows via templates."""

from __future__ import annotations

import asyncio
import json
from pathlib import Path

import typer
from openai import RateLimitError
from rich import print as rprint
from rich.console import Console
from rich.panel import Panel
from rich.text import Text

from squadron.config.manager import get_config
from squadron.integrations.context_forge import (
    ContextForgeClient,
    ContextForgeError,
    ContextForgeNotAvailable,
)
from squadron.models.aliases import resolve_model_alias
from squadron.review.git_utils import resolve_slice_diff_range
from squadron.review.models import ReviewResult, Severity, Verdict
from squadron.review.persistence import (
    SliceInfo,
    resolve_slice_info,
    save_review_result,
)
from squadron.review.review_client import run_review_with_profile
from squadron.review.rules import (
    detect_languages_from_paths,
    get_template_rules,
    load_rules_content,
    load_rules_frontmatter,
    match_rules_files,
    resolve_rules_dir,
)
from squadron.review.templates import (
    ReviewTemplate,
    get_template,
    list_templates,
    load_all_templates,
)

review_app = typer.Typer(
    name="review",
    help="Run review workflows using built-in templates.",
    no_args_is_help=True,
)

_VERDICT_COLORS: dict[Verdict, str] = {
    Verdict.PASS: "bright_green",
    Verdict.CONCERNS: "yellow",
    Verdict.FAIL: "red",
    Verdict.UNKNOWN: "dim",
}

_SEVERITY_COLORS: dict[Severity, str] = {
    Severity.PASS: "bright_green",
    Severity.NOTE: "cyan",
    Severity.CONCERN: "yellow",
    Severity.FAIL: "red",
}


# ---------------------------------------------------------------------------
# Output helpers
# ---------------------------------------------------------------------------


def display_result(
    result: ReviewResult,
    output_mode: str,
    output_path: str | None,
    verbosity: int = 0,
) -> None:
    """Format and deliver review results based on output mode."""
    match output_mode:
        case "terminal":
            _display_terminal(result, verbosity)
        case "json":
            _display_json(result)
        case "file":
            _write_file(result, output_path)
        case _:
            rprint(f"[red]Unknown output mode: {output_mode}[/red]")
            raise typer.Exit(code=1)


def _display_terminal(result: ReviewResult, verbosity: int = 0) -> None:
    """Rich-formatted terminal output with verbosity levels.

    Level 0: verdict badge + finding headings with severity
    Level 1: above + full finding descriptions
    """
    console = Console()
    color = _VERDICT_COLORS.get(result.verdict, "dim")

    header = Text(f"Review: {result.template_name}", style="bold")
    header.append("  Verdict: ", style="dim")
    header.append(result.verdict.value, style=f"bold {color}")
    if result.model is not None:
        header.append("  Model: ", style="dim")
        header.append(result.model)

    console.print(Panel(header, expand=False))

    if not result.findings:
        console.print("  No specific findings.", style="dim")
        return

    for finding in result.findings:
        sev_color = _SEVERITY_COLORS.get(finding.severity, "dim")
        console.print()  # blank line before each finding
        console.print(
            f"[{sev_color}][{finding.severity.value}][/{sev_color}] "
            f"[bold white]{finding.title}[/bold white]"
        )
        if verbosity >= 1 and finding.category:
            console.print(f"  category: {finding.category}", style="dim")
            console.print()  # blank line after category
        if verbosity >= 1 and finding.description:
            for line in finding.description.split("\n"):
                console.print(line)
        if verbosity >= 1 and finding.file_ref:
            console.print(f"  -> {finding.file_ref}", style="cyan")


def _display_json(result: ReviewResult) -> None:
    """JSON output to stdout."""
    typer.echo(json.dumps(result.to_dict(), indent=2))


def _write_file(result: ReviewResult, output_path: str | None) -> None:
    """Write JSON to file."""
    if not output_path:
        rprint("[red]Error: --output file requires a path argument.[/red]")
        raise typer.Exit(code=1)
    path = Path(output_path)
    path.write_text(json.dumps(result.to_dict(), indent=2))
    rprint(f"[green]Review result written to {path}[/green]")


# ---------------------------------------------------------------------------
# Shared helpers
# ---------------------------------------------------------------------------


def _resolve_cwd(cwd: str | None) -> str:
    """Resolve cwd: CLI flag overrides config default."""
    if cwd is not None:
        return cwd
    config_val = get_config("cwd")
    if isinstance(config_val, str):
        return config_val
    return "."


def _resolve_verbosity(verbose: int) -> int:
    """Resolve verbosity: CLI flag overrides config default."""
    if verbose > 0:
        return verbose
    config_val = get_config("verbosity")
    if isinstance(config_val, int):
        return config_val
    return 0


def _resolve_rules_content(rules_path: str | None) -> str | None:
    """Read rules file content if a path is provided."""
    if not rules_path:
        return None
    path = Path(rules_path)
    if not path.is_file():
        rprint(f"[red]Error: Rules file not found: {rules_path}[/red]")
        raise typer.Exit(code=1)
    return path.read_text()


def _extract_diff_paths(diff_ref: str, cwd: str) -> list[str]:
    """Run git diff and extract +++ b/ file paths."""
    import subprocess

    try:
        result = subprocess.run(
            ["git", "diff", "--name-only", diff_ref],
            capture_output=True,
            text=True,
            cwd=cwd,
            check=False,
        )
        if result.returncode == 0:
            return [line.strip() for line in result.stdout.splitlines() if line.strip()]
    except (FileNotFoundError, OSError):
        pass
    return []


def _resolve_arch_file(num: str) -> str:
    """Resolve an initiative index to an architecture document path.

    Searches ``project-documents/user/architecture/`` for files matching
    ``{num}-arch.*.md``.
    """
    arch_dir = Path("project-documents/user/architecture")
    pattern = f"{num}-arch.*.md"
    matches = sorted(arch_dir.glob(pattern))
    if not matches:
        rprint(
            f"[red]Error: No architecture document matching"
            f" '{pattern}' in {arch_dir}/[/red]"
        )
        raise typer.Exit(code=1)
    if len(matches) > 1:
        rprint(
            f"[yellow]Warning: Multiple arch docs for index {num},"
            f" using {matches[0].name}[/yellow]"
        )
    return str(matches[0])


def _resolve_slice_number(num: str) -> SliceInfo:
    """Resolve a bare slice number to file paths via Context-Forge.

    Delegates to ``resolve_slice_info`` (shared with pipeline review
    action) and wraps errors in CLI-friendly messages.
    """
    try:
        client = ContextForgeClient()
        return resolve_slice_info(client, int(num))
    except ContextForgeNotAvailable:
        rprint(
            "[red]Error: 'cf' (Context-Forge CLI) is not installed"
            " or not on PATH.[/red]"
        )
        raise typer.Exit(code=1)
    except (ContextForgeError, ValueError) as exc:
        rprint(f"[red]Error: {exc}[/red]")
        raise typer.Exit(code=1) from exc


def _resolve_profile(
    flag: str | None,
    template: ReviewTemplate | None = None,
) -> str:
    """Resolve profile: CLI flag → template → config → sdk.

    Model-based inference is handled upstream by alias resolution
    in _run_review_command().
    """
    if flag is not None:
        return flag
    if template is not None and template.profile is not None:
        return template.profile
    config_val = get_config("default_review_profile")
    if isinstance(config_val, str):
        return config_val
    return "sdk"


def _resolve_model(
    flag: str | None,
    template: ReviewTemplate | None = None,
    template_name: str | None = None,
) -> str | None:
    """Resolve model: CLI flag → per-template config → global config → template default.

    Per-template config keys follow the pattern ``default_model_{template}``
    (e.g. ``default_model_arch``, ``default_model_code``).
    """
    if flag is not None:
        return flag
    # Per-template config (e.g. default_model_arch)
    if template_name is not None:
        try:
            tmpl_val = get_config(f"default_model_{template_name}")
            if isinstance(tmpl_val, str):
                return tmpl_val
        except KeyError:
            pass
    # Global default
    config_val = get_config("default_model")
    if isinstance(config_val, str):
        return config_val
    if template is not None and template.model is not None:
        return template.model
    return None


def _run_review_command(
    template_name: str,
    inputs: dict[str, str],
    output: str,
    output_path: str | None,
    verbosity: int = 0,
    rules_content: str | None = None,
    model_flag: str | None = None,
    profile_flag: str | None = None,
    rules_dir: Path | None = None,
) -> ReviewResult:
    """Common logic for running a review and displaying results.

    Returns the ReviewResult so callers can save it.
    """
    load_all_templates()
    template = get_template(template_name)
    if template is None:
        available = [t.name for t in list_templates()]
        rprint(
            f"[red]Error: Unknown template '{template_name}'."
            f" Available: {available}[/red]"
        )
        raise typer.Exit(code=1)

    # Validate required inputs
    for req in template.required_inputs:
        if req.name not in inputs:
            rprint(
                f"[red]Error: Missing required input '{req.name}'"
                f" for template '{template_name}'.[/red]"
            )
            raise typer.Exit(code=1)

    # Prepend template-specific rules (review.md / review-{template}.md)
    if rules_dir is not None:
        template_rules = get_template_rules(template_name, rules_dir)
        if template_rules:
            rules_content = (
                template_rules
                if rules_content is None
                else f"{template_rules}\n\n---\n\n{rules_content}"
            )

    # Resolve model from flag → per-template config → config → template default
    raw_model = _resolve_model(model_flag, template, template_name)
    alias_model: str | None = None
    alias_profile: str | None = None
    if raw_model is not None:
        alias_model, alias_profile = resolve_model_alias(raw_model)

    resolved_model = alias_model or raw_model
    resolved_profile = _resolve_profile(profile_flag or alias_profile, template)

    try:
        result = asyncio.run(
            _execute_review(
                template,
                inputs,
                rules_content,
                resolved_model,
                resolved_profile,
                verbosity=verbosity,
            )
        )
    except RateLimitError as exc:
        rprint(
            "[red]Error: Rate limited by the API. "
            "Please wait a moment and try again.[/red]"
        )
        raise typer.Exit(code=1) from exc
    except Exception as exc:
        rprint(f"[red]Error: Review failed — {exc}[/red]")
        raise typer.Exit(code=1) from exc

    display_result(result, output, output_path, verbosity)

    return result


async def _execute_review(
    template: ReviewTemplate,
    inputs: dict[str, str],
    rules_content: str | None = None,
    model: str | None = None,
    profile: str = "sdk",
    verbosity: int = 0,
) -> ReviewResult:
    """Execute the review asynchronously."""
    return await run_review_with_profile(
        template,
        inputs,
        profile=profile,
        rules_content=rules_content,
        model=model,
        verbosity=verbosity,
    )


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


@review_app.command("slice")
def review_slice(
    input_file: str = typer.Argument(help="Document to review (or slice number)"),
    against: str | None = typer.Option(
        None, "--against", help="Architecture document to review against"
    ),
    cwd: str | None = typer.Option(
        None, "--cwd", help="Working directory (default: config or .)"
    ),
    model: str | None = typer.Option(
        None, "--model", help="Model override (e.g. opus, sonnet)"
    ),
    profile: str | None = typer.Option(
        None,
        "--profile",
        help="Provider profile (e.g. openrouter, openai, local, sdk)",
    ),
    verbose: int = typer.Option(
        0, "--verbose", "-v", count=True, help="Verbosity level (-v, -vv)"
    ),
    output: str = typer.Option(
        "terminal", "--output", help="Output format: terminal, json, file"
    ),
    output_path: str | None = typer.Option(
        None, "--output-path", help="File path for --output file"
    ),
    use_json: bool = typer.Option(
        False, "--json", help="Output and save as JSON instead of markdown"
    ),
    no_save: bool = typer.Option(False, "--no-save", help="Suppress review file save"),
    rules_dir_flag: str | None = typer.Option(
        None, "--rules-dir", help="Rules directory override"
    ),
) -> None:
    """Run a slice design review."""
    slice_info: SliceInfo | None = None
    if input_file.isdigit():
        slice_info = _resolve_slice_number(input_file)
        if not slice_info["design_file"]:
            rprint(f"[red]Error: No design file for slice {slice_info['index']}.[/red]")
            raise typer.Exit(code=1)
        input_file = slice_info["design_file"]
        against = slice_info["arch_file"]

    if not against:
        rprint("[red]Error: --against is required when not using a slice number.[/red]")
        raise typer.Exit(code=1)

    if use_json:
        output = "json"

    verbosity = _resolve_verbosity(verbose)
    resolved_cwd = _resolve_cwd(cwd)
    resolved_rules_dir = resolve_rules_dir(resolved_cwd, None, rules_dir_flag)
    inputs = {
        "input": input_file,
        "against": against,
        "cwd": resolved_cwd,
    }
    result = _run_review_command(
        "slice",
        inputs,
        output,
        output_path,
        verbosity,
        model_flag=model,
        profile_flag=profile,
        rules_dir=resolved_rules_dir,
    )

    if slice_info and not no_save:
        path = save_review_result(
            result, "slice", slice_info, as_json=use_json, input_file=input_file
        )
        rprint(f"[green]Saved review to {path}[/green]")

    if result.verdict == Verdict.FAIL:
        raise typer.Exit(code=2)


@review_app.command("arch")
def review_arch(
    input_file: str = typer.Argument(
        help="Architecture document to review (path or initiative index)"
    ),
    cwd: str | None = typer.Option(
        None, "--cwd", help="Working directory (default: config or .)"
    ),
    model: str | None = typer.Option(
        None, "--model", help="Model override (e.g. opus, sonnet)"
    ),
    profile: str | None = typer.Option(
        None,
        "--profile",
        help="Provider profile (e.g. openrouter, openai, local, sdk)",
    ),
    verbose: int = typer.Option(
        0, "--verbose", "-v", count=True, help="Verbosity level (-v, -vv)"
    ),
    output: str = typer.Option(
        "terminal", "--output", help="Output format: terminal, json, file"
    ),
    output_path: str | None = typer.Option(
        None, "--output-path", help="File path for --output file"
    ),
    use_json: bool = typer.Option(
        False, "--json", help="Output and save as JSON instead of markdown"
    ),
    no_save: bool = typer.Option(False, "--no-save", help="Suppress review file save"),
    rules_dir_flag: str | None = typer.Option(
        None, "--rules-dir", help="Rules directory override"
    ),
) -> None:
    """Review an architecture document on its own merits."""
    arch_index: int | None = None
    if input_file.isdigit():
        arch_index = int(input_file)
        input_file = _resolve_arch_file(input_file)

    if use_json:
        output = "json"

    verbosity = _resolve_verbosity(verbose)
    resolved_cwd = _resolve_cwd(cwd)
    resolved_rules_dir = resolve_rules_dir(resolved_cwd, None, rules_dir_flag)
    inputs = {
        "input": input_file,
        "cwd": resolved_cwd,
    }
    result = _run_review_command(
        "arch",
        inputs,
        output,
        output_path,
        verbosity,
        model_flag=model,
        profile_flag=profile,
        rules_dir=resolved_rules_dir,
    )

    if arch_index is not None and not no_save:
        # Build a minimal SliceInfo for save — arch reviews use initiative index
        arch_name = (
            Path(input_file).stem.split(".", 1)[1]
            if "." in Path(input_file).stem
            else Path(input_file).stem
        )
        arch_slice_info = SliceInfo(
            index=arch_index,
            name=arch_name,
            slice_name=arch_name,
            design_file=None,
            task_files=[],
            arch_file=input_file,
        )
        path = save_review_result(
            result, "arch", arch_slice_info, as_json=use_json, input_file=input_file
        )
        rprint(f"[green]Saved review to {path}[/green]")

    if result.verdict == Verdict.FAIL:
        raise typer.Exit(code=2)


@review_app.command("tasks")
def review_tasks(
    input_file: str = typer.Argument(
        help="Task breakdown file to review (or slice number)"
    ),
    against: str | None = typer.Option(
        None, "--against", help="Parent slice design to review against"
    ),
    cwd: str | None = typer.Option(
        None, "--cwd", help="Working directory (default: config or .)"
    ),
    model: str | None = typer.Option(
        None, "--model", help="Model override (e.g. opus, sonnet)"
    ),
    profile: str | None = typer.Option(
        None,
        "--profile",
        help="Provider profile (e.g. openrouter, openai, local, sdk)",
    ),
    verbose: int = typer.Option(
        0, "--verbose", "-v", count=True, help="Verbosity level (-v, -vv)"
    ),
    output: str = typer.Option(
        "terminal", "--output", help="Output format: terminal, json, file"
    ),
    output_path: str | None = typer.Option(
        None, "--output-path", help="File path for --output file"
    ),
    use_json: bool = typer.Option(
        False, "--json", help="Output and save as JSON instead of markdown"
    ),
    no_save: bool = typer.Option(False, "--no-save", help="Suppress review file save"),
    rules_dir_flag: str | None = typer.Option(
        None, "--rules-dir", help="Rules directory override"
    ),
) -> None:
    """Run a task plan review."""
    slice_info: SliceInfo | None = None
    if input_file.isdigit():
        slice_info = _resolve_slice_number(input_file)
        if not slice_info["task_files"]:
            rprint(f"[red]Error: No task file for slice {slice_info['index']}.[/red]")
            raise typer.Exit(code=1)
        if not slice_info["design_file"]:
            rprint(f"[red]Error: No design file for slice {slice_info['index']}.[/red]")
            raise typer.Exit(code=1)
        input_file = f"project-documents/user/tasks/{slice_info['task_files'][0]}"
        against = slice_info["design_file"]

    if not against:
        rprint("[red]Error: --against is required when not using a slice number.[/red]")
        raise typer.Exit(code=1)

    if use_json:
        output = "json"

    verbosity = _resolve_verbosity(verbose)
    resolved_cwd = _resolve_cwd(cwd)
    resolved_rules_dir = resolve_rules_dir(resolved_cwd, None, rules_dir_flag)
    inputs = {
        "input": input_file,
        "against": against,
        "cwd": resolved_cwd,
    }
    result = _run_review_command(
        "tasks",
        inputs,
        output,
        output_path,
        verbosity,
        model_flag=model,
        profile_flag=profile,
        rules_dir=resolved_rules_dir,
    )

    if slice_info and not no_save:
        path = save_review_result(
            result, "tasks", slice_info, as_json=use_json, input_file=input_file
        )
        rprint(f"[green]Saved review to {path}[/green]")

    if result.verdict == Verdict.FAIL:
        raise typer.Exit(code=2)


@review_app.command("code")
def review_code(
    slice_number: str | None = typer.Argument(
        None, help="Optional slice number for context (e.g. 118)"
    ),
    cwd: str | None = typer.Option(
        None, "--cwd", help="Project directory (default: config or .)"
    ),
    files: str | None = typer.Option(
        None, "--files", help="Glob pattern to scope the review"
    ),
    diff: str | None = typer.Option(None, "--diff", help="Git ref to diff against"),
    rules: str | None = typer.Option(
        None, "--rules", help="Path to additional rules file"
    ),
    rules_dir_flag: str | None = typer.Option(
        None, "--rules-dir", help="Rules directory override"
    ),
    no_rules: bool = typer.Option(
        False, "--no-rules", help="Suppress all rule injection"
    ),
    model: str | None = typer.Option(
        None, "--model", help="Model override (e.g. opus, sonnet)"
    ),
    profile: str | None = typer.Option(
        None,
        "--profile",
        help="Provider profile (e.g. openrouter, openai, local, sdk)",
    ),
    verbose: int = typer.Option(
        0, "--verbose", "-v", count=True, help="Verbosity level (-v, -vv)"
    ),
    output: str = typer.Option(
        "terminal", "--output", help="Output format: terminal, json, file"
    ),
    output_path: str | None = typer.Option(
        None, "--output-path", help="File path for --output file"
    ),
    use_json: bool = typer.Option(
        False, "--json", help="Output and save as JSON instead of markdown"
    ),
    no_save: bool = typer.Option(False, "--no-save", help="Suppress review file save"),
) -> None:
    """Run a code review."""
    slice_info: SliceInfo | None = None
    if slice_number is not None and slice_number.isdigit():
        slice_info = _resolve_slice_number(slice_number)
        if not diff:
            resolved_cwd_for_diff = _resolve_cwd(cwd)
            diff = resolve_slice_diff_range(int(slice_number), resolved_cwd_for_diff)

    if use_json:
        output = "json"

    verbosity = _resolve_verbosity(verbose)
    resolved_cwd = _resolve_cwd(cwd)

    rules_content: str | None = None
    resolved_rules_dir: Path | None = None

    if not no_rules:
        # Resolve explicit rules file: CLI flag > config default
        rules_path = rules
        if not rules_path:
            config_rules = get_config("default_rules")
            if isinstance(config_rules, str):
                rules_path = config_rules
        rules_content = _resolve_rules_content(rules_path)

        # Auto-detect language rules from diff or files input
        resolved_rules_dir = resolve_rules_dir(resolved_cwd, None, rules_dir_flag)
        if resolved_rules_dir is not None:
            file_paths = _extract_diff_paths(diff, resolved_cwd) if diff else []
            if not file_paths and files:
                import glob as _glob

                file_paths = _glob.glob(files, root_dir=resolved_cwd)
            if file_paths:
                extensions = detect_languages_from_paths(file_paths)
                frontmatter = load_rules_frontmatter(resolved_rules_dir)
                auto_files = match_rules_files(
                    extensions, resolved_rules_dir, frontmatter
                )
                auto_content = load_rules_content(auto_files)
                if auto_content:
                    rules_content = (
                        auto_content
                        if rules_content is None
                        else f"{rules_content}\n\n---\n\n{auto_content}"
                    )

    inputs: dict[str, str] = {"cwd": resolved_cwd}
    if files:
        inputs["files"] = files
    if diff:
        inputs["diff"] = diff
    result = _run_review_command(
        "code",
        inputs,
        output,
        output_path,
        verbosity,
        rules_content,
        model_flag=model,
        profile_flag=profile,
        rules_dir=resolved_rules_dir,
    )

    if slice_info and not no_save:
        path = save_review_result(result, "code", slice_info, as_json=use_json)
        rprint(f"[green]Saved review to {path}[/green]")

    if result.verdict == Verdict.FAIL:
        raise typer.Exit(code=2)


@review_app.command("list")
def review_list() -> None:
    """List available review templates."""
    load_all_templates()
    templates = list_templates()
    if not templates:
        rprint("[dim]No templates available.[/dim]")
        return

    rprint("[bold]Available review templates:[/bold]")
    max_name_len = max(len(t.name) for t in templates)
    for t in templates:
        rprint(f"  {t.name:<{max_name_len}}  {t.description}")
