#!/usr/bin/env python3
import hashlib
import json
import os
import pathlib
import shutil
import subprocess
import sys
from typing import Dict, Iterable, List, Sequence


PROXY_VARS = [
    "HTTP_PROXY",
    "HTTPS_PROXY",
    "ALL_PROXY",
    "http_proxy",
    "https_proxy",
    "all_proxy",
    "GIT_HTTP_PROXY",
    "GIT_HTTPS_PROXY",
    "PIP_NO_INDEX",
]


def resolve_repo_root(start: str | None = None) -> pathlib.Path:
    base = pathlib.Path(start or os.getcwd()).resolve()

    # Preferred marker for SDT-managed projects.
    for cur in [base, *base.parents]:
        cfg = cur / "devtool.json"
        if cfg.exists():
            hints = load_project_root_hints(cur)
            if not hints:
                return cur
            if any(_hint_matches(cur, hint) for hint in hints):
                return cur

    # Fall back to git root when available.
    try:
        proc = subprocess.run(
            ["git", "-C", str(base), "rev-parse", "--show-toplevel"],
            capture_output=True,
            text=True,
            check=False,
        )
        if proc.returncode == 0:
            git_root = proc.stdout.strip()
            if git_root:
                return pathlib.Path(git_root).resolve()
    except Exception:
        pass

    return base


def load_project_root_hints(repo_root: pathlib.Path) -> list[str]:
    cfg = repo_root / "devtool.json"
    if not cfg.exists():
        return []
    try:
        data = json.loads(cfg.read_text(encoding="utf-8"))
        hints = data.get("project", {}).get("rootHints", [])
        return [str(x) for x in hints if isinstance(x, str) and x.strip()]
    except Exception:
        return []


def ensure_dirs(paths: List[pathlib.Path]) -> None:
    for p in paths:
        p.mkdir(parents=True, exist_ok=True)


def clean_proxy_env(env: Dict[str, str]) -> None:
    for k in PROXY_VARS:
        env.pop(k, None)


def dotnet_env(repo_root: pathlib.Path) -> Dict[str, str]:
    env = dict(os.environ)
    clean_proxy_env(env)
    dotnet_cli_home = repo_root / ".dotnet_home"
    nuget_packages = repo_root / ".nuget" / "packages"
    nuget_http_cache = repo_root / ".nuget" / "http-cache"
    ensure_dirs([dotnet_cli_home, nuget_packages, nuget_http_cache])
    env["DOTNET_CLI_HOME"] = str(dotnet_cli_home)
    env["NUGET_PACKAGES"] = str(nuget_packages)
    env["NUGET_HTTP_CACHE_PATH"] = str(nuget_http_cache)
    env["DOTNET_SKIP_FIRST_TIME_EXPERIENCE"] = "1"
    env["DOTNET_ADD_GLOBAL_TOOLS_TO_PATH"] = "0"
    env["DOTNET_GENERATE_ASPNET_CERTIFICATE"] = "0"
    env["DOTNET_CLI_TELEMETRY_OPTOUT"] = "1"
    env["NUGET_CERT_REVOCATION_MODE"] = "offline"
    return env


def pip_env(repo_root: pathlib.Path) -> Dict[str, str]:
    env = dict(os.environ)
    clean_proxy_env(env)
    pip_cache = repo_root / ".pip" / "cache"
    pip_tmp = repo_root / ".tmp" / "pip-temp"
    ensure_dirs([pip_cache, pip_tmp])
    env["PIP_CACHE_DIR"] = str(pip_cache)
    env["PIP_DISABLE_PIP_VERSION_CHECK"] = "1"
    env["PIP_DEFAULT_TIMEOUT"] = "30"
    env["PIP_RETRIES"] = "2"
    env["TEMP"] = str(pip_tmp)
    env["TMP"] = str(pip_tmp)
    return env


def run(command: str, args: List[str], cwd: pathlib.Path, env: Dict[str, str] | None = None) -> int:
    resolved = resolve_command(command)
    try:
        proc = subprocess.run([resolved, *args], cwd=str(cwd), env=env, check=False)
        return proc.returncode
    except FileNotFoundError:
        print(f"Command not found: {resolved}", file=sys.stderr)
        return 127


def run_capture(command: str, args: Sequence[str], cwd: pathlib.Path, env: Dict[str, str] | None = None) -> tuple[int, str, str]:
    resolved = resolve_command(command)
    try:
        proc = subprocess.run(
            [resolved, *args],
            cwd=str(cwd),
            env=env,
            capture_output=True,
            text=True,
            check=False,
        )
        return proc.returncode, proc.stdout, proc.stderr
    except FileNotFoundError:
        return 127, "", f"Command not found: {resolved}"


