#!/usr/bin/env python3
import argparse
import hashlib
import json
import os
import pathlib
import shutil
import subprocess
import sys
import time
from script_common import resolve_command


def run_step(command, args, cwd):
    resolved = resolve_command(command)
    if shutil.which(resolved) is None and not pathlib.Path(resolved).exists():
        return {
            "command": resolved,
            "args": args,
            "cwd": cwd,
            "exit_code": 127,
            "elapsed_seconds": 0.0,
            "status": "failed",
            "failure_reason": f"command_not_found:{resolved}",
        }

    started = time.time()
    proc = subprocess.run([resolved, *args], cwd=cwd, check=False)
    elapsed = round(time.time() - started, 3)
    return {
        "command": resolved,
        "args": args,
        "cwd": cwd,
        "exit_code": proc.returncode,
        "elapsed_seconds": elapsed,
        "status": "ok" if proc.returncode == 0 else "failed",
        "failure_reason": None if proc.returncode == 0 else f"non_zero_exit:{proc.returncode}",
    }


def resolve_python_executable():
    candidates = ["py", "python"] if os.name == "nt" else ["python3", "python"]
    for c in candidates:
        if shutil.which(c):
            return c
    return "python"


def parse_common(parser):
    parser.add_argument("--project-root", required=True)
    parser.add_argument("--working-dir", default=".")
    parser.add_argument("--json", action="store_true")


def resolve_cwd(project_root, working_dir):
    return os.path.abspath(os.path.join(project_root, working_dir))


EXCLUDED_SCAN_DIRS = {".git", "node_modules", "bin", "obj", ".venv", "venv", ".sdt", "dist", "build"}


def discover_dotnet_target(project_root: str, cwd: str):
    # Prefer nearest/top-level solution from cwd, then csproj, then bounded scan from project root.
    local_sln = sorted(pathlib.Path(cwd).glob("*.sln"))
    if len(local_sln) == 1:
        return str(local_sln[0])

    local_csproj = sorted(pathlib.Path(cwd).glob("*.csproj"))
    if len(local_csproj) == 1:
        return str(local_csproj[0])

    sln_hits = bounded_find_files(project_root, ".sln", max_depth=4)
    if len(sln_hits) == 1:
        return sln_hits[0]

    csproj_hits = bounded_find_files(project_root, ".csproj", max_depth=4)
    if len(csproj_hits) == 1:
        return csproj_hits[0]

    return None


def bounded_find_files(root: str, extension: str, max_depth: int):
    root_path = pathlib.Path(root).resolve()
    results = []
    for current_root, dirs, files in os.walk(root_path):
        rel = pathlib.Path(current_root).resolve().relative_to(root_path)
        depth = len(rel.parts)
        dirs[:] = [d for d in dirs if d not in EXCLUDED_SCAN_DIRS]
        if depth > max_depth:
            dirs[:] = []
            continue

        for name in files:
            if name.lower().endswith(extension.lower()):
                results.append(str(pathlib.Path(current_root) / name))
    return sorted(results)


def run_dotnet_action(project_root, working_dir, verb):
    cwd = resolve_cwd(project_root, working_dir)
    args = [verb]
    target = discover_dotnet_target(project_root, cwd)
    if target:
        args.append(target)
    step = run_step("dotnet", args, cwd)
    if target:
        step["resolved_target"] = target
    return 0 if step["exit_code"] == 0 else step["exit_code"], step


def _deps_hash(app_root):
    h = hashlib.sha256()
    for name in ("package.json", "package-lock.json"):
        p = pathlib.Path(app_root) / name
        if p.exists():
            h.update(p.read_bytes())
    return h.hexdigest()


def ensure_npm_dependencies(app_root):
    node_modules = pathlib.Path(app_root) / "node_modules"
    deps_hash_file = node_modules / ".sdt-deps.sha256"
    expected = _deps_hash(app_root)

    should_install = not node_modules.exists()
    if not should_install:
        if not deps_hash_file.exists():
            should_install = True
        else:
            current = deps_hash_file.read_text(encoding="utf-8").strip()
            should_install = current != expected

    if not should_install:
        return {"installed": False, "reason": "deps_unchanged"}

    lock_exists = (pathlib.Path(app_root) / "package-lock.json").exists()
    install_args = ["ci", "--no-audit", "--fund=false"] if lock_exists else ["install", "--no-audit", "--fund=false"]
    install_step = run_step("npm", install_args, app_root)
    if install_step["exit_code"] != 0:
        if lock_exists and install_args[0] == "ci":
            fallback = run_step("npm", ["install", "--no-audit", "--fund=false"], app_root)
            if fallback["exit_code"] != 0:
                fallback["failure_reason"] = "deps_install_failed_after_ci_fallback"
                return {"installed": True, "reason": "install_failed", "step": fallback}
            install_step = fallback
        else:
            return {"installed": True, "reason": "install_failed", "step": install_step}

    node_modules.mkdir(parents=True, exist_ok=True)
    deps_hash_file.write_text(expected, encoding="utf-8")
    return {"installed": True, "reason": "installed", "step": install_step}


