"""Version compatibility checks and auto-update notification for dotpromptz.

Handles major-version migration of .prompt files and queries PyPI
for newer package versions (best-effort, never blocks CLI).
"""

import json
import os
import urllib.error
import urllib.request
import warnings
from collections.abc import Callable
from importlib.metadata import PackageNotFoundError, version as installed_version
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:
        """Initialize with prompt and engine version numbers."""
        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

    # --- 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


# ---------------------------------------------------------------------------
# Auto-update check
# ---------------------------------------------------------------------------

_PYPI_PACKAGE_NAME = 'dotpromptz-py'
_PYPI_URL = f'https://pypi.org/pypi/{_PYPI_PACKAGE_NAME}/json'
_TIMEOUT_SECONDS = 3
_DISABLE_UPDATE_CHECK_ENV = 'DOTPROMPTZ_DISABLE_UPDATE_CHECK'


def _parse_version_tuple(version_str: str) -> tuple[int, ...]:
    """Parse a PEP-440 version string into a comparable tuple of ints.

    Only numeric segments are considered (pre-release suffixes are ignored).

    Args:
        version_str: A version string like ``"1.8.0"`` or ``"2.0.0rc1"``.

    Returns:
        A tuple of integers, e.g. ``(1, 8, 0)``.
    """
    parts: list[int] = []
    for segment in version_str.split('.'):
        # Strip non-numeric suffixes (e.g. "0rc1" → "0").
        numeric = ''
        for ch in segment:
            if ch.isdigit():
                numeric += ch
            else:
                break
        if numeric:
            parts.append(int(numeric))
    return tuple(parts)


def check_for_update() -> None:
    """Check PyPI for a newer version and warn if one is available.

    This function is designed to be called at CLI startup.  It is
    fully best-effort: any failure (network, parsing, missing metadata)
    is silently swallowed so it never interferes with normal operation.
    """
    disable_flag = os.getenv(_DISABLE_UPDATE_CHECK_ENV, '').strip().lower()
    if disable_flag in {'1', 'true', 'yes', 'on'}:
        return

    try:
        current = installed_version(_PYPI_PACKAGE_NAME)
    except PackageNotFoundError:
        # Running from source or editable install without metadata.
        return

    try:
        req = urllib.request.Request(_PYPI_URL, headers={'Accept': 'application/json'})
        with urllib.request.urlopen(req, timeout=_TIMEOUT_SECONDS) as resp:
            data = json.loads(resp.read())
        latest: str = data['info']['version']
    except Exception:  # noqa: BLE001  — intentionally broad
        return

    if _parse_version_tuple(latest) > _parse_version_tuple(current):
        logger.warning(
            'A newer version of dotpromptz-py is available.',
            current=current,
            latest=latest,
            upgrade_command='uv tool upgrade dotpromptz-py',
        )
