# oceansguard/frontend_policy.py
from __future__ import annotations

import json
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple

from .runner import run_shell, run_shell_stream
from .types import UX
from .utils import timeout_for_check_command


@dataclass(frozen=True)
class FrontendResult:
    failed: List[Tuple[str, int]]
    warnings: List[str]


def _read_json(path: Path) -> Dict[str, Any]:
    try:
        return json.loads(path.read_text(encoding="utf-8"))
    except Exception:
        return {}


def has_frontend(repo: Path) -> bool:
    return (repo / "frontend" / "package.json").exists()


def frontend_scripts(repo: Path) -> Dict[str, str]:
    pj = repo / "frontend" / "package.json"
    data = _read_json(pj) if pj.exists() else {}
    scripts = data.get("scripts", {}) if isinstance(data.get("scripts", {}), dict) else {}
    return {k: str(v) for k, v in scripts.items()}


def node_version(repo: Path) -> Optional[str]:
    try:
        cp = run_shell("node -v", cwd=repo)
        v = (cp.stdout or "").strip()
        return v if v else None
    except Exception:
        return None


def _parse_semver(v: str) -> Tuple[int, int, int]:
    s = v.strip()
    if s.startswith("v"):
        s = s[1:]
    parts = s.split(".")
    try:
        major = int(parts[0]) if len(parts) > 0 else 0
        minor = int(parts[1]) if len(parts) > 1 else 0
        patch = int(parts[2]) if len(parts) > 2 else 0
        return major, minor, patch
    except Exception:
        return (0, 0, 0)


def node_satisfies_for_vite(v: str) -> bool:
    """
    Vite の警告に合わせる:
    - 20.19+ or 22.12+ （将来のメジャーは許容）
    """
    major, minor, patch = _parse_semver(v)
    if major == 20:
        return (minor > 19) or (minor == 19 and patch >= 0)
    if major == 22:
        return (minor > 12) or (minor == 12 and patch >= 0)
    return major > 22


def _npm_run(repo: Path, script: str, ux: UX) -> Tuple[int, List[str]]:
    warnings: List[str] = []
    fe = repo / "frontend"
    cmd = f"npm run {script} --silent"
    ux.info(f"frontend: {cmd}")
    cp = run_shell_stream(
        cmd,
        cwd=fe,
        timeout=timeout_for_check_command(cmd),
        print_prefix="[OceansGuard][frontend] ",
    )
    if cp.returncode != 0:
        stderr = cp.stderr or ""
        if "TS5072" in stderr:
            warnings.append("frontend: TS5072 detected (tsconfig/build options mismatch).")
        if "Vite requires Node.js version" in stderr:
            warnings.append("frontend: Node version does not satisfy Vite requirements.")
    return cp.returncode, warnings


def run_frontend_strict_policy(repo: Path, ux: UX) -> FrontendResult:
    failed: List[Tuple[str, int]] = []
    warnings: List[str] = []

    if not has_frontend(repo):
        return FrontendResult(failed=[], warnings=[])

    v = node_version(repo)
    if not v:
        failed.append(("node -v (node not found)", 1))
        return FrontendResult(failed=failed, warnings=warnings)

    if not node_satisfies_for_vite(v):
        failed.append((f"node version not supported for vite: {v}", 1))
        return FrontendResult(failed=failed, warnings=warnings)

    fe = repo / "frontend"
    has_lock = (fe / "package-lock.json").exists()
    install_cmd = "npm ci --silent" if has_lock else "npm install --silent"
    ux.info(f"frontend: {install_cmd}")
    cp_i = run_shell_stream(
        install_cmd,
        cwd=fe,
        timeout=timeout_for_check_command(install_cmd),
        print_prefix="[OceansGuard][frontend] ",
    )
    if cp_i.returncode != 0:
        failed.append((install_cmd, cp_i.returncode))
        return FrontendResult(failed=failed, warnings=warnings)

    scripts = frontend_scripts(repo)

    # あるものだけ叩く（街のルールに追従）
    for s in ("lint", "typecheck", "build"):
        if s in scripts:
            rc, ws = _npm_run(repo, s, ux=ux)
            warnings.extend(ws)
            if rc != 0:
                failed.append((f"npm run {s}", rc))

    return FrontendResult(failed=failed, warnings=warnings)
