# oceansguard/frontend_policy.py
from __future__ import annotations

import json
import shutil
from pathlib import Path
from typing import Any, Dict, Tuple
import re
from dataclasses import dataclass, field
from typing import List


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


@dataclass
class FrontendPolicyResult:
    """
    cli.py は fe.failed を
      - truthy 判定
      - (label, rc) の列挙
    の両方に使うため、failed は List[Tuple[str,int]] とする。
    """
    returncode: int = 0
    warnings: List[str] = field(default_factory=list)
    failed: List[Tuple[str, int]] = field(default_factory=list)


# =========================
# Public API (tests depend on these)
# =========================
def frontend_scripts(repo: Path) -> Dict[str, str]:
    """
    frontend/package.json の scripts を返す。
    読めない/無い場合は {}。
    """
    pkg = repo / "frontend" / "package.json"
    if not pkg.is_file():
        return {}
    try:
        data = json.loads(pkg.read_text(encoding="utf-8"))
    except UnicodeDecodeError:
        data = json.loads(pkg.read_text(encoding="utf-8-sig"))
    except Exception:
        return {}

    scripts = data.get("scripts", {})
    if not isinstance(scripts, dict):
        return {}
    out: Dict[str, str] = {}
    for k, v in scripts.items():
        if isinstance(k, str) and isinstance(v, str):
            out[k] = v
    return out


def has_frontend_script(frontend_dir: Path, name: str) -> bool:
    """
    package.json に指定 script が存在するか。
    """
    return name in frontend_scripts(frontend_dir)


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

# =========================
# Internal helpers
# =========================
def _read_package_json(frontend_dir: Path) -> dict[str, Any]:
    pj = frontend_dir / "package.json"
    if not pj.exists():
        return {}
    return json.loads(pj.read_text(encoding="utf-8"))


def _rmtree_if_exists(p: Path) -> None:
    if p.exists():
        shutil.rmtree(p, ignore_errors=True)


def _frontend_clean_generated(frontend_dir: Path) -> None:
    """
    Guard側で「生成物は検問対象外」を徹底する。
    Sign 側に .eslintignore を要求しない。
    """
    _rmtree_if_exists(frontend_dir / ".vite")
    _rmtree_if_exists(frontend_dir / "dist")
    _rmtree_if_exists(frontend_dir / "node_modules" / ".cache")
    _rmtree_if_exists(frontend_dir / "node_modules" / ".tmp")


def _run_npm_install(ux: UX, frontend_dir: Path) -> int:
    lock = frontend_dir / "package-lock.json"
    if lock.exists():
        ux.info("frontend: npm ci --silent")
        return run_shell(
            ux,
            "npm ci --silent",
            cwd=str(frontend_dir),
            timeout=timeout_for_check_command(),
        )
    ux.info("frontend: npm install --silent")
    return run_shell(
        ux,
        "npm install --silent",
        cwd=str(frontend_dir),
        timeout=timeout_for_check_command(),
    )


def _run_frontend_lint(ux: UX, frontend_dir: Path) -> int:
    _frontend_clean_generated(frontend_dir)

    scripts = frontend_scripts(frontend_dir)

    if "lint" in scripts:
        ux.info("frontend: npm run lint --silent")
        return run_shell(
            ux,
            "npm run lint --silent",
            cwd=str(frontend_dir),
            timeout=timeout_for_check_command(),
        )

    # フォールバック：src だけ lint（Sign 側に設定追加させない）
    src = frontend_dir / "src"
    if src.exists():
        ux.info("frontend: npx eslint src --max-warnings=0")
        return run_shell(
            ux,
            "npx eslint src --max-warnings=0",
            cwd=str(frontend_dir),
            timeout=timeout_for_check_command(),
        )

    return 0


def _run_frontend_typecheck(ux: UX, frontend_dir: Path) -> int:
    """
    Guard が TypeScript を “直接” 検問する（npm scripts に依存しない）
    """
    scripts = frontend_scripts(frontend_dir)

    if "typecheck" in scripts:
        ux.info("frontend: npm run typecheck --silent")
        return run_shell(
            ux,
            "npm run typecheck --silent",
            cwd=str(frontend_dir),
            timeout=timeout_for_check_command(),
        )

    # 無ければ標準：tsc -b を直叩き
    tsconfig = frontend_dir / "tsconfig.json"
    if tsconfig.exists():
        ux.info("frontend: npx tsc -b --pretty false")
        return run_shell(
            ux,
            "npx tsc -b --pretty false",
            cwd=str(frontend_dir),
            timeout=timeout_for_check_command(),
        )

    return 0


