"""Background workers and job tracker for bincio.serve."""
from __future__ import annotations

import logging
import os
import shutil
import subprocess
import threading
import time
import uuid
from pathlib import Path

from bincio.serve import deps

log = logging.getLogger("bincio.serve")

# ── Job tracker ───────────────────────────────────────────────────────────────

_jobs_lock = threading.Lock()
_active_jobs: dict[str, dict] = {}


def _job_start(user_handle: str, total_files: int) -> str:
    job_id = uuid.uuid4().hex[:8]
    with _jobs_lock:
        _active_jobs[job_id] = {
            "id": job_id,
            "user": user_handle,
            "started_at": int(time.time()),
            "total": total_files,
            "done": 0,
            "current": "",
        }
    return job_id


def _job_update(job_id: str, done: int, current: str) -> None:
    with _jobs_lock:
        if job_id in _active_jobs:
            _active_jobs[job_id]["done"] = done
            _active_jobs[job_id]["current"] = current


def _job_finish(job_id: str) -> None:
    with _jobs_lock:
        _active_jobs.pop(job_id, None)


# ── Post-write rebuild ────────────────────────────────────────────────────────

_rebuild_lock = threading.Lock()
_site_rebuild_event = threading.Event()

_low_priority = {"preexec_fn": lambda: os.nice(19)}


def _site_rebuild_worker() -> None:
    """Single background thread: debounced Astro build + rsync after uploads.

    Waits for _site_rebuild_event, sleeps 60 s to let upload bursts settle,
    then runs one full build. Uploads that arrive during the build set the
    event again, so a follow-up build starts after the current one finishes.
    """
    _webroot  = str(deps.webroot)
    _data_dir = str(deps.data_dir)
    _site_dir = str(deps.site_dir)
    uv = shutil.which("uv") or str(Path.home() / ".local" / "bin" / "uv")
    while True:
        _site_rebuild_event.wait()
        _site_rebuild_event.clear()
        time.sleep(60)
        _site_rebuild_event.clear()
        log.info("site-rebuild: starting full build + rsync to %s", _webroot)
        try:
            result = subprocess.run(
                [uv, "run", "bincio", "render",
                 "--data-dir", _data_dir,
                 "--site-dir", _site_dir],
                capture_output=True,
                text=True,
                **_low_priority,
            )
            if result.returncode != 0:
                log.error("site-rebuild: build failed (rc=%d):\n%s\n%s",
                          result.returncode, result.stdout, result.stderr)
                continue
            dist_data = Path(_site_dir) / "dist" / "data"
            if dist_data.exists():
                shutil.rmtree(dist_data)
            rsync = subprocess.run(
                ["rsync", "-a", "--delete", "--exclude=data/",
                 f"{_site_dir}/dist/", _webroot + "/"],
                capture_output=True,
                text=True,
                **_low_priority,
            )
            if rsync.returncode != 0:
                log.error("site-rebuild: rsync failed (rc=%d):\n%s\n%s",
                          rsync.returncode, rsync.stdout, rsync.stderr)
            else:
                log.info("site-rebuild: rsync done, generating OG images")
                og_script = Path(_site_dir).parent / "scripts" / "generate_og_images.py"
                if og_script.exists() and deps.webroot is not None:
                    og = subprocess.run(
                        [uv, "run", "python3", str(og_script),
                         "--data-dir", _data_dir,
                         "--www-root", _webroot],
                        capture_output=True,
                        text=True,
                        preexec_fn=lambda: os.nice(19),
                    )
                    if og.returncode != 0:
                        log.error("site-rebuild: og-images failed (rc=%d):\n%s\n%s",
                                  og.returncode, og.stdout, og.stderr)
                    else:
                        log.info("site-rebuild: done")
        except Exception:
            log.exception("site-rebuild: unexpected error")


def _trigger_rebuild(handle: str) -> None:
    """Merge sidecars for handle asynchronously; signal the site-rebuild worker."""
    if deps.site_dir is None:
        return
    if not deps._VALID_HANDLE.match(handle):
        return

    uv = shutil.which("uv") or str(Path.home() / ".local" / "bin" / "uv")
    _data_dir = str(deps.data_dir)
    _site_dir = str(deps.site_dir)
    _handle = handle

    def _run() -> None:
        try:
            log.info("rebuild[%s]: merge-only", _handle)
            with _rebuild_lock:
                result = subprocess.run(
                    [uv, "run", "bincio", "render",
                     "--data-dir", _data_dir,
                     "--site-dir", _site_dir,
                     "--handle", _handle,
                     "--no-build"],
                    capture_output=True,
                    text=True,
                    **_low_priority,
                )
                if result.returncode != 0:
                    log.error("rebuild[%s]: merge failed (rc=%d):\n%s\n%s",
                              _handle, result.returncode, result.stdout, result.stderr)
                else:
                    log.info("rebuild[%s]: merge done", _handle)
                    if deps.webroot is not None:
                        _site_rebuild_event.set()
        except Exception:
            log.exception("rebuild[%s]: unexpected error", _handle)

    threading.Thread(target=_run, daemon=True).start()
