from __future__ import annotations

import re
import shutil
import subprocess
import os
from pathlib import Path
from typing import Annotated, Dict, List, Optional

import typer

from iac.cli.formatting import build_table, emit_structured, ensure_output_format
from iac.runbook.deps import detect_platform_target, resolve_install_command
from iac.runbook.discovery import parse_book_dirs, resolve_book
from iac.runbook.loader import load_book, merge_vars
from iac.runbook.model import Requirement
from iac.runbook.template import TemplateEngine

app = typer.Typer(
    add_completion=False, no_args_is_help=True, help="Check requirements for a book"
)


def _book_dirs() -> List[Path]:
    v = os.environ.get("IAC_BOOK_DIRS") or os.environ.get("IAC_BOOKS_DIRS")
    return parse_book_dirs(v)


def _extract_argv_executable(
    step, *, template: TemplateEngine, vars_map: Dict[str, str]
) -> Optional[str]:
    if step.mode != "argv" or not step.argv:
        return None
    argv_obj = template.render_obj(step.argv, vars_map=vars_map, env_map=os.environ)
    if not isinstance(argv_obj, list) or not argv_obj:
        return None
    first = str(argv_obj[0]).strip()
    return first or None


def _probe_check(cmd: str) -> tuple[int, str]:
    try:
        p = subprocess.run(
            ["bash", "-lc", cmd], capture_output=True, text=True, timeout=8
        )
    except Exception as e:
        return (2, str(e))
    out = (p.stdout or "").strip()
    err = (p.stderr or "").strip()
    line = (out.splitlines()[0] if out else "") or (err.splitlines()[0] if err else "")
    return (int(p.returncode), line)


def _parse_version_token(s: str) -> Optional[tuple[int, ...]]:
    m = re.search(r"(\d+(?:\.\d+)+)", s or "")
    if not m:
        return None
    try:
        return tuple(int(x) for x in m.group(1).split("."))
    except Exception:
        return None


def _min_version_ok(found_line: str, constraint: Optional[str]) -> Optional[bool]:
    if not constraint:
        return None
    c = constraint.strip()
    if not c.startswith(">="):
        return None
    min_tok = _parse_version_token(c[2:].strip())
    found_tok = _parse_version_token(found_line)
    if min_tok is None or found_tok is None:
        return None
    return found_tok >= min_tok


@app.command("book")
def check_book(
    ref: Annotated[str, typer.Argument(help="Book name or path (.yaml/.yml).")],
    var: Annotated[
        List[str],
        typer.Option(
            "--var", help="Override vars (repeatable): --var k=v", show_default=False
        ),
    ] = [],
    vars_env: Annotated[
        Optional[str],
        typer.Option("--vars-env", envvar="IAC_VARS", help="Vars from env (IAC_VARS)"),
    ] = None,
    output: Annotated[
        str, typer.Option("--output", help="Output format: table|json|yaml")
    ] = "table",
) -> None:
    output_fmt = ensure_output_format(output)
    dirs = _book_dirs()
    bp = resolve_book(ref, dirs)
    b = load_book(bp)
    vars_map = merge_vars(b.vars, vars_env=vars_env, vars_cli=var)
    template = TemplateEngine.default()

    reqs: Dict[str, Requirement] = {r.name: r for r in b.requires}
    used_by: Dict[str, set[str]] = {name: set() for name in reqs.keys()}

    for s in b.steps:
        for rname in s.requires:
            reqs.setdefault(rname, Requirement(name=rname))
            used_by.setdefault(rname, set()).add(s.name)

        inferred = _extract_argv_executable(s, template=template, vars_map=vars_map)
        if inferred:
            reqs.setdefault(inferred, Requirement(name=inferred))
            used_by.setdefault(inferred, set()).add(s.name)

    rows: List[dict] = []
    unmet = 0
    os_name, distro_id, distro_like = detect_platform_target()
    for name in sorted(reqs.keys(), key=lambda x: x.lower()):
        req = reqs[name]
        resolved = shutil.which(req.name)
        status = "present" if resolved else "missing"
        path = resolved or ""
        notes = ""
        version_line = ""

        if resolved and req.check:
            rc, line = _probe_check(req.check)
            version_line = line
            if rc != 0:
                notes = f"check command failed (exit={rc})"
            else:
                ok = _min_version_ok(line, req.min_version)
                if ok is False:
                    status = "version_mismatch"
                    notes = f"requires {req.min_version}"
                elif ok is None and req.min_version:
                    notes = (
                        f"min_version declared ({req.min_version}) but not parseable"
                    )

        if status in ("missing", "version_mismatch"):
            unmet += 1

        target, install_cmd, tried = resolve_install_command(req.install)
        rows.append(
            {
                "book": str(bp),
                "requirement": req.name,
                "min_version": req.min_version or "",
                "status": status,
                "path": path,
                "check": req.check or "",
                "version": version_line,
                "steps": sorted(used_by.get(name, set())),
                "install_target": target or "",
                "install_cmd": install_cmd or "",
                "install_known_targets": sorted(req.install.keys()),
                "install_tried_targets": tried,
                "notes": notes,
            }
        )

    payload = {
        "book": str(bp),
        "host": {"os": os_name, "distro": distro_id, "distro_like": distro_like},
        "requirements": rows,
        "summary": {"total": len(rows), "unmet": unmet},
    }

    code = 1 if unmet > 0 else 0
    if output_fmt in ("json", "yaml"):
        emit_structured(payload, output_fmt)
        raise typer.Exit(code)

    table = build_table(
        [
            "REQUIREMENT",
            "STATUS",
            "PATH",
            "MIN_VERSION",
            "VERSION",
            "STEPS",
            "INSTALL_TARGET",
            "NOTES",
        ],
        [
            [
                r["requirement"],
                r["status"],
                r["path"],
                r["min_version"],
                r["version"],
                ", ".join(r["steps"]),
                r["install_target"],
                r["notes"],
            ]
            for r in rows
        ],
    )
    typer.echo(f"Check Book: {bp}")
    typer.echo(table.get_string())
    typer.echo(f"Summary: total={len(rows)} unmet={unmet}")
    raise typer.Exit(code)
