import uuid
from collections import defaultdict
from datetime import datetime, timedelta, timezone
from threading import RLock

from hiveos.intelligence.contracts import MEMORY_SCOPES, validate_memory_entry


def _now_utc():
    return datetime.now(timezone.utc)


def _parse_iso8601(value):
    if value is None:
        return None
    return datetime.fromisoformat(str(value).replace("Z", "+00:00"))


class MemoryStore:
    """
    In-memory scoped memory with TTL and inspectability metadata.
    Store is session-local by ownership in Core.
    """

    def __init__(self, session_id=None):
        self.session_id = session_id
        self._lock = RLock()
        self._entries = {}
        self._ids_by_scope = defaultdict(set)
        self._stats = {"puts": 0, "gets": 0, "queries": 0, "purged": 0}

    def put(self, entry):
        candidate = self._normalize_entry(entry)
        validate_memory_entry(candidate)

        with self._lock:
            self._entries[candidate["entry_id"]] = candidate
            self._ids_by_scope[candidate["scope"]].add(candidate["entry_id"])
            self._stats["puts"] += 1
            return dict(candidate)

    def get(self, scope, key, scope_ref=None, include_expired=False):
        if scope not in MEMORY_SCOPES:
            raise ValueError(f"Invalid scope '{scope}'")
        with self._lock:
            if not include_expired:
                self._purge_expired_locked(_now_utc())
            self._stats["gets"] += 1
            wanted_ref = scope_ref or {}
            for entry_id in list(self._ids_by_scope.get(scope, set())):
                entry = self._entries.get(entry_id)
                if not entry:
                    continue
                if entry.get("key") != key:
                    continue
                if self._scope_ref_matches(entry.get("scope_ref", {}), wanted_ref):
                    if include_expired or not self._is_expired(entry):
                        return dict(entry)
            return None

    def query(self, scope=None, filters=None, include_expired=False):
        with self._lock:
            if not include_expired:
                self._purge_expired_locked(_now_utc())
            self._stats["queries"] += 1

            ref_filter = (filters or {}).get("scope_ref", {})
            tag_filter = set((filters or {}).get("tags", []))
            key_filter = (filters or {}).get("key")

            scopes = [scope] if scope else list(MEMORY_SCOPES)
            results = []
            for selected_scope in scopes:
                if selected_scope not in MEMORY_SCOPES:
                    raise ValueError(f"Invalid scope '{selected_scope}'")
                for entry_id in list(self._ids_by_scope.get(selected_scope, set())):
                    entry = self._entries.get(entry_id)
                    if not entry:
                        continue
                    if not include_expired and self._is_expired(entry):
                        continue
                    if key_filter and entry.get("key") != key_filter:
                        continue
                    if ref_filter and not self._scope_ref_matches(entry.get("scope_ref", {}), ref_filter):
                        continue
                    if tag_filter and not tag_filter.issubset(set(entry.get("tags", []))):
                        continue
                    results.append(dict(entry))
            return results

    def purge_expired(self, now=None):
        with self._lock:
            return self._purge_expired_locked(now or _now_utc())

    def stats(self):
        with self._lock:
            by_scope = {scope: len(self._ids_by_scope.get(scope, set())) for scope in MEMORY_SCOPES}
            return {
                "entries": len(self._entries),
                "by_scope": by_scope,
                **self._stats,
            }

    def _normalize_entry(self, entry):
        if not isinstance(entry, dict):
            raise ValueError("Memory entry must be an object")

        scope = entry.get("scope")
        scope_ref = dict(entry.get("scope_ref") or {})
        created_at = entry.get("created_at")
        expires_at = entry.get("expires_at")
        ttl_seconds = int(entry.get("ttl_seconds", 0) or 0)

        if self.session_id:
            existing = scope_ref.get("session_id")
            if existing and existing != self.session_id:
                raise ValueError("scope_ref.session_id mismatch with active session")
            scope_ref["session_id"] = self.session_id

        created_dt = _parse_iso8601(created_at) if created_at else _now_utc()
        if expires_at:
            expires_dt = _parse_iso8601(expires_at)
        elif ttl_seconds > 0:
            expires_dt = created_dt + timedelta(seconds=ttl_seconds)
        else:
            expires_dt = None

        return {
            "entry_id": str(entry.get("entry_id") or f"mem-{uuid.uuid4().hex[:10]}"),
            "scope": scope,
            "scope_ref": scope_ref,
            "key": str(entry.get("key")),
            "value": entry.get("value"),
            "content_type": str(entry.get("content_type", "json")),
            "created_at": created_dt.isoformat().replace("+00:00", "Z"),
            "expires_at": expires_dt.isoformat().replace("+00:00", "Z") if expires_dt else None,
            "ttl_seconds": ttl_seconds,
            "tags": list(entry.get("tags", [])),
            "source": str(entry.get("source", "system")),
            "inspect": dict(entry.get("inspect") or {"visible": True, "redaction": "none", "notes": ""}),
        }

    def _scope_ref_matches(self, have_ref, want_ref):
        for key, expected in (want_ref or {}).items():
            if expected is None:
                continue
            if have_ref.get(key) != expected:
                return False
        return True

    def _is_expired(self, entry):
        expires_at = _parse_iso8601(entry.get("expires_at"))
        return bool(expires_at and expires_at <= _now_utc())

    def _purge_expired_locked(self, now):
        purged = 0
        for entry_id, entry in list(self._entries.items()):
            expires_at = _parse_iso8601(entry.get("expires_at"))
            if expires_at and expires_at <= now:
                scope = entry.get("scope")
                self._entries.pop(entry_id, None)
                self._ids_by_scope.get(scope, set()).discard(entry_id)
                purged += 1
        self._stats["purged"] += purged
        return purged
