"""
本地事件存储。

使用 JSON 文件作为单用户场景下的轻量事件仓储：
- 事件持久化
- 原子写入
- 简单文件锁
- 查询 / 删除 / 同步状态更新
"""

from __future__ import annotations

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

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

from config.settings import LOCAL_EVENTS_FILE
from gcal.models import CalendarEvent


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


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


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)


def _normalize_record(raw: dict) -> dict:
    record = dict(raw)
    record.setdefault("id", f"local_{uuid.uuid4().hex[:12]}")
    record.setdefault("summary", "无标题")
    record.setdefault("status", "confirmed")
    record.setdefault("is_all_day", False)
    record.setdefault("location", "")
    record.setdefault("description", "")
    record.setdefault("remind_minutes", 0)
    record.setdefault("google_event_id", "")
    record.setdefault("sync_state", "local_only")
    record.setdefault("last_sync_error", "")
    record.setdefault("created_at", record.get("start", ""))
    record.setdefault("updated_at", record.get("created_at", ""))
    return record


def _read_store(path: Path) -> dict:
    try:
        with open(path, encoding="utf-8") as f:
            data = json.load(f)
    except FileNotFoundError:
        return _default_store()
    if not isinstance(data, dict):
        raise ValueError("local_events.json 格式错误：根节点必须是对象")
    data.setdefault("version", 1)
    events = data.setdefault("events", [])
    if not isinstance(events, list):
        raise ValueError("local_events.json 格式错误：events 必须是数组")
    data["events"] = [_normalize_record(item) for item in events if isinstance(item, dict)]
    return data


@contextmanager
def _locked_store(path: str | Path | None = None) -> Iterator[tuple[Path, dict]]:
    store_path = resolve_store_path(path)
    store_path.parent.mkdir(parents=True, exist_ok=True)
    lock_path = store_path.with_suffix(store_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 store_path, _read_store(store_path)


def _ensure_aware(dt: datetime) -> datetime:
    if dt.tzinfo is None:
        raise ValueError("datetime 必须携带时区信息")
    return dt


def record_to_event(record: dict) -> CalendarEvent:
    return CalendarEvent(
        id=record["id"],
        summary=record.get("summary", "无标题"),
        start=datetime.fromisoformat(record["start"]),
        end=datetime.fromisoformat(record["end"]),
        status=record.get("status", "confirmed"),
        is_all_day=bool(record.get("is_all_day", False)),
        location=record.get("location", ""),
        description=record.get("description", ""),
    )


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


def list_records(
    time_min: datetime | None = None,
    time_max: datetime | None = None,
    include_cancelled: bool = False,
    path: str | Path | None = None,
) -> list[dict]:
    with _locked_store(path) as (_, data):
        records = []
        for raw in data.get("events", []):
            record = _normalize_record(raw)
            if not include_cancelled and record.get("status") == "cancelled":
                continue
            event = record_to_event(record)
            if time_min is not None and _ensure_aware(event.end) <= _ensure_aware(time_min):
                continue
            if time_max is not None and _ensure_aware(event.start) >= _ensure_aware(time_max):
                continue
            records.append(record)
        records.sort(key=lambda item: item.get("start", ""))
        return records


def list_events(
    time_min: datetime | None = None,
    time_max: datetime | None = None,
    include_cancelled: bool = False,
    path: str | Path | None = None,
) -> list[CalendarEvent]:
    return [
        record_to_event(record)
        for record in list_records(
            time_min=time_min,
            time_max=time_max,
            include_cancelled=include_cancelled,
            path=path,
        )
    ]


def get_record(event_id: str, path: str | Path | None = None) -> dict | None:
    with _locked_store(path) as (_, data):
        for raw in data.get("events", []):
            record = _normalize_record(raw)
            if record["id"] == event_id:
                return record
    return None


def get_event_status(event_id: str, path: str | Path | None = None) -> str | None:
    record = get_record(event_id, path=path)
    return record.get("status") if record else None


def create_event(
    summary: str,
    start: datetime,
    end: datetime,
    location: str = "",
    description: str = "",
    remind_minutes: int = 0,
    path: str | Path | None = None,
) -> dict:
    start = _ensure_aware(start)
    end = _ensure_aware(end)
    now = datetime.now(start.tzinfo)
    record = _normalize_record({
        "id": f"local_{uuid.uuid4().hex[:12]}",
        "summary": summary,
        "start": start.isoformat(),
        "end": end.isoformat(),
        "status": "confirmed",
        "is_all_day": False,
        "location": location,
        "description": description,
        "remind_minutes": int(remind_minutes),
        "google_event_id": "",
        "sync_state": "local_only",
        "last_sync_error": "",
        "created_at": now.isoformat(),
        "updated_at": now.isoformat(),
    })
    with _locked_store(path) as (store_path, data):
        events = data.setdefault("events", [])
        events.append(record)
        _atomic_write_json(store_path, data)
    return record


def delete_event(event_id: str, path: str | Path | None = None) -> bool:
    with _locked_store(path) as (store_path, data):
        changed = False
        for record in data.get("events", []):
            normalized = _normalize_record(record)
            if normalized["id"] != event_id:
                continue
            record["status"] = "cancelled"
            record["updated_at"] = datetime.now().isoformat()
            if not record.get("sync_state"):
                record["sync_state"] = "local_only"
            changed = True
            break
        if changed:
            _atomic_write_json(store_path, data)
        return changed


def update_sync_status(
    event_id: str,
    sync_state: str,
    google_event_id: str = "",
    last_sync_error: str = "",
    path: str | Path | None = None,
) -> bool:
    with _locked_store(path) as (store_path, data):
        changed = False
        for record in data.get("events", []):
            normalized = _normalize_record(record)
            if normalized["id"] != event_id:
                continue
            record["sync_state"] = sync_state
            record["google_event_id"] = google_event_id
            record["last_sync_error"] = last_sync_error
            record["updated_at"] = datetime.now().isoformat()
            changed = True
            break
        if changed:
            _atomic_write_json(store_path, data)
        return changed


def list_syncable_records(path: str | Path | None = None) -> list[dict]:
    records = list_records(include_cancelled=False, path=path)
    result = []
    for record in records:
        if record.get("status") != "confirmed":
            continue
        if record.get("sync_state") == "synced" and record.get("google_event_id"):
            continue
        result.append(record)
    return result