def action_dotnet_build(args):
    return run_dotnet_action(args.project_root, args.working_dir, "build")


def action_dotnet_restore(args):
    return run_dotnet_action(args.project_root, args.working_dir, "restore")


def action_dotnet_test(args):
    return run_dotnet_action(args.project_root, args.working_dir, "test")


def action_dotnet_publish(args):
    return run_dotnet_action(args.project_root, args.working_dir, "publish")


def action_npm_install(args):
    cwd = resolve_cwd(args.project_root, args.working_dir)
    step = run_step("npm", ["install"], cwd)
    return 0 if step["exit_code"] == 0 else step["exit_code"], step


def action_npm_ci(args):
    cwd = resolve_cwd(args.project_root, args.working_dir)
    step = run_step("npm", ["ci"], cwd)
    return 0 if step["exit_code"] == 0 else step["exit_code"], step


def action_npm_build(args):
    cwd = resolve_cwd(args.project_root, args.working_dir)
    deps = ensure_npm_dependencies(cwd)
    if deps.get("reason") == "install_failed":
        step = deps["step"]
        step["failure_reason"] = "deps_install_failed"
        return step["exit_code"], step
    step = run_step("npm", ["run", "build"], cwd)
    return 0 if step["exit_code"] == 0 else step["exit_code"], step


def action_npm_test(args):
    cwd = resolve_cwd(args.project_root, args.working_dir)
    deps = ensure_npm_dependencies(cwd)
    if deps.get("reason") == "install_failed":
        step = deps["step"]
        step["failure_reason"] = "deps_install_failed"
        return step["exit_code"], step
    step = run_step("npm", ["test"], cwd)
    return 0 if step["exit_code"] == 0 else step["exit_code"], step


def action_npm_audit(args):
    cwd = resolve_cwd(args.project_root, args.working_dir)
    step = run_step("npm", ["audit"], cwd)
    return 0 if step["exit_code"] == 0 else step["exit_code"], step


def action_python_venv_create(args):
    cwd = resolve_cwd(args.project_root, ".")
    venv_dir = args.venv_dir or ".venv"
    step = run_step(resolve_python_executable(), ["-m", "venv", venv_dir], cwd)
    return 0 if step["exit_code"] == 0 else step["exit_code"], step


def action_python_pip_install(args):
    cwd = resolve_cwd(args.project_root, ".")
    req = args.requirements
    step = run_step(resolve_python_executable(), ["-m", "pip", "install", "-r", req], cwd)
    return 0 if step["exit_code"] == 0 else step["exit_code"], step


def action_python_pip_sync(args):
    cwd = resolve_cwd(args.project_root, ".")
    req = args.requirements
    step = run_step(resolve_python_executable(), ["-m", "pip", "install", "-r", req], cwd)
    return 0 if step["exit_code"] == 0 else step["exit_code"], step


def action_python_pytest(args):
    cwd = resolve_cwd(args.project_root, args.working_dir)
    step = run_step(resolve_python_executable(), ["-m", "pytest"], cwd)
    return 0 if step["exit_code"] == 0 else step["exit_code"], step


def action_cargo_build(args):
    cwd = resolve_cwd(args.project_root, args.working_dir)
    step = run_step("cargo", ["build"], cwd)
    return 0 if step["exit_code"] == 0 else step["exit_code"], step


def action_cargo_test(args):
    cwd = resolve_cwd(args.project_root, args.working_dir)
    step = run_step("cargo", ["test"], cwd)
    return 0 if step["exit_code"] == 0 else step["exit_code"], step


def action_tauri_build(args):
    cwd = resolve_cwd(args.project_root, args.working_dir)
    deps = ensure_npm_dependencies(cwd)
    if deps.get("reason") == "install_failed":
        step = deps["step"]
        step["failure_reason"] = "deps_install_failed"
        return step["exit_code"], step

    tauri_args = ["run", "tauri", "build"]
    if args.no_bundle:
        tauri_args.extend(["--", "--no-bundle"])
    step = run_step("npm", tauri_args, cwd)
    return 0 if step["exit_code"] == 0 else step["exit_code"], step


def action_git_status(args):
    cwd = resolve_cwd(args.project_root, args.working_dir)
    step = run_step("git", ["status"], cwd)
    return 0 if step["exit_code"] == 0 else step["exit_code"], step