def resolve_command(command: str) -> str:
    if not command:
        return command

    if os.name != "nt":
        return command

    if any(sep in command for sep in ("\\", "/")):
        return command

    if pathlib.Path(command).suffix:
        found = shutil.which(command)
        return found or command

    candidates = []
    lowered = command.lower()
    if lowered in ("npm", "npx", "pnpm", "yarn", "tauri"):
        candidates.extend([f"{command}.cmd", f"{command}.exe", f"{command}.bat", command])
    else:
        candidates.append(command)

    for c in candidates:
        found = _which_windows(c)
        if found:
            name = pathlib.Path(found).name.lower()
            if name in ("npm", "npx", "pnpm", "yarn", "tauri"):
                shim = pathlib.Path(found).with_name(name + ".cmd")
                if shim.exists():
                    return str(shim)
            return found

    if lowered in ("npm", "npx", "pnpm", "yarn"):
        node = _which_windows("node.exe") or _which_windows("node")
        if node:
            node_dir = pathlib.Path(node).parent
            shim = node_dir / f"{lowered}.cmd"
            if shim.exists():
                return str(shim)

    return candidates[-1]


def _hint_matches(root: pathlib.Path, hint: str) -> bool:
    h = hint.strip()
    if not h:
        return False

    has_glob = any(ch in h for ch in ("*", "?", "["))
    if has_glob:
        # Match both anywhere in root and directly at root-level for common hints like "*.sln".
        if any(root.glob(h)):
            return True
        return any(root.rglob(h))

    marker = root / h
    if marker.exists():
        return True

    # If hint is just a filename marker, look bounded in tree.
    if not any(sep in h for sep in ("\\", "/")):
        return any(p.name == h for p in root.rglob(h))

    return False


def _expand_windows_path_segment(segment: str) -> str:
    expanded = segment
    # Expand %VAR% tokens repeatedly for nested references.
    for _ in range(4):
        next_value = os.path.expandvars(expanded)
        if next_value == expanded:
            break
        expanded = next_value
    return expanded


def _which_windows(command: str) -> str | None:
    found = shutil.which(command)
    if found:
        return found

    if os.name != "nt":
        return None

    path_value = os.environ.get("PATH", "")
    pathext = os.environ.get("PATHEXT", ".COM;.EXE;.BAT;.CMD")
    exts = [e.lower() for e in pathext.split(";") if e]

    has_ext = pathlib.Path(command).suffix != ""
    names = [command] if has_ext else [command, *(command + e.lower() for e in exts)]

    for raw_segment in path_value.split(os.pathsep):
        segment = _expand_windows_path_segment(raw_segment.strip())
        if not segment:
            continue
        base = pathlib.Path(segment)
        for name in names:
            candidate = base / name
            if candidate.exists():
                return str(candidate)

    return None


def sha256_files(paths: Iterable[pathlib.Path]) -> str:
    h = hashlib.sha256()
    for p in paths:
        if not p.exists():
            continue
        h.update(p.read_bytes())
    return h.hexdigest()


def first_existing(paths: Iterable[pathlib.Path]) -> pathlib.Path | None:
    for p in paths:
        if p.exists():
            return p
    return None


def find_csproj(repo_root: pathlib.Path, hints: Sequence[str] | None = None) -> pathlib.Path | None:
    if hints:
        for hint in hints:
            candidate = (repo_root / hint).resolve()
            if candidate.exists() and candidate.suffix.lower() == ".csproj":
                return candidate

    csprojs = sorted(repo_root.rglob("*.csproj"))
    if not csprojs:
        return None
    if len(csprojs) == 1:
        return csprojs[0]
    return None


def find_csproj_by_keyword(repo_root: pathlib.Path, keywords: Sequence[str]) -> pathlib.Path | None:
    kws = [k.lower() for k in keywords]
    matches: list[pathlib.Path] = []
    for p in repo_root.rglob("*.csproj"):
        text = str(p).lower()
        if any(k in text for k in kws):
            matches.append(p)
    if len(matches) == 1:
        return matches[0]
    return None


def find_node_app_root(repo_root: pathlib.Path, preferred: str | None = None) -> pathlib.Path | None:
    if preferred:
        p = (repo_root / preferred).resolve()
        if (p / "package.json").exists():
            return p

    direct = repo_root / "package.json"
    if direct.exists():
        return repo_root

    tauri_candidates = []
    for package_json in repo_root.rglob("package.json"):
        d = package_json.parent
        if (d / "src-tauri" / "tauri.conf.json").exists():
            tauri_candidates.append(d)
    if len(tauri_candidates) == 1:
        return tauri_candidates[0]

    all_candidates = [p.parent for p in repo_root.rglob("package.json")]
    if len(all_candidates) == 1:
        return all_candidates[0]
    return None


def newest_file(search_root: pathlib.Path, pattern: str) -> pathlib.Path | None:
    if not search_root.exists():
        return None
    files = [p for p in search_root.rglob(pattern) if p.is_file() and "\\obj\\" not in str(p).replace("/", "\\")]
    if not files:
        return None
    files.sort(key=lambda p: p.stat().st_mtime, reverse=True)
    return files[0]
