"""
Phase 95: Data Migration Engine for Platform Upgrades.

Register versioned migrations with up/down callables, track current version,
apply pending migrations forward, rollback to a target version, and log
every applied migration for auditability.

Storage:
  state/broker/migrations/registry.json  -- registered migrations
  state/broker/migrations/state.json     -- current version + status
  state/broker/migrations/history.jsonl  -- applied migration log
"""

import json
import traceback
from datetime import datetime, timezone
from pathlib import Path
from typing import Callable, Optional

from brynq.server import BROKER_DIR, _write_json_sync, _read_jsonl_sync, _append_jsonl_sync

# ── Paths ──────────────────────────────────────────────────────────────────

MIG_DIR = BROKER_DIR / "migrations"
MIG_DIR.mkdir(parents=True, exist_ok=True)

REGISTRY_FILE = MIG_DIR / "registry.json"
STATE_FILE = MIG_DIR / "state.json"
HISTORY_FILE = MIG_DIR / "history.jsonl"

# ── In-memory migration registry (callables cannot be serialised) ──────────

_migrations: dict[str, dict] = {}  # version -> {version, description, up_fn, down_fn}

# ── Persistence Helpers ────────────────────────────────────────────────────


def _load_state() -> dict:
    try:
        if STATE_FILE.exists():
            return json.loads(STATE_FILE.read_text("utf-8"))
    except (json.JSONDecodeError, OSError):
        pass
    return {"current_version": None, "applied": []}


def _save_state(data: dict) -> None:
    _write_json_sync(STATE_FILE, data)


def _load_registry_meta() -> dict:
    """Load registry metadata (descriptions, order) from disk."""
    try:
        if REGISTRY_FILE.exists():
            return json.loads(REGISTRY_FILE.read_text("utf-8"))
    except (json.JSONDecodeError, OSError):
        pass
    return {}


def _save_registry_meta(data: dict) -> None:
    _write_json_sync(REGISTRY_FILE, data)


# ── Public API ─────────────────────────────────────────────────────────────


def register_migration(version: str, description: str,
                       up_fn: Callable, down_fn: Optional[Callable] = None) -> dict:
    """Register a migration. *up_fn* and *down_fn* are zero-arg callables."""
    if version in _migrations:
        return {"error": f"Migration '{version}' already registered"}

    _migrations[version] = {
        "version": version,
        "description": description,
        "up_fn": up_fn,
        "down_fn": down_fn,
    }

    # Persist metadata (no callables)
    meta = _load_registry_meta()
    meta[version] = {"version": version, "description": description,
                     "has_down": down_fn is not None}
    _save_registry_meta(meta)

    return {"ok": True, "version": version, "description": description}


def get_current_version() -> Optional[str]:
    """Return the current migration version, or None if no migrations applied."""
    state = _load_state()
    return state.get("current_version")


def get_pending_migrations() -> list[dict]:
    """Return migrations registered but not yet applied, in version order."""
    state = _load_state()
    applied = set(state.get("applied", []))
    pending = []
    for version in sorted(_migrations.keys()):
        if version not in applied:
            m = _migrations[version]
            pending.append({
                "version": m["version"],
                "description": m["description"],
            })
    return pending


def migrate_up(target_version: Optional[str] = None) -> dict:
    """Apply pending migrations up to *target_version* (inclusive).

    If target_version is None, apply all pending migrations.
    Returns a summary of applied migrations.
    """
    state = _load_state()
    applied_set = set(state.get("applied", []))
    applied_list = list(state.get("applied", []))

    results = []
    for version in sorted(_migrations.keys()):
        if version in applied_set:
            continue
        if target_version and version > target_version:
            break

        m = _migrations[version]
        started = datetime.now(timezone.utc).isoformat()
        try:
            m["up_fn"]()
            status = "success"
            error_msg = None
        except Exception as exc:
            status = "failed"
            error_msg = str(exc)

        entry = {
            "version": version,
            "direction": "up",
            "status": status,
            "error": error_msg,
            "started_at": started,
            "completed_at": datetime.now(timezone.utc).isoformat(),
        }
        _append_jsonl_sync(HISTORY_FILE, entry)
        results.append(entry)

        if status == "failed":
            break

        applied_set.add(version)
        applied_list.append(version)
        state["current_version"] = version
        state["applied"] = applied_list
        _save_state(state)

    return {"ok": True, "applied": results, "current_version": state.get("current_version")}


def migrate_down(target_version: str) -> dict:
    """Rollback migrations down to *target_version* (exclusive).

    Applies down_fn in reverse order for each migration above target_version.
    """
    state = _load_state()
    applied_list = list(state.get("applied", []))

    if target_version not in applied_list and target_version is not None:
        # Allow rolling back to a version not in applied if it's below all applied
        pass

    # Find migrations to roll back (those above target_version, in reverse)
    to_rollback = [v for v in reversed(applied_list) if v > target_version]

    results = []
    for version in to_rollback:
        m = _migrations.get(version)
        if not m:
            results.append({"version": version, "status": "skipped", "reason": "not registered"})
            applied_list.remove(version)
            continue

        if not m.get("down_fn"):
            results.append({"version": version, "status": "failed",
                            "error": "No down migration defined"})
            break

        started = datetime.now(timezone.utc).isoformat()
        try:
            m["down_fn"]()
            status = "success"
            error_msg = None
        except Exception as exc:
            status = "failed"
            error_msg = str(exc)

        entry = {
            "version": version,
            "direction": "down",
            "status": status,
            "error": error_msg,
            "started_at": started,
            "completed_at": datetime.now(timezone.utc).isoformat(),
        }
        _append_jsonl_sync(HISTORY_FILE, entry)
        results.append(entry)

        if status == "failed":
            break

        applied_list.remove(version)

    state["applied"] = applied_list
    state["current_version"] = applied_list[-1] if applied_list else None
    _save_state(state)

    return {"ok": True, "rolled_back": results, "current_version": state.get("current_version")}


def get_migration_history() -> list[dict]:
    """Return the full log of applied/rolled-back migrations."""
    return _read_jsonl_sync(HISTORY_FILE)


def get_migration_status() -> dict:
    """Current version, pending count, last applied, and total applied."""
    state = _load_state()
    pending = get_pending_migrations()
    history = _read_jsonl_sync(HISTORY_FILE)
    last_applied = history[-1] if history else None
    return {
        "current_version": state.get("current_version"),
        "applied_count": len(state.get("applied", [])),
        "pending_count": len(pending),
        "last_applied": last_applied,
        "total_registered": len(_migrations),
    }


# ── Reset (for testing) ───────────────────────────────────────────────────


def _reset() -> None:
    """Clear all runtime state. For tests only."""
    _migrations.clear()