def action_git_fetch(args):
    cwd = resolve_cwd(args.project_root, args.working_dir)
    step = run_step("git", ["fetch"], cwd)
    return 0 if step["exit_code"] == 0 else step["exit_code"], step


def action_git_pull(args):
    cwd = resolve_cwd(args.project_root, args.working_dir)
    step = run_step("git", ["pull"], cwd)
    return 0 if step["exit_code"] == 0 else step["exit_code"], step


def action_git_clean(args):
    cwd = resolve_cwd(args.project_root, args.working_dir)
    step = run_step("git", ["clean", "-fd"], cwd)
    return 0 if step["exit_code"] == 0 else step["exit_code"], step


def action_docker_build(args):
    cwd = resolve_cwd(args.project_root, args.working_dir)
    step = run_step("docker", ["build", "."], cwd)
    return 0 if step["exit_code"] == 0 else step["exit_code"], step


def action_docker_compose_up(args):
    cwd = resolve_cwd(args.project_root, args.working_dir)
    step = run_step("docker", ["compose", "up", "-d"], cwd)
    return 0 if step["exit_code"] == 0 else step["exit_code"], step


def action_docker_compose_down(args):
    cwd = resolve_cwd(args.project_root, args.working_dir)
    step = run_step("docker", ["compose", "down"], cwd)
    return 0 if step["exit_code"] == 0 else step["exit_code"], step


def main():
    parser = argparse.ArgumentParser(description="SDT normalized build actions")
    sub = parser.add_subparsers(dest="action", required=True)

    p0 = sub.add_parser("dotnet-restore")
    parse_common(p0)

    p1 = sub.add_parser("dotnet-build")
    parse_common(p1)

    p1b = sub.add_parser("dotnet-test")
    parse_common(p1b)

    p1c = sub.add_parser("dotnet-publish")
    parse_common(p1c)

    p2 = sub.add_parser("npm-install")
    parse_common(p2)

    p2b = sub.add_parser("npm-ci")
    parse_common(p2b)

    p3 = sub.add_parser("npm-build")
    parse_common(p3)

    p3b = sub.add_parser("npm-test")
    parse_common(p3b)

    p3c = sub.add_parser("npm-audit")
    parse_common(p3c)

    p4 = sub.add_parser("python-venv-create")
    parse_common(p4)
    p4.add_argument("--venv-dir", default=".venv")

    p5 = sub.add_parser("python-pip-install")
    parse_common(p5)
    p5.add_argument("--requirements", required=True)

    p5b = sub.add_parser("python-pip-sync")
    parse_common(p5b)
    p5b.add_argument("--requirements", required=True)

    p5c = sub.add_parser("python-pytest")
    parse_common(p5c)

    p6 = sub.add_parser("cargo-build")
    parse_common(p6)

    p6b = sub.add_parser("cargo-test")
    parse_common(p6b)

    p7 = sub.add_parser("tauri-build")
    parse_common(p7)
    p7.add_argument("--no-bundle", action="store_true")

    p8 = sub.add_parser("git-status")
    parse_common(p8)

    p9 = sub.add_parser("git-fetch")
    parse_common(p9)

    p10 = sub.add_parser("git-pull")
    parse_common(p10)

    p11 = sub.add_parser("git-clean")
    parse_common(p11)

    p12 = sub.add_parser("docker-build")
    parse_common(p12)

    p13 = sub.add_parser("docker-compose-up")
    parse_common(p13)

    p14 = sub.add_parser("docker-compose-down")
    parse_common(p14)

    args = parser.parse_args()

    handlers = {
        "dotnet-restore": action_dotnet_restore,
        "dotnet-build": action_dotnet_build,
        "dotnet-test": action_dotnet_test,
        "dotnet-publish": action_dotnet_publish,
        "npm-install": action_npm_install,
        "npm-ci": action_npm_ci,
        "npm-build": action_npm_build,
        "npm-test": action_npm_test,
        "npm-audit": action_npm_audit,
        "python-venv-create": action_python_venv_create,
        "python-pip-install": action_python_pip_install,
        "python-pip-sync": action_python_pip_sync,
        "python-pytest": action_python_pytest,
        "cargo-build": action_cargo_build,
        "cargo-test": action_cargo_test,
        "tauri-build": action_tauri_build,
        "git-status": action_git_status,
        "git-fetch": action_git_fetch,
        "git-pull": action_git_pull,
        "git-clean": action_git_clean,
        "docker-build": action_docker_build,
        "docker-compose-up": action_docker_compose_up,
        "docker-compose-down": action_docker_compose_down,
    }

    code, summary = handlers[args.action](args)
    if args.json:
        print(json.dumps(summary))
    return code


if __name__ == "__main__":
    sys.exit(main())
