"""Settings schema migration utilities.

Provides version-aware migration for settings stored as raw JSON dicts.
Settings types opt into versioning by declaring ClassVar on the Settings subclass:

    class MySettings(Settings[MyMenu]):
        _schema_version: ClassVar[int] = 2
        _migrations: ClassVar[dict[int, Callable[[dict[str, Any]], dict[str, Any]]]] = {
            1: migrate_v1_to_v2,
        }

On read, ``migrate_raw_json`` applies sequential migrations and strips the
version key before Pydantic validation.  On write, ``stamp_version`` injects
the version key into the dict produced by ``model_dump()``.
"""

from __future__ import annotations

from collections.abc import Callable, Mapping
from typing import TYPE_CHECKING, Any

if TYPE_CHECKING:
    from bayesline.api._src.registry import Settings

VERSION_KEY = "_schema_version"


def migrate_raw_json(
    settings_type: type[Settings],
    raw_json: Mapping[str, Any],
) -> dict[str, Any]:
    """Apply sequential migrations to raw_json and strip the version key.

    Parameters
    ----------
    settings_type : type[Settings]
        The Settings subclass that defines ``_schema_version`` and ``_migrations``.
    raw_json : Mapping[str, Any]
        The raw JSON dict, possibly from an older schema version.

    Returns
    -------
    dict[str, Any]
        The migrated dict with ``VERSION_KEY`` stripped, ready for ``model_validate()``.

    Raises
    ------
    ValueError
        If the stored version is newer than the code version (cannot downgrade),
        or if a required migration step is missing.
    """
    target_version: int = settings_type._schema_version
    migrations: dict[int, Callable[[dict[str, Any]], dict[str, Any]]] = (
        settings_type._migrations
    )

    # Don't mutate the original
    data = dict(raw_json)
    current_version = data.pop(VERSION_KEY, 1)

    if current_version > target_version:
        raise ValueError(
            f"Settings data version {current_version} is newer than "
            f"the current schema version {target_version} for "
            f"{settings_type.__name__}. Cannot downgrade."
        )

    while current_version < target_version:
        migration_fn = migrations.get(current_version)
        if migration_fn is None:
            raise ValueError(
                f"No migration found from version {current_version} to "
                f"{current_version + 1} for {settings_type.__name__}."
            )
        data = migration_fn(data)
        current_version += 1

    return data


def stamp_version(
    settings_type: type,
    raw_json: dict[str, Any],
) -> dict[str, Any]:
    """Stamp the current schema version into raw_json for persistence.

    Only stamps for version > 1 so that existing v1 settings are untouched.

    Parameters
    ----------
    settings_type : type
        The Settings subclass.
    raw_json : dict[str, Any]
        The raw JSON dict from ``model_dump()``.

    Returns
    -------
    dict[str, Any]
        The dict with ``VERSION_KEY`` added (if version > 1).
    """
    version: int = getattr(settings_type, "_schema_version", 1)
    if version <= 1:
        return raw_json
    return {VERSION_KEY: version, **raw_json}


def validate_migration_chain(settings_type: type) -> None:
    """Validate that a settings type has a complete chain of migrations.

    Should be called in tests for every versioned settings type.

    Parameters
    ----------
    settings_type : type
        The Settings subclass to validate.

    Raises
    ------
    ValueError
        If any migration step is missing in the chain from 1 to ``_schema_version``.
    """
    target_version: int = getattr(settings_type, "_schema_version", 1)
    migrations: dict[int, Callable[[dict[str, Any]], dict[str, Any]]] = getattr(
        settings_type, "_migrations", {}
    )

    if target_version <= 1:
        return

    for v in range(1, target_version):
        if v not in migrations:
            raise ValueError(
                f"{settings_type.__name__}: Missing migration from "
                f"version {v} to {v + 1}. Current schema version is {target_version}."
            )
