"""Per-episode Flora project resolver and shared atomic JSON helpers."""

from __future__ import annotations

import fcntl
import json
import logging
import os
import tempfile
import urllib.error
import urllib.request
from datetime import datetime, timezone
from pathlib import Path
from typing import Callable, Optional

logger = logging.getLogger(__name__)

_BASE_URL = "https://app.flora.ai/api/v1"
_USER_AGENT = "recoil-pipeline/1.0"


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


def _cache_read(path: str | Path) -> dict:
    try:
        raw = Path(path).read_text(encoding="utf-8")
        data = json.loads(raw)
        return data if isinstance(data, dict) else {}
    except (FileNotFoundError, OSError, json.JSONDecodeError):
        return {}


def _cache_write(path: str | Path, data: dict) -> dict:
    cache_file = Path(path)
    cache_file.parent.mkdir(parents=True, exist_ok=True)
    fd, tmp_name = tempfile.mkstemp(
        prefix=f".{cache_file.name}.",
        suffix=".tmp",
        dir=cache_file.parent,
    )
    tmp_path = Path(tmp_name)
    try:
        with os.fdopen(fd, "w", encoding="utf-8") as tmp:
            tmp.write(json.dumps(data, indent=2, sort_keys=True) + "\n")
        os.replace(tmp_path, cache_file)
    finally:
        try:
            tmp_path.unlink()
        except FileNotFoundError:
            pass
    return data


def _cache_update(path: str | Path, mutate_fn: Callable[[dict], Optional[dict]]) -> dict:
    cache_file = Path(path)
    cache_file.parent.mkdir(parents=True, exist_ok=True)
    lock_path = cache_file.with_name(f"{cache_file.name}.lock")
    lock_path.parent.mkdir(parents=True, exist_ok=True)
    with lock_path.open("a+", encoding="utf-8") as lock:
        fcntl.flock(lock.fileno(), fcntl.LOCK_EX)
        try:
            data = _cache_read(cache_file)
            updated = mutate_fn(data)
            if updated is None:
                updated = data
            return _cache_write(cache_file, updated)
        finally:
            fcntl.flock(lock.fileno(), fcntl.LOCK_UN)


def _projects_path(project: str) -> Path:
    from recoil.core.paths import projects_root

    return (
        projects_root()
        / project
        / "_pipeline"
        / "state"
        / "flora"
        / "projects.json"
    )


def _episode_key(episode: int) -> str:
    return f"ep_{episode:03d}"


def _env_project_fallback(reason: str) -> str:
    fallback = os.environ.get("RECOIL_FLORA_PROJECT")
    logger.warning(
        "Flora per-episode project resolution fell back to "
        "RECOIL_FLORA_PROJECT: %s",
        reason,
    )
    if fallback:
        return fallback
    raise RuntimeError(
        "RECOIL_FLORA_PROJECT environment variable not set. "
        "Flora requires a project_id (starts with prj_)."
    )


def _create_project(*, api_key: str, workspace_id: str, name: str) -> str:
    body = json.dumps({"workspace_id": workspace_id, "name": name}).encode()
    req = urllib.request.Request(
        f"{_BASE_URL}/projects",
        data=body,
        headers={
            "Authorization": f"Bearer {api_key}",
            "Content-Type": "application/json",
            "User-Agent": _USER_AGENT,
        },
        method="POST",
    )
    try:
        with urllib.request.urlopen(req, timeout=30) as resp:
            data = json.loads(resp.read().decode())
    except urllib.error.HTTPError as e:
        err = e.read().decode() if e.fp else ""
        raise RuntimeError(f"Flora project create failed: HTTP {e.code}: {err}") from e
    except (OSError, json.JSONDecodeError) as e:
        raise RuntimeError(f"Flora project create failed: {e}") from e

    project_id = data.get("project_id") or data.get("id")
    if not project_id:
        raise RuntimeError(f"Flora project create response missing project id: {data}")
    return str(project_id)


def resolve_flora_project(
    project: str,
    episode: Optional[int],
    *,
    api_key: str,
    workspace_id: str,
    create: bool = True,
) -> str:
    """Return the Flora project id for an episode, fail-opening to env fallback."""
    if episode is None:
        return _env_project_fallback("episode is not set")

    mapping_file = _projects_path(project)
    key = _episode_key(episode)
    entry = _cache_read(mapping_file).get(key)
    if isinstance(entry, dict) and entry.get("project_id"):
        return str(entry["project_id"])

    if not create:
        return _env_project_fallback(f"{key} is not mapped and create=False")

    resolved: dict[str, str] = {}

    def _persist(data: dict) -> dict:
        current = data.get(key)
        if isinstance(current, dict) and current.get("project_id"):
            resolved["project_id"] = str(current["project_id"])
            return data

        created_id = _create_project(
            api_key=api_key,
            workspace_id=workspace_id,
            name=f"recoil-{project}-ep{episode:03d}",
        )
        resolved["project_id"] = created_id
        data[key] = {"project_id": created_id, "created_at": _utc_now_iso()}
        return data

    try:
        _cache_update(mapping_file, _persist)
    except Exception as e:
        return _env_project_fallback(str(e))
    return resolved["project_id"]


__all__ = [
    "resolve_flora_project",
    "_cache_read",
    "_cache_write",
    "_cache_update",
    "_projects_path",
]
