"""
OpenClaw jobs.json 安全读写。

目标：
- 单一入口，避免多处无锁读改写
- 追加写入使用文件锁
- 落盘使用原子替换
"""

from __future__ import annotations

import json
import os
from contextlib import contextmanager
from pathlib import Path
from tempfile import NamedTemporaryFile
from typing import Iterator

try:
    import fcntl
except ImportError:  # pragma: no cover - Windows fallback
    fcntl = None

_DEFAULT_JOBS_PATH = Path.home() / ".openclaw" / "cron" / "jobs.json"


def _default_jobs() -> dict:
    return {"version": 1, "jobs": []}


def resolve_jobs_path(path: str | Path | None = None) -> Path:
    return Path(path) if path is not None else _DEFAULT_JOBS_PATH


def _read_jobs(path: Path) -> dict:
    try:
        with open(path, encoding="utf-8") as f:
            data = json.load(f)
    except FileNotFoundError:
        return _default_jobs()
    if not isinstance(data, dict):
        raise ValueError(f"jobs.json 格式错误：根节点应为对象，收到 {type(data).__name__}")
    data.setdefault("version", 1)
    data.setdefault("jobs", [])
    if not isinstance(data["jobs"], list):
        raise ValueError("jobs.json 格式错误：jobs 字段应为数组")
    return data


def _atomic_write_json(path: Path, data: dict) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    tmp_name = None
    try:
        with NamedTemporaryFile(
            "w",
            dir=path.parent,
            delete=False,
            encoding="utf-8",
        ) as tmp:
            json.dump(data, tmp, indent=2, ensure_ascii=False)
            tmp.flush()
            os.fsync(tmp.fileno())
            tmp_name = tmp.name
        os.replace(tmp_name, path)
    finally:
        if tmp_name and os.path.exists(tmp_name):
            os.unlink(tmp_name)


@contextmanager
def _locked_jobs(path: str | Path | None = None) -> Iterator[tuple[Path, dict]]:
    jobs_path = resolve_jobs_path(path)
    jobs_path.parent.mkdir(parents=True, exist_ok=True)
    lock_path = jobs_path.with_suffix(jobs_path.suffix + ".lock")
    with open(lock_path, "a+", encoding="utf-8") as lock_file:
        if fcntl is not None:
            fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX)
        yield jobs_path, _read_jobs(jobs_path)


def load_jobs(path: str | Path | None = None) -> dict:
    with _locked_jobs(path) as (_, data):
        return data


def list_job_names(path: str | Path | None = None) -> set[str]:
    jobs = load_jobs(path).get("jobs", [])
    return {job.get("name", "") for job in jobs if job.get("name")}


def append_job_if_absent(payload: dict, path: str | Path | None = None) -> bool:
    name = payload.get("name", "")
    with _locked_jobs(path) as (jobs_path, data):
        jobs = data.setdefault("jobs", [])
        existing = {job.get("name", "") for job in jobs}
        if name and name in existing:
            return False
        jobs.append(payload)
        _atomic_write_json(jobs_path, data)
        return True
