# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0

"""Prompt version compatibility layer.

Ensures ``.prompt`` files written for older (or newer) major versions of
dotpromptz are handled correctly at runtime:

- **prompt version == engine version** → run as-is.
- **prompt version < engine version** → chain-migrate through registered
  adapters and emit a deprecation warning.
- **prompt version > engine version** → raise ``PromptVersionError``
  (the engine cannot understand a newer format).
- **prompt version missing** → default to ``CURRENT_MAJOR``.
"""

from __future__ import annotations

import warnings
from collections.abc import Callable
from typing import TypeVar

import structlog

from dotpromptz.typing import ParsedPrompt

logger = structlog.get_logger(__name__)

T = TypeVar('T')

# ---------------------------------------------------------------------------
# Engine version constant
# ---------------------------------------------------------------------------

CURRENT_MAJOR: int = 1
"""The current major version of the dotpromptz engine.

This MUST be bumped whenever a backwards-incompatible change is made to the
``.prompt`` file format (frontmatter schema, template semantics, etc.).
"""

# ---------------------------------------------------------------------------
# Errors
# ---------------------------------------------------------------------------


class PromptVersionError(Exception):
    """Raised when a ``.prompt`` file's version is incompatible with the engine."""

    def __init__(self, prompt_version: int, engine_version: int) -> None:
        self.prompt_version = prompt_version
        self.engine_version = engine_version
        super().__init__(
            f'.prompt file requires dotpromptz v{prompt_version}.x, '
            f'but the current engine is v{engine_version}.x. '
            f'Please upgrade dotpromptz to a compatible version.'
        )


# ---------------------------------------------------------------------------
# Adapter registry
# ---------------------------------------------------------------------------

VersionAdapter = Callable[[ParsedPrompt], ParsedPrompt]
"""Signature for a function that migrates a ``ParsedPrompt`` from version *N*
to version *N + 1*.
"""

_ADAPTERS: dict[int, VersionAdapter] = {}
"""Registry mapping source major version → adapter that upgrades to the next."""


def register_adapter(from_version: int) -> Callable[[VersionAdapter], VersionAdapter]:
    """Decorator to register a version adapter.

    Usage::

        @register_adapter(1)
        def _adapt_v1_to_v2(prompt: ParsedPrompt) -> ParsedPrompt:
            # transform v1 frontmatter fields to v2 equivalents
            ...
            return updated_prompt

    Args:
        from_version: The source major version this adapter migrates *from*.
            The target version is implicitly ``from_version + 1``.

    Returns:
        A decorator that registers the adapter function.
    """

    def _decorator(fn: VersionAdapter) -> VersionAdapter:
        _ADAPTERS[from_version] = fn
        return fn

    return _decorator


# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------


def adapt_prompt(prompt: ParsedPrompt) -> ParsedPrompt:
    """Validate and adapt a parsed prompt to the current engine version.

    Args:
        prompt: The parsed prompt (as returned by ``parse_document``).

    Returns:
        The (possibly migrated) ``ParsedPrompt`` compatible with the
        current engine version.

    Raises:
        PromptVersionError: If the prompt's version is higher than the
            engine's current major version (forward-incompatible).
        PromptVersionError: If no migration path exists between the
            prompt's version and the current engine version.
    """
    prompt_version: int = prompt.version if prompt.version is not None else CURRENT_MAJOR

    # --- Forward-incompatible: prompt is newer than engine ---
    if prompt_version > CURRENT_MAJOR:
        raise PromptVersionError(prompt_version, CURRENT_MAJOR)

    # --- Exact match: nothing to do ---
    if prompt_version == CURRENT_MAJOR:
        return prompt

    # --- Backward-compatible: chain-migrate through adapters ---
    for v in range(prompt_version, CURRENT_MAJOR):
        adapter = _ADAPTERS.get(v)
        if adapter is None:
            raise PromptVersionError(
                prompt_version,
                CURRENT_MAJOR,
            )
        warnings.warn(
            f'Migrating .prompt from v{v} to v{v + 1} format. Consider updating the file to version {CURRENT_MAJOR}.',
            DeprecationWarning,
            stacklevel=2,
        )
        logger.info('Migrating .prompt format.', from_version=v, to_version=v + 1)
        prompt = adapter(prompt)

    return prompt
