"""Path resolution + atomic persistence for the litgoal durable runtime."""

from __future__ import annotations

import json
import os
import tempfile
from json import JSONDecodeError
from datetime import datetime, timezone
from pathlib import Path
from typing import Any

from . import model

# Anchor every litgoal path under the constant declared in core (dual-context).
try:  # installed as lithermes.litgoal.store
    from ..core import LITGOAL_STATE_DIRNAME
except (ImportError, ModuleNotFoundError):  # pragma: no cover - standalone import fallback
    try:
        from core import LITGOAL_STATE_DIRNAME  # type: ignore
    except (ImportError, ModuleNotFoundError):
        LITGOAL_STATE_DIRNAME = "litgoal"

try:
    from ..redaction import redact_obj
except (ImportError, ModuleNotFoundError):  # pragma: no cover - standalone import fallback
    try:
        from redaction import redact_obj  # type: ignore
    except (ImportError, ModuleNotFoundError):
        def redact_obj(value):  # type: ignore
            return value


def _utc_now() -> str:
    return datetime.now(timezone.utc).isoformat()


def state_dir(workspace: Path) -> Path:
    return Path(workspace) / ".hermes" / "lithermes" / LITGOAL_STATE_DIRNAME


def goals_path(workspace: Path) -> Path:
    return state_dir(workspace) / "goals.json"


def ledger_path(workspace: Path) -> Path:
    return state_dir(workspace) / "ledger.jsonl"


def evidence_dir(workspace: Path) -> Path:
    return state_dir(workspace) / "evidence"


def brief_path(workspace: Path) -> Path:
    return state_dir(workspace) / "brief.md"


def load_or_create(workspace: Path) -> model.LitgoalState:
    path = goals_path(workspace)
    if path.exists():
        try:
            data = json.loads(path.read_text(encoding="utf-8"))
        except JSONDecodeError as exc:
            raise ValueError(f"malformed litgoal state at {path}: {exc}") from exc
        return model.LitgoalState.from_dict(data)
    return model.LitgoalState(created_at=_utc_now(), updated_at=_utc_now())


def save(workspace: Path, state: model.LitgoalState) -> None:
    state.updated_at = _utc_now()
    if not state.created_at:
        state.created_at = state.updated_at
    target = goals_path(workspace)
    target.parent.mkdir(parents=True, exist_ok=True)
    payload = json.dumps(model.to_dict(state), indent=2, sort_keys=False, ensure_ascii=False)
    # Atomic: write to a temp file in the same dir, fsync, then os.replace.
    fd, tmp_name = tempfile.mkstemp(dir=str(target.parent), suffix=".tmp")
    try:
        with os.fdopen(fd, "w", encoding="utf-8") as handle:
            handle.write(payload + "\n")
            handle.flush()
            os.fsync(handle.fileno())
        os.replace(tmp_name, target)
        # Best-effort: fsync the parent directory so the rename is durable.
        try:
            dir_fd = os.open(str(target.parent), os.O_RDONLY)
            try:
                os.fsync(dir_fd)
            finally:
                os.close(dir_fd)
        except (OSError, PermissionError):
            pass  # platforms that disallow dir-fsync degrade silently
    finally:
        if os.path.exists(tmp_name):
            os.remove(tmp_name)


def append_ledger(workspace: Path, event: dict[str, Any]) -> None:
    path = ledger_path(workspace)
    path.parent.mkdir(parents=True, exist_ok=True)
    entry = redact_obj({"at": _utc_now(), **event})
    # NOTE: ledger appends are best-effort append-durable; no fsync here to keep
    # high-frequency event writes cheap. Data loss on crash is limited to the
    # last unflushed entry; goals.json (the source of truth) is fsync-durable.
    with path.open("a", encoding="utf-8") as handle:
        handle.write(json.dumps(entry, ensure_ascii=False) + "\n")
