from __future__ import annotations

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

import typer

from iac.cli.formatting import build_table, emit_structured, ensure_output_format
from iac.runbook.deps import detect_platform_target
from iac.runbook.discovery import (
    list_modules,
    parse_book_dirs,
    parse_module_dirs,
    resolve_book,
    resolve_module_file,
)
from iac.runbook.loader import load_book
from iac.runbook.module_contract import load_validated_callable
from iac.runbook.model import Requirement

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")
    return parse_book_dirs(v)


def _module_dirs() -> List[Path]:
    v = os.environ.get("IAC_MODULE_DIRS")
    return parse_module_dirs(v)


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 _probe_py_package(name: str) -> tuple[bool, str]:
    spec_expr = repr(name)
    try:
        p = subprocess.run(
            ["python3", "-m", "pip", "show", name],
            capture_output=True,
            text=True,
            timeout=8,
        )
    except Exception as e:
        # Fallback: check importability when pip is unavailable.
        try:
            ip = subprocess.run(
                [
                    "python3",
                    "-c",
                    f"import importlib.util; print(bool(importlib.util.find_spec({spec_expr})))",
                ],
                capture_output=True,
                text=True,
                timeout=8,
            )
            if ip.returncode == 0 and (ip.stdout or "").strip().lower() == "true":
                return (True, "")
        except Exception:
            pass
        return (False, str(e))
    if p.returncode != 0:
        try:
            ip = subprocess.run(
                [
                    "python3",
                    "-c",
                    f"import importlib.util; print(bool(importlib.util.find_spec({spec_expr})))",
                ],
                capture_output=True,
                text=True,
                timeout=8,
            )
            if ip.returncode == 0 and (ip.stdout or "").strip().lower() == "true":
                return (True, "")
        except Exception:
            pass
        return (False, "")
    version = ""
    for line in (p.stdout or "").splitlines():
        if line.lower().startswith("version:"):
            version = line.split(":", 1)[1].strip()
            break
    return (True, version)


def _resolve_install_hint(req: Requirement) -> Tuple[str, str, List[str]]:
    if not req.install_cmd:
        return ("", "", [])
    target = "pip" if req.req_type == "py" else "rocky10"
    return (target, req.install_cmd, [])


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).")],
    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)

    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:
        inferred = ""
        if s.mode == "argv" and s.argv:
            inferred = str(s.argv[0]).strip()
        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]
        if req.req_type == "py":
            ok, pip_version = _probe_py_package(req.name)
            status = "present" if ok else "missing"
            path = "python3 -m pip"
            version_line = pip_version
        else:
            resolved = shutil.which(req.name)
            status = "present" if resolved else "missing"
            path = resolved or ""
            version_line = ""
        notes = ""

        if status == "present" 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_hint(req)
        rows.append(
            {
                "book": str(bp),
                "requirement": req.name,
                "type": req.req_type,
                "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,
                "install_cmd": install_cmd,
                "install_known_targets": [],
                "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",
            "TYPE",
            "STATUS",
            "PATH",
            "MIN_VERSION",
            "VERSION",
            "STEPS",
            "INSTALL_TARGET",
            "NOTES",
        ],
        [
            [
                r["requirement"],
                r["type"],
                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)


def _validate_module_functions(module_ref: str, module_dirs: List[Path]) -> List[dict]:
    _ = resolve_module_file(module_ref, module_dirs)
    mods = {m.module: m for m in list_modules(module_dirs)}
    info = mods.get(module_ref)
    if info is None:
        return []
    rows: List[dict] = []
    for fn in info.functions:
        ok = True
        error = ""
        try:
            load_validated_callable(module_ref, fn.name, module_dirs=module_dirs)
        except Exception as e:
            ok = False
            error = str(e)
        rows.append(
            {
                "module": module_ref,
                "function": fn.name,
                "status": "valid" if ok else "invalid",
                "doc": fn.doc,
                "error": error,
            }
        )
    return rows


@app.command("module")
def check_module(
    ref: Annotated[str, typer.Argument(help="Module ref (for example example_ops).")],
    output: Annotated[
        str, typer.Option("--output", help="Output format: table|json|yaml")
    ] = "table",
) -> None:
    output_fmt = ensure_output_format(output)
    module_dirs = _module_dirs()
    rows = _validate_module_functions(ref, module_dirs)
    invalid = sum(1 for r in rows if r["status"] != "valid")
    payload = {
        "module": ref,
        "module_dirs": [str(d) for d in module_dirs],
        "functions": rows,
        "summary": {"total": len(rows), "invalid": invalid},
    }
    code = 1 if invalid > 0 else 0
    if output_fmt in ("json", "yaml"):
        emit_structured(payload, output_fmt)
        raise typer.Exit(code)

    table = build_table(
        ["FUNCTION", "STATUS", "DOC", "ERROR"],
        [[r["function"], r["status"], r["doc"], r["error"]] for r in rows],
    )
    typer.echo(f"Check Module: {ref}")
    typer.echo(table.get_string())
    typer.echo(f"Summary: total={len(rows)} invalid={invalid}")
    raise typer.Exit(code)


@app.command("modules")
def check_modules(
    output: Annotated[
        str, typer.Option("--output", help="Output format: table|json|yaml")
    ] = "table",
) -> None:
    output_fmt = ensure_output_format(output)
    module_dirs = _module_dirs()
    mods = list_modules(module_dirs)
    rows: List[dict] = []
    for m in mods:
        valid = 0
        invalid = 0
        errors: List[str] = []
        for fn in m.functions:
            try:
                load_validated_callable(m.module, fn.name, module_dirs=module_dirs)
                valid += 1
            except Exception as e:
                invalid += 1
                errors.append(str(e))
        rows.append(
            {
                "module": m.module,
                "function_count": len(m.functions),
                "valid": valid,
                "invalid": invalid,
                "error": m.error or ("; ".join(errors[:2]) if errors else ""),
                "path": m.path,
            }
        )
    total_invalid = sum(r["invalid"] for r in rows)
    payload = {
        "module_dirs": [str(d) for d in module_dirs],
        "modules": rows,
        "summary": {"total": len(rows), "invalid_functions": total_invalid},
    }
    code = 1 if total_invalid > 0 else 0
    if output_fmt in ("json", "yaml"):
        emit_structured(payload, output_fmt)
        raise typer.Exit(code)

    table = build_table(
        ["MODULE", "FUNCTIONS", "VALID", "INVALID", "ERROR", "PATH"],
        [
            [
                r["module"],
                r["function_count"],
                r["valid"],
                r["invalid"],
                r["error"],
                r["path"],
            ]
            for r in rows
        ],
    )
    typer.echo("Check Modules")
    typer.echo(table.get_string())
    typer.echo(f"Summary: total={len(rows)} invalid_functions={total_invalid}")
    raise typer.Exit(code)
