from __future__ import annotations

import json
import os
import subprocess
from contextlib import contextmanager
from pathlib import Path
from tempfile import NamedTemporaryFile
from typing import Any, Dict, Iterator, List, Optional, Set, Tuple, Union

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

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


def resolve_jobs_path(path: Optional[Union[str, Path]] = None) -> Path:
    env = os.environ.get("OPENCLAW_CRON_JOBS_PATH", "").strip()
    if path is not None:
        return Path(path)
    if env:
        return Path(env)
    return _DEFAULT_JOBS_PATH


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


def _read_jobs(path: Path) -> Dict[str, Any]:
    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("jobs.json root must be object")
    data.setdefault("version", 1)
    data.setdefault("jobs", [])
    if not isinstance(data["jobs"], list):
        raise ValueError("jobs.jobs must be array")
    return data


def _atomic_write_json(path: Path, data: Dict[str, Any]) -> 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: Optional[Union[str, Path]] = None) -> Iterator[Tuple[Path, Dict[str, Any]]]:
    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: Optional[Union[str, Path]] = None) -> Dict[str, Any]:
    with _locked_jobs(path) as (_, data):
        return data


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


def list_store_jobs(path: Optional[Union[str, Path]] = None) -> List[Dict[str, Any]]:
    jobs = load_jobs(path).get("jobs", [])
    return [j for j in jobs if str(j.get("name", "")).startswith(STORE_JOB_PREFIX)]


def append_job_if_absent(payload: Dict[str, Any], path: Optional[Union[str, Path]] = 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


def remove_job_by_name(name: str, path: Optional[Union[str, Path]] = None) -> bool:
    with _locked_jobs(path) as (jobs_path, data):
        jobs = data.get("jobs", [])
        new_jobs = [j for j in jobs if j.get("name") != name]
        if len(new_jobs) == len(jobs):
            return False
        data["jobs"] = new_jobs
        _atomic_write_json(jobs_path, data)
        return True


def register_via_openclaw_cli(
    name: str,
    cron_expr: str,
    message: str,
    *,
    tz: str = "Asia/Shanghai",
    delete_after_run: bool = False,
) -> bool:
    cmd = [
        "openclaw", "cron", "add",
        "--name", name,
        "--cron", cron_expr,
        "--tz", tz,
        "--session", "main",
        "--system-event", message,
    ]
    if delete_after_run:
        cmd.append("--delete-after-run")
    proc = subprocess.run(cmd, capture_output=True, text=True)
    return proc.returncode == 0


def register_job(payload: Dict[str, Any], path: Optional[Union[str, Path]] = None) -> bool:
    try:
        return append_job_if_absent(payload, path)
    except OSError:
        sched = payload.get("schedule", {})
        if sched.get("kind") == "cron":
            return register_via_openclaw_cli(
                payload.get("name", ""),
                sched.get("expr", ""),
                payload.get("payload", {}).get("message", ""),
                tz=sched.get("tz", "Asia/Shanghai"),
                delete_after_run=bool(payload.get("deleteAfterRun")),
            )
        return False
