"""
Experience Versioning — Phase 23.7

Creates immutable snapshots of Experience state before any mutation.
Enables safe rollback and full audit trail of experience evolution.

Each version captures:
  - Full experience snapshot (JSON)
  - Version number (1-indexed, monotonically increasing)
  - Parent version number
  - Reason for the update
  - Who triggered the update
  - Timestamp

Versioning is append-only — versions are never deleted (only marked inactive).
"""

import json
import logging
import time
import uuid
from dataclasses import dataclass, field
from typing import Dict, List, Optional

from openmemo.experience.models import Experience

logger = logging.getLogger("openmemo.experience.versioning")


@dataclass
class ExperienceVersion:
    """
    Immutable snapshot of an Experience at a point in time.

    Fields:
        version_id      — unique ID (ev_<hex>)
        experience_id   — which experience this version belongs to
        version         — version number (1, 2, 3 …)
        parent_version  — previous version number (0 = initial)
        snapshot        — full experience dict at this version
        reason          — why this version was created
        triggered_by    — "system" | "user" | "evaluator"
        status          — "active" | "superseded" | "rolled_back"
        created_at      — unix timestamp
    """
    version_id: str = field(default_factory=lambda: f"ev_{uuid.uuid4().hex[:12]}")
    experience_id: str = ""
    version: int = 1
    parent_version: int = 0
    snapshot: Dict = field(default_factory=dict)
    reason: str = ""
    triggered_by: str = "system"
    status: str = "active"
    created_at: float = field(default_factory=time.time)

    def to_dict(self) -> dict:
        return {
            "version_id": self.version_id,
            "experience_id": self.experience_id,
            "version": self.version,
            "parent_version": self.parent_version,
            "snapshot": self.snapshot,
            "reason": self.reason,
            "triggered_by": self.triggered_by,
            "status": self.status,
            "created_at": self.created_at,
        }

    @classmethod
    def from_dict(cls, d: dict) -> "ExperienceVersion":
        def _loads(v, default):
            if isinstance(v, str):
                try:
                    return json.loads(v)
                except Exception:
                    return default
            return v if v is not None else default

        return cls(
            version_id=d.get("version_id", f"ev_{uuid.uuid4().hex[:12]}"),
            experience_id=d.get("experience_id", ""),
            version=int(d.get("version", 1)),
            parent_version=int(d.get("parent_version", 0)),
            snapshot=_loads(d.get("snapshot", {}), {}),
            reason=d.get("reason", ""),
            triggered_by=d.get("triggered_by", "system"),
            status=d.get("status", "active"),
            created_at=float(d.get("created_at", time.time())),
        )

    def restore(self) -> Experience:
        """Restore Experience object from this version's snapshot."""
        return Experience.from_dict(self.snapshot)


class ExperienceVersioning:
    """
    Manages version snapshots for Experiences.

    Args:
        store — SQLiteStore instance
    """

    def __init__(self, store):
        self._store = store

    def snapshot(self, experience: Experience,
                 reason: str = "",
                 triggered_by: str = "system") -> ExperienceVersion:
        """
        Create a new version snapshot for an experience.

        Automatically determines the next version number.
        Marks all previous versions of this experience as "superseded".
        """
        existing = self.list_versions(experience.id)
        next_version = len(existing) + 1
        parent_version = existing[0].version if existing else 0

        ev = ExperienceVersion(
            experience_id=experience.id,
            version=next_version,
            parent_version=parent_version,
            snapshot=experience.to_dict(),
            reason=reason or f"version_{next_version}",
            triggered_by=triggered_by,
            status="active",
        )
        self._store.put_experience_version(ev.to_dict())

        if existing:
            for old_v in existing:
                if old_v.status == "active":
                    old_v.status = "superseded"
                    self._store.put_experience_version(old_v.to_dict())

        logger.info(
            "[versioning] exp=%s → v%d (reason=%s)",
            experience.id, next_version, reason
        )
        return ev

    def rollback(self, experience_id: str,
                 target_version: int) -> Optional[Experience]:
        """
        Restore an Experience to a specific version.

        Returns the restored Experience object (caller must save it).
        Marks the target version as "active", current version as "rolled_back".
        """
        target = self._store.get_experience_version(experience_id, target_version)
        if not target:
            logger.warning(
                "[versioning] rollback failed: v%d not found for exp=%s",
                target_version, experience_id
            )
            return None

        target_ev = ExperienceVersion.from_dict(target)
        current_versions = self.list_versions(experience_id)
        for v in current_versions:
            if v.status == "active":
                v.status = "rolled_back"
                self._store.put_experience_version(v.to_dict())

        target_ev.status = "active"
        self._store.put_experience_version(target_ev.to_dict())

        restored = target_ev.restore()
        logger.info(
            "[versioning] exp=%s rolled back to v%d",
            experience_id, target_version
        )
        return restored

    def list_versions(self, experience_id: str,
                      limit: int = 50) -> List[ExperienceVersion]:
        """List all versions for an experience (newest first)."""
        rows = self._store.list_experience_versions(
            experience_id=experience_id, limit=limit
        )
        return [ExperienceVersion.from_dict(r) for r in rows]

    def get_version(self, experience_id: str,
                    version: int) -> Optional[ExperienceVersion]:
        """Get a specific version."""
        row = self._store.get_experience_version(experience_id, version)
        if not row:
            return None
        return ExperienceVersion.from_dict(row)

    def current_version(self, experience_id: str) -> Optional[ExperienceVersion]:
        """Get the currently active version."""
        versions = self.list_versions(experience_id)
        for v in versions:
            if v.status == "active":
                return v
        return versions[0] if versions else None
