#!/usr/bin/env python3
"""Upload Projects/x → public share (github-style) with live progress JSON.

Dest: /var/www/bgrtx-share/x/
Progress: /var/www/bgrtx-share/upload-progress.json  (popup polls this)
Public: https://share.biggrinrtx.com/x/

  python3 Build/bgrtx-share-upload-x.py
"""
from __future__ import annotations

import json
import os
import shutil
import sys
import time
from pathlib import Path

SRC = Path.home() / "Projects" / "x"
DEST = Path("/var/www/bgrtx-share/x")
PROG = Path("/var/www/bgrtx-share/upload-progress.json")
SHARE = Path("/var/www/bgrtx-share")

# skip thrash / secrets / huge binary dumps · still “full” source desk
SKIP_DIR_NAMES = {
    ".git",
    "__pycache__",
    "node_modules",
    ".venv",
    "venv",
    ".mypy_cache",
    ".pytest_cache",
    ".ruff_cache",
    "OpenPaperWork",  # huge third-party tree · keep out of public share
}
SKIP_FILE_SUFFIX = {
    ".pyc",
    ".pyo",
    ".o",
    ".so",
    ".a",
    ".log",
    ".mp4",
    ".mkv",
    ".iso",
    ".zip",
    ".tar",
    ".gz",
    ".7z",
}
SKIP_NAME_PARTS = {
    "passwords.csv",
    "titan.pass",
    "xai.key",
    ".wp-oauth.json",
    "wp-oauth.json",
    "client_private.key",
    "server_private.key",
}


def write_prog(**kw):
    PROG.parent.mkdir(parents=True, exist_ok=True)
    base = {
        "ts": time.strftime("%Y-%m-%dT%H:%M:%S"),
        "src": str(SRC),
        "dest": str(DEST),
        "public": "https://share.biggrinrtx.com/x/",
        "watch": "https://share.biggrinrtx.com/upload-watch.html",
    }
    base.update(kw)
    tmp = PROG.with_suffix(".tmp")
    tmp.write_text(json.dumps(base, indent=2) + "\n", encoding="utf-8")
    tmp.replace(PROG)


def should_skip(path: Path, rel: Path) -> bool:
    parts = set(rel.parts)
    if parts & SKIP_DIR_NAMES:
        return True
    name = path.name
    if name in SKIP_NAME_PARTS:
        return True
    if path.suffix.lower() in SKIP_FILE_SUFFIX:
        return True
    # secrets dirs
    if "secrets" in parts or name.endswith(".key") and "example" not in name:
        if name.endswith(".key") and "example" not in name.lower():
            return True
    return False


def collect_files() -> list[Path]:
    files: list[Path] = []
    for root, dirs, names in os.walk(SRC):
        # prune dirs in-place
        dirs[:] = [d for d in dirs if d not in SKIP_DIR_NAMES]
        rp = Path(root)
        for n in names:
            p = rp / n
            try:
                rel = p.relative_to(SRC)
            except ValueError:
                continue
            if should_skip(p, rel):
                continue
            if not p.is_file():
                continue
            files.append(p)
    return files


def main() -> int:
    if not SRC.is_dir():
        print("missing", SRC, file=sys.stderr)
        return 1
    if not SHARE.is_dir():
        print("missing share root", SHARE, file=sys.stderr)
        return 1

    write_prog(
        running=True,
        done=False,
        ok=True,
        phase="scan",
        pct=0,
        message="scanning Projects/x …",
        files_done=0,
        files_total=0,
        bytes_done=0,
        bytes_total=0,
        current="",
        errors=[],
    )

    files = collect_files()
    sizes = []
    total = 0
    for p in files:
        try:
            sz = p.stat().st_size
        except OSError:
            sz = 0
        sizes.append(sz)
        total += sz

    write_prog(
        running=True,
        done=False,
        ok=True,
        phase="upload",
        pct=0,
        message=f"uploading {len(files)} files · {total} bytes",
        files_done=0,
        files_total=len(files),
        bytes_done=0,
        bytes_total=total,
        current="",
        errors=[],
    )

    DEST.mkdir(parents=True, exist_ok=True)
    done_b = 0
    errors: list[str] = []
    t0 = time.time()

    for i, p in enumerate(files):
        rel = p.relative_to(SRC)
        dest = DEST / rel
        try:
            dest.parent.mkdir(parents=True, exist_ok=True)
            shutil.copy2(p, dest)
            # readable on www
            try:
                os.chmod(dest, 0o644)
            except OSError:
                pass
        except Exception as e:
            errors.append(f"{rel}: {type(e).__name__}: {e}")
        done_b += sizes[i]
        pct = int(100 * done_b / total) if total else 100
        # throttle progress writes
        if i % 25 == 0 or i + 1 == len(files):
            elapsed = max(0.001, time.time() - t0)
            rate = done_b / elapsed
            rem = max(0, total - done_b)
            eta = int(rem / rate) if rate > 0 else 0
            write_prog(
                running=True,
                done=False,
                ok=True,
                phase="upload",
                pct=min(99, pct),
                message=f"{i+1}/{len(files)} · {pct}%",
                files_done=i + 1,
                files_total=len(files),
                bytes_done=done_b,
                bytes_total=total,
                current=str(rel),
                rate_bps=int(rate),
                eta_s=eta,
                errors=errors[-20:],
                error_n=len(errors),
            )

    # tree index stub
    (DEST / "README_SHARE.txt").write_text(
        "BigGrinRTX share · Projects/x mirror · https://share.biggrinrtx.com/x/\n"
        "SOUL sole · FLAW cook · free thrift · Always Hostess 7\n",
        encoding="utf-8",
    )

    write_prog(
        running=False,
        done=True,
        ok=len(errors) == 0,
        phase="done",
        pct=100,
        message="upload complete" if not errors else f"done with {len(errors)} errors",
        files_done=len(files),
        files_total=len(files),
        bytes_done=done_b,
        bytes_total=total,
        current="",
        rate_bps=0,
        eta_s=0,
        errors=errors[-50:],
        error_n=len(errors),
        elapsed_s=round(time.time() - t0, 1),
    )
    print(f"done files={len(files)} bytes={done_b} errors={len(errors)} → {DEST}")
    return 0 if not errors else 2


if __name__ == "__main__":
    raise SystemExit(main())
