"""
soul_loader.py — SOUL.md persona parser and hot-reload watcher.

SOUL.md format
--------------
The file uses YAML front-matter (delimited by ``---`` lines) with an optional
Markdown description body that is ignored by the parser.  Example::

    ---
    name: "Salty"
    humor_level: 7
    sass_level: 4
    base_mood: "playful"
    ...
    ---
    # Optional description text (ignored)

Public API
----------
    load_soul(path)          -> dict  (raises on parse error)
    SoulWatcher(path, cb, interval)
        .start()
        .stop()
        .reload_now()        -> dict

Pure module — no ROS2 imports.
"""

import os
import re
import threading
import time

import yaml

# Keys that are required in every SOUL.md file
_REQUIRED_KEYS = {
    "name",
    "humor_level",
    "sass_level",
    "base_mood",
    "threshold_regular",
    "threshold_favorite",
    "greeting_stranger",
    "greeting_regular",
    "greeting_favorite",
}

_VALID_MOODS = {"happy", "curious", "annoyed", "playful"}


def _extract_frontmatter(text: str) -> str:
    """Return the YAML block between the first pair of ``---`` delimiters.

    Raises ``ValueError`` if the file does not contain valid front-matter.
    """
    lines = text.splitlines()
    delimiters = [i for i, l in enumerate(lines) if l.strip() == "---"]
    if len(delimiters) < 2:
        # No delimiter found — treat the whole file as plain YAML
        return text
    start = delimiters[0] + 1
    end   = delimiters[1]
    return "\n".join(lines[start:end])


def load_soul(path: str) -> dict:
    """Parse a SOUL.md file and return the validated config dict.

    Parameters
    ----------
    path : str
        Absolute path to the SOUL.md file.

    Returns
    -------
    dict
        Validated persona configuration.

    Raises
    ------
    FileNotFoundError
        If the file does not exist.
    ValueError
        If the YAML is malformed or required keys are missing.
    """
    if not os.path.isfile(path):
        raise FileNotFoundError(f"SOUL.md not found: {path}")

    with open(path, "r", encoding="utf-8") as fh:
        raw = fh.read()

    yaml_text = _extract_frontmatter(raw)

    try:
        data = yaml.safe_load(yaml_text)
    except yaml.YAMLError as exc:
        raise ValueError(f"SOUL.md YAML parse error in {path}: {exc}") from exc

    if not isinstance(data, dict):
        raise ValueError(f"SOUL.md top level must be a YAML mapping, got {type(data)}")

    # Validate required keys
    missing = _REQUIRED_KEYS - data.keys()
    if missing:
        raise ValueError(f"SOUL.md missing required keys: {sorted(missing)}")

    # Validate ranges
    for key in ("humor_level", "sass_level"):
        val = data.get(key)
        if not isinstance(val, (int, float)) or not (0 <= val <= 10):
            raise ValueError(f"SOUL.md '{key}' must be a number 0–10, got {val!r}")

    if data.get("base_mood") not in _VALID_MOODS:
        raise ValueError(
            f"SOUL.md 'base_mood' must be one of {sorted(_VALID_MOODS)}, "
            f"got {data.get('base_mood')!r}"
        )

    for key in ("threshold_regular", "threshold_favorite"):
        val = data.get(key)
        if not isinstance(val, int) or val < 0:
            raise ValueError(f"SOUL.md '{key}' must be a non-negative integer, got {val!r}")

    if data["threshold_regular"] > data["threshold_favorite"]:
        raise ValueError(
            "SOUL.md 'threshold_regular' must be <= 'threshold_favorite'"
        )

    return data


class SoulWatcher:
    """Background thread that polls SOUL.md for changes and calls a callback.

    Parameters
    ----------
    path : str
        Path to the SOUL.md file to watch.
    on_reload : callable
        ``on_reload(soul_dict)`` called whenever a valid new SOUL.md is loaded.
    interval : float
        Polling interval in seconds (default 5.0).
    on_error : callable, optional
        ``on_error(exception)`` called when a reload attempt fails.
    """

    def __init__(self, path: str, on_reload, interval: float = 5.0, on_error=None):
        self._path      = path
        self._on_reload = on_reload
        self._interval  = interval
        self._on_error  = on_error
        self._thread    = None
        self._stop_evt  = threading.Event()
        self._last_mtime = 0.0

    # ------------------------------------------------------------------

    def start(self):
        """Start the background polling thread."""
        if self._thread and self._thread.is_alive():
            return
        self._stop_evt.clear()
        self._thread = threading.Thread(target=self._run, daemon=True)
        self._thread.start()

    def stop(self):
        """Signal the watcher thread to stop and block until it exits."""
        self._stop_evt.set()
        if self._thread:
            self._thread.join(timeout=self._interval + 1.0)

    def reload_now(self) -> dict:
        """Force an immediate reload and return the new soul dict."""
        soul = load_soul(self._path)
        self._last_mtime = os.path.getmtime(self._path)
        self._on_reload(soul)
        return soul

    # ------------------------------------------------------------------

    def _run(self):
        while not self._stop_evt.wait(self._interval):
            try:
                mtime = os.path.getmtime(self._path)
            except OSError:
                continue
            if mtime != self._last_mtime:
                try:
                    soul = load_soul(self._path)
                except (FileNotFoundError, ValueError) as exc:
                    if self._on_error:
                        self._on_error(exc)
                    continue
                self._last_mtime = mtime
                self._on_reload(soul)
