"""异步盘后管道任务注册表 — 每个 job 独立 JSON 文件。

设计:
  - job_store/ 文件夹,每个 job 一个 {id}.json,最多保留 max_jobs 个文件
  - running/pending 状态的 job 仅存内存(高频读写)
  - succeeded/failed 后写入独立文件并从内存释放
  - 列表查询 = 内存中的活跃 job + 磁盘文件扫描,按时间排序
  - 单个查询 = 内存优先,没有则读磁盘
  - 创建新 job 前检查文件数量,>= max_jobs 时删除最老的文件
"""
from __future__ import annotations

import json
import logging
import os
import threading
import uuid
from datetime import datetime
from pathlib import Path
from typing import Any, Literal

logger = logging.getLogger(__name__)

JobStatus = Literal["pending", "running", "succeeded", "failed"]


def _default_store_dir() -> Path:
    from app.config import settings
    return settings.data_dir / "job_store"


_STORE_DIR = _default_store_dir()


class JobStore:
    def __init__(self, max_jobs: int = 50, store_dir: Path = _STORE_DIR) -> None:
        self._max_jobs = max_jobs
        self._store_dir = store_dir
        self._active_jobs: dict[str, dict[str, Any]] = {}   # running/pending
        self._active_id: str | None = None
        self._lock = threading.Lock()
        self._store_dir.mkdir(parents=True, exist_ok=True)

    # ===== persistence =====

    def _write_file(self, job: dict[str, Any]) -> None:
        """将终态 job 写入独立 JSON 文件。"""
        path = self._store_dir / f"{job['id']}.json"
        try:
            path.write_text(
                json.dumps(job, ensure_ascii=False, indent=None),
                encoding="utf-8",
            )
        except Exception:
            logger.warning("failed to write job file %s", path)

    def _read_file(self, job_id: str) -> dict[str, Any] | None:
        """从磁盘读取单个 job 文件。"""
        path = self._store_dir / f"{job_id}.json"
        if not path.exists():
            return None
        try:
            return json.loads(path.read_text("utf-8"))
        except Exception:
            logger.warning("failed to read job file %s", path)
            return None

    def _delete_oldest(self) -> None:
        """删除最老的 job 文件,保持文件数量 < max_jobs。"""
        try:
            files = sorted(self._store_dir.glob("*.json"), key=lambda f: f.stat().st_mtime)
        except Exception:
            return
        while len(files) >= self._max_jobs:
            oldest = files.pop(0)
            try:
                oldest.unlink()
            except Exception:
                logger.warning("failed to delete old job file %s", oldest)

    def _job_files_sorted(self) -> list[dict[str, Any]]:
        """扫描磁盘上所有 job 文件,按 started_at 从新到旧排序。"""
        jobs: list[dict[str, Any]] = []
        for f in self._store_dir.glob("*.json"):
            try:
                jobs.append(json.loads(f.read_text("utf-8")))
            except Exception:
                continue
        jobs.sort(key=lambda j: j.get("started_at") or "", reverse=True)
        return jobs

    # ===== lifecycle =====

    def create(self) -> str:
        with self._lock:
            if self._active_id and self._active_jobs.get(self._active_id, {}).get("status") == "running":
                return self._active_id

            job_id = uuid.uuid4().hex[:10]
            self._active_jobs[job_id] = {
                "id": job_id,
                "status": "pending",
                "stage": "init",
                "progress": 0,
                "stage_pct": 0,
                "log": [],
                "started_at": None,
                "finished_at": None,
                "duration_s": None,
                "result": None,
                "error": None,
            }
            self._active_id = job_id
            return job_id

    def start(self, job_id: str) -> None:
        with self._lock:
            j = self._active_jobs.get(job_id)
            if not j:
                return
            j["status"] = "running"
            j["started_at"] = datetime.utcnow().isoformat(timespec="seconds") + "Z"

    def succeed(self, job_id: str, result: Any) -> None:
        with self._lock:
            j = self._active_jobs.pop(job_id, None)
            if not j:
                return
            j["status"] = "succeeded"
            j["finished_at"] = datetime.utcnow().isoformat(timespec="seconds") + "Z"
            j["progress"] = 100
            j["result"] = result
            j["duration_s"] = _duration_s(j)
            if self._active_id == job_id:
                self._active_id = None
            self._delete_oldest()
            self._write_file(j)

    def fail(self, job_id: str, error: str) -> None:
        with self._lock:
            j = self._active_jobs.pop(job_id, None)
            if not j:
                return
            j["status"] = "failed"
            j["finished_at"] = datetime.utcnow().isoformat(timespec="seconds") + "Z"
            j["error"] = error
            j["duration_s"] = _duration_s(j)
            if self._active_id == job_id:
                self._active_id = None
            self._delete_oldest()
            self._write_file(j)

    # ===== progress =====

    def progress(self, job_id: str, stage: str, pct: int, msg: str,
                 stage_pct: int | None = None, skip_log: bool = False) -> None:
        with self._lock:
            j = self._active_jobs.get(job_id)
            if not j:
                return
            j["stage"] = stage
            j["progress"] = max(0, min(100, int(pct)))
            if stage_pct is not None:
                j["stage_pct"] = max(0, min(100, int(stage_pct)))
            elif j["stage"] != stage:
                j["stage_pct"] = 0
            entry = {
                "ts": datetime.utcnow().isoformat(timespec="seconds") + "Z",
                "stage": stage,
                "msg": msg,
            }
            if skip_log:
                entry["_skip"] = True
            if skip_log and j["log"] and j["log"][-1].get("stage") == stage and j["log"][-1].get("_skip"):
                j["log"][-1] = entry
            else:
                j["log"].append(entry)
                if len(j["log"]) > 200:
                    j["log"] = j["log"][-200:]

    # ===== query =====

    def get(self, job_id: str) -> dict[str, Any] | None:
        # 内存中的活跃 job 优先
        j = self._active_jobs.get(job_id)
        if j:
            return j
        # 否则从磁盘读
        return self._read_file(job_id)

    def list_recent(self, limit: int = 20) -> list[dict[str, Any]]:
        # 合并: 内存中的活跃 job + 磁盘文件
        all_jobs: list[dict[str, Any]] = list(self._active_jobs.values())
        all_jobs.extend(self._job_files_sorted())
        # 按 started_at 从新到旧排序,去重(理论上不会有重复)
        seen: set[str] = set()
        result: list[dict[str, Any]] = []
        for j in sorted(all_jobs, key=lambda x: x.get("started_at") or "", reverse=True):
            jid = j["id"]
            if jid in seen:
                continue
            seen.add(jid)
            result.append(_summary(j))
            if len(result) >= limit:
                break
        return result

    def active_id(self) -> str | None:
        return self._active_id

    def clear(self) -> None:
        """清空所有任务（内存 + 磁盘文件）。"""
        with self._lock:
            self._active_jobs.clear()
            self._active_id = None
            for f in self._store_dir.glob("*.json"):
                try:
                    f.unlink()
                except Exception:
                    pass


def _summary(j: dict[str, Any]) -> dict[str, Any]:
    return {
        "id": j["id"],
        "status": j["status"],
        "stage": j["stage"],
        "progress": j["progress"],
        "stage_pct": j.get("stage_pct", 0),
        "started_at": j["started_at"],
        "finished_at": j["finished_at"],
        "duration_s": j["duration_s"],
        "result": j["result"],
        "error": j["error"],
    }


def _duration_s(j: dict[str, Any]) -> float | None:
    if not j.get("started_at") or not j.get("finished_at"):
        return None
    try:
        s = datetime.fromisoformat(j["started_at"])
        e = datetime.fromisoformat(j["finished_at"])
        return round((e - s).total_seconds(), 2)
    except Exception:  # noqa: BLE001
        return None


# 进程内单例
job_store = JobStore()