def _run_frontend_build(ux: UX, frontend_dir: Path) -> int:
    scripts = frontend_scripts(frontend_dir)
    if "build" in scripts:
        ux.info("frontend: npm run build --silent")
        return run_shell(
            ux,
            "npm run build --silent",
            cwd=str(frontend_dir),
            timeout=timeout_for_check_command(),
        )
    return 0

def node_satisfies_for_vite(node_version: str) -> bool:
    """
    ルール:
      - 20.19+  または 22.12+ が必要
      - 23 以上は許容
    """
    m = re.search(r"(\d+)\.(\d+)\.(\d+)", node_version.strip())
    if not m:
        return False

    major, minor, patch = (int(m.group(1)), int(m.group(2)), int(m.group(3)))

    if major >= 23:
        return True
    if major == 22:
        return (minor, patch) >= (12, 0)
    if major == 20:
        return (minor, patch) >= (19, 0)
    return False

# =========================
# Main strict policy entry
# =========================
def run_frontend_strict_policy(repo: Path, ux: UX) -> FrontendPolicyResult:
    """
    strict検問（Guard側の責務）：
      - frontend が無ければ何もしない（returncode=0）
      - npm ci / lint / typecheck / build は「Guardが実行する」だけで、
        プロジェクトへ設定ファイルを追加しない（Signへ影響させない）
      - 失敗は failed=[(label, rc), ...] に積む（cli がそのまま表示できる）
    """
    res = FrontendPolicyResult(returncode=0)

    if not has_frontend(repo):
        return res

    frontend_dir = repo / "frontend"
    scripts = frontend_scripts(repo)

    # 0) node -v (vite 要件)
    cmd = "node -v"
    ux.info(f"frontend: {cmd}")
    cp = run_shell(
        cmd,
        cwd=repo,
        timeout=timeout_for_check_command(cmd),
        )

    node_v = (cp.stdout or "").strip()
    if cp.returncode != 0 or not node_v:
        res.failed.append(("node -v (node not found)", 1))
        res.returncode = 1
        return res

    if not node_satisfies_for_vite(node_v):
        res.failed.append((f"node version not supported for vite: {node_v}", 1))
        res.returncode = 1
        return res

    # 1) npm ci / install
    if (frontend_dir / "package-lock.json").is_file():
        cmd = "npm ci --silent"
    else:
        cmd = "npm install --silent"
    ux.info(f"frontend: {cmd}")
    cp = run_shell_stream(
        cmd,
        cwd=frontend_dir,
        timeout=timeout_for_check_command(cmd),
        print_prefix="frontend: ",
    )
    if cp.returncode != 0:
        res.failed.append((cmd, cp.returncode))
        res.returncode = 1
        return res

    # 2) lint（生成物は Guard で掃除してから）
    _frontend_clean_generated(frontend_dir)
    if "lint" in scripts:
        cmd = "npm run lint --silent"
        ux.info(f"frontend: {cmd}")
        cp = run_shell_stream(
            cmd,
            cwd=frontend_dir,
            timeout=timeout_for_check_command(cmd),
            print_prefix="frontend: ",
        )
        if cp.returncode != 0:
            res.failed.append(("npm run lint", cp.returncode))
            res.returncode = 1

    # 3) typecheck（script がなければ npx tsc -b を試す）
    if "typecheck" in scripts:
        cmd = "npm run typecheck --silent"
        ux.info(f"frontend: {cmd}")
        cp = run_shell_stream(
            cmd,
            cwd=frontend_dir,
            timeout=timeout_for_check_command(cmd),
            print_prefix="frontend: ",
        )
        if cp.returncode != 0:
            res.failed.append(("npm run typecheck", cp.returncode))
            res.returncode = 1
    else:
        tsconfig = frontend_dir / "tsconfig.json"
        if tsconfig.is_file():
            cmd = "npx tsc -b --pretty false"
            ux.info(f"frontend: {cmd}")
            cp = run_shell_stream(
                cmd,
                cwd=frontend_dir,
                timeout=timeout_for_check_command(cmd),
                print_prefix="frontend: ",
            )
            if cp.returncode != 0:
                res.failed.append((cmd, cp.returncode))
                res.returncode = 1

    # 4) build（script がある場合のみ）
    if "build" in scripts:
        cmd = "npm run build --silent"
        ux.info(f"frontend: {cmd}")
        cp = run_shell_stream(
            cmd,
            cwd=frontend_dir,
            timeout=timeout_for_check_command(cmd),
            print_prefix="frontend: ",
        )
        if cp.returncode != 0:
            res.failed.append(("npm run build", cp.returncode))
            res.returncode = 1

    return res