#!/usr/bin/env python3
"""Install npm deps by curling tarballs and extracting into node_modules (no npm network)."""
from __future__ import annotations

import json
import os
import shutil
import subprocess
import sys
import tarfile
from pathlib import Path

CURL = shutil.which("curl") or "curl"


def curl_download(url: str, dest: Path, retries: int = 6) -> None:
    dest.parent.mkdir(parents=True, exist_ok=True)
    if dest.exists() and dest.stat().st_size > 0:
        return
    tmp = dest.with_suffix(dest.suffix + ".partial")
    for attempt in range(1, retries + 1):
        try:
            subprocess.run(
                [CURL, "-fsSL", "--retry", "5", "--retry-delay", "2", "-o", str(tmp), url],
                check=True,
            )
            tmp.replace(dest)
            return
        except subprocess.CalledProcessError:
            print(f"  retry {attempt}/{retries}: {url}", flush=True)
            if attempt == retries:
                raise


def install_project(project: Path) -> None:
    lock_path = project / "package-lock.json"
    if not lock_path.exists():
        raise SystemExit(f"no lockfile: {lock_path}")

    print(f"\n=== {project} ===", flush=True)
    lock = json.loads(lock_path.read_text())
    packages = lock.get("packages") or {}
    tarball_dir = project / ".offline-tarballs"
    tarball_dir.mkdir(exist_ok=True)

    nm = project / "node_modules"
    if nm.exists():
        shutil.rmtree(nm)
    nm.mkdir()

    # packages keys like "", "node_modules/express", "node_modules/@types/node"
    entries = [(k, v) for k, v in packages.items() if k and isinstance(v, dict) and v.get("resolved")]
    print(f"  {len(entries)} packages", flush=True)

    for key, meta in entries:
        url = meta["resolved"]
        if not str(url).startswith("http"):
            continue
        # stable local name
        name = key.split("node_modules/")[-1].replace("/", "__")
        version = meta.get("version", "0")
        dest = tarball_dir / f"{name}@{version}.tgz".replace("/", "_")
        print(f"  fetch {key}", flush=True)
        curl_download(url, dest)

        target = project / key  # already includes node_modules/...
        if target.exists():
            shutil.rmtree(target)
        target.mkdir(parents=True, exist_ok=True)

        with tarfile.open(dest, "r:gz") as tar:
            # npm packs files under package/
            members = tar.getmembers()
            for m in members:
                # strip package/ prefix
                path = m.name
                if path.startswith("package/"):
                    path = path[len("package/") :]
                elif path == "package":
                    continue
                else:
                    # unexpected layout
                    if "/" in path:
                        path = path.split("/", 1)[1]
                    else:
                        continue
                if not path or path.endswith("/"):
                    continue
                m.name = path
                try:
                    tar.extract(m, path=target)
                except Exception as e:
                    print(f"  ! extract warn {key}: {e}", flush=True)

    # Ensure .bin shims for common bins from package.json bin fields
    bindir = nm / ".bin"
    bindir.mkdir(exist_ok=True)
    for key, meta in entries:
        bins = meta.get("bin")
        if not bins:
            # check package.json inside extracted dir
            pkg_json = project / key / "package.json"
            if pkg_json.exists():
                try:
                    pj = json.loads(pkg_json.read_text())
                    bins = pj.get("bin")
                except Exception:
                    bins = None
        if not bins:
            continue
        if isinstance(bins, str):
            pkg_name = key.split("node_modules/")[-1]
            # for scoped packages use last segment
            short = pkg_name.split("/")[-1]
            bins = {short: bins}
        pkg_dir = project / key
        for bin_name, rel in bins.items():
            src = pkg_dir / rel
            link = bindir / bin_name
            if link.exists() or link.is_symlink():
                link.unlink()
            # relative symlink
            try:
                link.symlink_to(os.path.relpath(src, bindir))
                # make executable if file
                if src.exists():
                    src.chmod(src.stat().st_mode | 0o111)
            except Exception as e:
                print(f"  ! bin {bin_name}: {e}", flush=True)

    print(f"  ✓ installed into {nm}", flush=True)


def main() -> None:
    root = Path(__file__).resolve().parents[1]
    projects = sys.argv[1:]
    if not projects:
        projects = [
            str(root / "nmdpr_consumption" / "backend"),
            str(root / "nmdpr_consumption" / "frontend"),
            str(root / "nmdpr_supply" / "frontend"),
        ]
    for p in projects:
        install_project(Path(p))


if __name__ == "__main__":
    main()
