from __future__ import annotations

import fcntl
import json
import secrets
import time
from dataclasses import asdict, dataclass
from datetime import datetime, timezone
from pathlib import Path
from typing import Any

from niannian.storage.manifest import utc_now_iso


@dataclass
class UploadProgress:
    job_id: str
    provider: str
    rel_path: str
    bytes_total: int
    bytes_done: int
    chunk_index: int
    chunk_total: int
    phase: str
    message: str
    started_at: str
    updated_at: str
    error: str | None = None
    checkpoint_id: str | None = None

    @property
    def percent(self) -> float:
        if self.bytes_total <= 0:
            return 0.0
        return round(100.0 * self.bytes_done / self.bytes_total, 1)

    def to_dict(self) -> dict[str, Any]:
        out = asdict(self)
        out["percent"] = self.percent
        out["resumable"] = self.phase in ("uploading", "paused", "failed")
        return out


def _now_id() -> str:
    ts = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S")
    return f"up_{ts}_{secrets.token_hex(3)}"


class UploadProgressTracker:
    """Best-effort upload job progress — persisted at {home}/sync/upload_progress.json."""

    def __init__(self, home: Path, *, progress_file: str = "sync/upload_progress.json") -> None:
        self._path = home / progress_file
        self._path.parent.mkdir(parents=True, exist_ok=True)

    @property
    def path(self) -> Path:
        return self._path

    def _read_state(self) -> dict[str, Any]:
        if not self._path.is_file():
            return {"active_job_id": None, "jobs": {}}
        try:
            with self._path.open("r", encoding="utf-8") as fh:
                fcntl.flock(fh.fileno(), fcntl.LOCK_SH)
                try:
                    raw = json.load(fh)
                finally:
                    fcntl.flock(fh.fileno(), fcntl.LOCK_UN)
        except (OSError, json.JSONDecodeError):
            return {"active_job_id": None, "jobs": {}}
        raw.setdefault("jobs", {})
        return raw

    def _write_state(self, state: dict[str, Any]) -> None:
        self._path.parent.mkdir(parents=True, exist_ok=True)
        with self._path.open("w", encoding="utf-8") as fh:
            fcntl.flock(fh.fileno(), fcntl.LOCK_EX)
            try:
                fh.write(json.dumps(state, ensure_ascii=False, indent=2) + "\n")
            finally:
                fcntl.flock(fh.fileno(), fcntl.LOCK_UN)

    def _mutate(self, mutator) -> None:
        self._path.parent.mkdir(parents=True, exist_ok=True)
        if not self._path.is_file():
            self._path.write_text(
                json.dumps({"active_job_id": None, "jobs": {}}, ensure_ascii=False, indent=2) + "\n",
                encoding="utf-8",
            )
        with self._path.open("r+", encoding="utf-8") as fh:
            fcntl.flock(fh.fileno(), fcntl.LOCK_EX)
            try:
                fh.seek(0)
                raw_text = fh.read()
                try:
                    state = json.loads(raw_text) if raw_text.strip() else {"active_job_id": None, "jobs": {}}
                except json.JSONDecodeError:
                    state = {"active_job_id": None, "jobs": {}}
                state.setdefault("jobs", {})
                mutator(state)
                fh.seek(0)
                fh.truncate()
                fh.write(json.dumps(state, ensure_ascii=False, indent=2) + "\n")
            finally:
                fcntl.flock(fh.fileno(), fcntl.LOCK_UN)

    def start_job(
        self,
        *,
        provider: str,
        rel_path: str,
        bytes_total: int,
        chunk_total: int,
        checkpoint_id: str | None = None,
        job_id: str | None = None,
    ) -> str:
        jid = job_id or _now_id()
        now = utc_now_iso()

        def mutator(state: dict[str, Any]) -> None:
            jobs = state.setdefault("jobs", {})
            jobs[jid] = {
                "job_id": jid,
                "provider": provider,
                "rel_path": rel_path,
                "bytes_total": int(bytes_total),
                "bytes_done": 0,
                "chunk_index": -1,
                "chunk_total": int(chunk_total),
                "phase": "uploading",
                "message": "uploading",
                "started_at": now,
                "updated_at": now,
                "error": None,
                "checkpoint_id": checkpoint_id,
            }
            state["active_job_id"] = jid

        self._mutate(mutator)
        return jid

    def on_chunk_done(self, job_id: str, *, bytes_done: int, chunk_index: int) -> None:
        def mutator(state: dict[str, Any]) -> None:
            job = (state.get("jobs") or {}).get(job_id)
            if not job:
                return
            job["bytes_done"] = int(bytes_done)
            job["chunk_index"] = int(chunk_index)
            job["phase"] = "uploading"
            job["updated_at"] = utc_now_iso()

        self._mutate(mutator)

    def finish_job(self, job_id: str, *, ok: bool, error: str | None = None) -> None:
        def mutator(state: dict[str, Any]) -> None:
            job = (state.get("jobs") or {}).get(job_id)
            if not job:
                return
            job["phase"] = "done" if ok else "failed"
            job["updated_at"] = utc_now_iso()
            if error:
                job["error"] = error[:500]
            if state.get("active_job_id") == job_id:
                state["active_job_id"] = None

        self._mutate(mutator)

    def _job_from_raw(self, raw: dict[str, Any]) -> UploadProgress:
        return UploadProgress(
            job_id=str(raw["job_id"]),
            provider=str(raw["provider"]),
            rel_path=str(raw["rel_path"]),
            bytes_total=int(raw.get("bytes_total") or 0),
            bytes_done=int(raw.get("bytes_done") or 0),
            chunk_index=int(raw["chunk_index"]) if raw.get("chunk_index") is not None else -1,
            chunk_total=int(raw.get("chunk_total") or 0),
            phase=str(raw.get("phase") or "init"),
            message=str(raw.get("message") or ""),
            started_at=str(raw.get("started_at") or ""),
            updated_at=str(raw.get("updated_at") or ""),
            error=raw.get("error"),
            checkpoint_id=raw.get("checkpoint_id"),
        )

    def get_active(self) -> UploadProgress | None:
        state = self._read_state()
        jid = state.get("active_job_id")
        if not jid:
            return None
        raw = (state.get("jobs") or {}).get(jid)
        if not raw:
            return None
        return self._job_from_raw(raw)

    def get_job(self, job_id: str) -> UploadProgress | None:
        state = self._read_state()
        raw = (state.get("jobs") or {}).get(job_id)
        if not raw:
            return None
        return self._job_from_raw(raw)

    def list_recent(self, limit: int = 10) -> list[UploadProgress]:
        state = self._read_state()
        jobs = list((state.get("jobs") or {}).values())
        jobs.sort(key=lambda j: j.get("updated_at") or "", reverse=True)
        return [self._job_from_raw(j) for j in jobs[:limit]]
