from __future__ import annotations

import json
import os
import tempfile
import threading
import time
from contextlib import contextmanager
from hashlib import sha256
from pathlib import Path
from typing import Optional, Tuple

from backend.app_paths import get_workload_folder_path
from backend.utils.logging_utils import get_logger

PROMPTS_FILENAME = "prompts-library.json"
PROMPTS_DIR = Path(get_workload_folder_path()) / "config"
PROMPTS_PATH = PROMPTS_DIR / PROMPTS_FILENAME

# Path to default prompts in the codebase
DEFAULT_PROMPTS_PATH = Path(__file__).parent / "default_prompts.json"

logger = get_logger(__name__)

_LOCK = threading.RLock()


_PROMPTS_CACHE: dict | None = None
_PROMPTS_ETAG: str | None = None


def _canonical_json(obj: dict) -> str:
    return json.dumps(obj, separators=(",", ":"), sort_keys=True, ensure_ascii=False)


def _compute_etag(obj: dict) -> str:
    return sha256(_canonical_json(obj).encode("utf-8")).hexdigest()


@contextmanager
def _with_lock():
    """Serialize access to the prompts file within this process."""
    tname = threading.current_thread().name
    logger.debug("[prompts-lock] waiting (thread=%s)", tname)
    start = time.time()
    with _LOCK:
        waited = time.time() - start
        if waited >= 0.005:
            logger.debug("[prompts-lock] acquired after %.3fs (thread=%s)", waited, tname)
        else:
            logger.debug("[prompts-lock] acquired (thread=%s)", tname)
        try:
            yield
        finally:
            logger.debug("[prompts-lock] released (thread=%s)", tname)


def _atomic_write_json(path: Path, data: dict) -> None:
    """Write JSON atomically to avoid partial writes."""
    serialized = _canonical_json(data)
    dir_ = path.parent
    dir_.mkdir(parents=True, exist_ok=True)
    fd, tmp_path = tempfile.mkstemp(dir=str(dir_), prefix=path.name, suffix=".tmp")
    logger.debug("[prompts-write] temp (tmp=%s, size=%d bytes)", tmp_path, len(serialized))
    try:
        with os.fdopen(fd, "w", encoding="utf-8") as f:
            f.write(serialized)
        os.replace(tmp_path, path)
        logger.debug("[prompts-write] replace OK (target=%s)", path)
    finally:
        try:
            if os.path.exists(tmp_path):
                os.remove(tmp_path)
                logger.debug("[prompts-write] temp cleanup OK (tmp=%s)", tmp_path)
        except Exception as e:  # pragma: no cover
            logger.warning("[prompts-write] temp cleanup failed (%s): %s", tmp_path, e)


def _load_default_prompts() -> dict:
    """Load default prompts from the codebase."""
    try:
        if DEFAULT_PROMPTS_PATH.exists():
            with open(DEFAULT_PROMPTS_PATH, "r", encoding="utf-8") as f:
                return json.load(f)
        else:
            logger.warning("[prompts] default_prompts.json not found at %s", DEFAULT_PROMPTS_PATH)
            return {"version": "1.0.0", "prompts": []}
    except Exception as e:
        logger.error("[prompts] error loading default prompts: %s", e)
        return {"version": "1.0.0", "prompts": []}


def setup_prompts_library() -> None:
    """Initialize prompts library if it doesn't exist."""
    logger.info("[setup_prompts_library] start (dir=%s, path=%s)", PROMPTS_DIR, PROMPTS_PATH)
    PROMPTS_DIR.mkdir(parents=True, exist_ok=True)
    with _with_lock():
        if not PROMPTS_PATH.exists():
            logger.info("[setup_prompts_library] file missing; loading default prompts")
            default_data = _load_default_prompts()
            _atomic_write_json(PROMPTS_PATH, default_data)
    logger.info("[setup_prompts_library] done")


def read_prompts() -> Tuple[dict, str]:
    """Read the prompts library and return data with etag. user in-memory cache"""
    logger.debug("[read_prompts] in (path=%s)", PROMPTS_PATH)
    with _with_lock():
        global _PROMPTS_CACHE, _PROMPTS_ETAG
        if _PROMPTS_CACHE is not None and _PROMPTS_ETAG is not None:
            return _PROMPTS_CACHE, _PROMPTS_ETAG

        if PROMPTS_PATH.exists():
            try:
                raw = PROMPTS_PATH.read_text(encoding="utf-8") or "{}"
                data = json.loads(raw)
            except Exception:
                logger.error("[read_prompts] invalid JSON; returning defaults (not persisted)")
                data = _load_default_prompts()
        else:
            logger.warning("[read_prompts] file not found; returning defaults (not persisted)")
            data = _load_default_prompts()

        etag = _compute_etag(data)
        _PROMPTS_CACHE, _PROMPTS_ETAG = data, etag
        logger.debug("[read_prompts] etag=%s", etag)
        return data, etag


def write_prompts(new_data: dict, *, expected_etag: Optional[str]) -> str:
    """Write prompts library with optimistic locking."""
    global _PROMPTS_CACHE, _PROMPTS_ETAG
    logger.debug("[write_prompts] in (expected_etag=%s)", expected_etag)
    if not isinstance(new_data, dict):
        logger.error("[write_prompts] payload must be a dict, got %s", type(new_data))
        raise ValueError("prompts data must be an object")

    with _with_lock():
        current, current_etag = read_prompts()
        logger.debug("[write_prompts] current_etag=%s", current_etag)

        if expected_etag and expected_etag != current_etag:
            from werkzeug.exceptions import PreconditionFailed

            logger.warning("[write_prompts] etag mismatch (expected=%s != current=%s)", expected_etag, current_etag)
            raise PreconditionFailed("etag mismatch")

        _atomic_write_json(PROMPTS_PATH, new_data)
        new_etag = _compute_etag(new_data)
        _PROMPTS_CACHE, _PROMPTS_ETAG = new_data, new_etag
        logger.info("[write_prompts] write OK (new_etag=%s)", new_etag)
        return new_etag
