"""
relationship_db.py — SQLite-backed per-person relationship memory.

No ROS2 imports — safe to unit-test without a live ROS2 environment.

Schema
------
    people (
        person_id       TEXT PRIMARY KEY,
        score           REAL DEFAULT 0.0,
        interaction_count INTEGER DEFAULT 0,
        first_seen      TEXT,        -- ISO-8601 UTC timestamp
        last_seen       TEXT,        -- ISO-8601 UTC timestamp
        prefs           TEXT         -- JSON blob for learned preferences
    )

    interactions (
        id              INTEGER PRIMARY KEY AUTOINCREMENT,
        person_id       TEXT,
        ts              TEXT,        -- ISO-8601 UTC timestamp
        event_type      TEXT,        -- greeting | positive | negative | detection
        details         TEXT         -- free-form JSON blob
    )

Public API
----------
    RelationshipDB(db_path)
        .get_person(person_id)          -> dict
        .record_interaction(person_id, event_type, details, delta_score)
        .set_pref(person_id, key, value)
        .get_pref(person_id, key, default)
        .get_recent_events(person_id, window_s)  -> list[dict]
        .all_people()                   -> list[dict]
        .close()
"""

from __future__ import annotations

import json
import os
import sqlite3
import threading
from datetime import datetime, timezone


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


class RelationshipDB:
    """Thread-safe SQLite relationship store.

    Parameters
    ----------
    db_path : str
        Path to the SQLite file.  Created (with parent dirs) if absent.
    """

    def __init__(self, db_path: str):
        parent = os.path.dirname(db_path)
        if parent:
            os.makedirs(parent, exist_ok=True)
        self._path = db_path
        self._lock = threading.Lock()
        self._conn = sqlite3.connect(db_path, check_same_thread=False)
        self._conn.row_factory = sqlite3.Row
        self._migrate()

    # ── Schema ────────────────────────────────────────────────────────────────

    def _migrate(self):
        with self._conn:
            self._conn.executescript("""
                CREATE TABLE IF NOT EXISTS people (
                    person_id         TEXT PRIMARY KEY,
                    score             REAL    DEFAULT 0.0,
                    interaction_count INTEGER DEFAULT 0,
                    first_seen        TEXT,
                    last_seen         TEXT,
                    prefs             TEXT    DEFAULT '{}'
                );

                CREATE TABLE IF NOT EXISTS interactions (
                    id          INTEGER PRIMARY KEY AUTOINCREMENT,
                    person_id   TEXT    NOT NULL,
                    ts          TEXT    NOT NULL,
                    event_type  TEXT    NOT NULL,
                    details     TEXT    DEFAULT '{}'
                );

                CREATE INDEX IF NOT EXISTS idx_interactions_person_ts
                    ON interactions (person_id, ts);
            """)

    # ── People ────────────────────────────────────────────────────────────────

    def get_person(self, person_id: str) -> dict:
        """Return the person record; inserts a default row if not found.

        Returns
        -------
        dict with keys: person_id, score, interaction_count,
                        first_seen, last_seen, prefs (dict)
        """
        with self._lock:
            row = self._conn.execute(
                "SELECT * FROM people WHERE person_id = ?", (person_id,)
            ).fetchone()

            if row is None:
                now = _now_iso()
                self._conn.execute(
                    "INSERT INTO people (person_id, first_seen, last_seen) VALUES (?,?,?)",
                    (person_id, now, now),
                )
                self._conn.commit()
                return {
                    "person_id":         person_id,
                    "score":             0.0,
                    "interaction_count": 0,
                    "first_seen":        now,
                    "last_seen":         now,
                    "prefs":             {},
                }

            prefs = {}
            try:
                prefs = json.loads(row["prefs"] or "{}")
            except json.JSONDecodeError:
                pass

            return {
                "person_id":         row["person_id"],
                "score":             float(row["score"]),
                "interaction_count": int(row["interaction_count"]),
                "first_seen":        row["first_seen"],
                "last_seen":         row["last_seen"],
                "prefs":             prefs,
            }

    def all_people(self) -> list:
        """Return all person records as a list of dicts."""
        with self._lock:
            rows = self._conn.execute("SELECT * FROM people ORDER BY score DESC").fetchall()
        result = []
        for row in rows:
            prefs = {}
            try:
                prefs = json.loads(row["prefs"] or "{}")
            except json.JSONDecodeError:
                pass
            result.append({
                "person_id":         row["person_id"],
                "score":             float(row["score"]),
                "interaction_count": int(row["interaction_count"]),
                "first_seen":        row["first_seen"],
                "last_seen":         row["last_seen"],
                "prefs":             prefs,
            })
        return result

    # ── Interactions ──────────────────────────────────────────────────────────

    def record_interaction(
        self,
        person_id: str,
        event_type: str,
        details: dict | None = None,
        delta_score: float = 0.0,
    ):
        """Record an interaction event and update the person's score.

        Parameters
        ----------
        person_id : str
        event_type : str
            One of: ``"greeting"``, ``"positive"``, ``"negative"``,
            ``"detection"``.
        details : dict, optional
            Arbitrary key/value data stored as JSON.
        delta_score : float
            Amount to add to the person's score (can be negative).
            Interaction count is always incremented by 1.
        """
        now = _now_iso()
        details_json = json.dumps(details or {})

        with self._lock:
            # Ensure person exists
            self.get_person.__wrapped__(self, person_id) if hasattr(
                self.get_person, "__wrapped__"
            ) else None

            # Upsert person row
            self._conn.execute("""
                INSERT INTO people (person_id, first_seen, last_seen)
                VALUES (?, ?, ?)
                ON CONFLICT(person_id) DO UPDATE SET
                    last_seen         = excluded.last_seen
            """, (person_id, now, now))

            # Increment count + score
            self._conn.execute("""
                UPDATE people
                SET interaction_count = interaction_count + 1,
                    score             = score + ?,
                    last_seen         = ?
                WHERE person_id = ?
            """, (delta_score, now, person_id))

            # Insert interaction log row
            self._conn.execute("""
                INSERT INTO interactions (person_id, ts, event_type, details)
                VALUES (?, ?, ?, ?)
            """, (person_id, now, event_type, details_json))

            self._conn.commit()

    def get_recent_events(self, person_id: str, window_s: float = 120.0) -> list:
        """Return interaction events for *person_id* within the last *window_s* seconds.

        Returns
        -------
        list of dict
            Each dict: ``{"type": str, "dt": float, "ts": str, "details": dict}``
            where ``dt`` is seconds ago (positive = in the past).
        """
        from datetime import timedelta

        cutoff = (
            datetime.now(timezone.utc) - timedelta(seconds=window_s)
        ).isoformat()

        with self._lock:
            rows = self._conn.execute("""
                SELECT ts, event_type, details FROM interactions
                WHERE person_id = ? AND ts >= ?
                ORDER BY ts DESC
            """, (person_id, cutoff)).fetchall()

        now_dt = datetime.now(timezone.utc)
        result = []
        for row in rows:
            try:
                row_dt = datetime.fromisoformat(row["ts"])
                # Make timezone-aware if needed
                if row_dt.tzinfo is None:
                    row_dt = row_dt.replace(tzinfo=timezone.utc)
                dt_secs = (now_dt - row_dt).total_seconds()
            except (ValueError, TypeError):
                dt_secs = window_s

            details = {}
            try:
                details = json.loads(row["details"] or "{}")
            except json.JSONDecodeError:
                pass

            result.append({
                "type":    row["event_type"],
                "dt":      dt_secs,
                "ts":      row["ts"],
                "details": details,
            })
        return result

    # ── Preferences ───────────────────────────────────────────────────────────

    def set_pref(self, person_id: str, key: str, value):
        """Set a learned preference for a person.

        Parameters
        ----------
        person_id, key : str
        value : JSON-serialisable
        """
        person = self.get_person(person_id)
        prefs  = person["prefs"]
        prefs[key] = value

        with self._lock:
            self._conn.execute(
                "UPDATE people SET prefs = ? WHERE person_id = ?",
                (json.dumps(prefs), person_id),
            )
            self._conn.commit()

    def get_pref(self, person_id: str, key: str, default=None):
        """Return a specific learned preference for a person."""
        return self.get_person(person_id)["prefs"].get(key, default)

    # ── Lifecycle ─────────────────────────────────────────────────────────────

    def close(self):
        """Close the database connection."""
        with self._lock:
            self._conn.close()
