from __future__ import annotations

import ast

from amsdal.models.core.file import File
from amsdal_models.classes.model import Model
from amsdal_server.apps.transactions.serializers.responses import _TransactionListResponse
from amsdal_server.apps.transactions.services.transaction_api import TransactionApi
from amsdal_utils.models.enums import TransactionType

from amsdal_ml.mcp_server.server_model_explorer.services.dependencies import _get_model_registry
from amsdal_ml.mcp_server.server_model_explorer.services.models import TransactionParameterSpec
from amsdal_ml.mcp_server.server_model_explorer.services.models import TransactionSpec
from amsdal_ml.mcp_server.server_model_explorer.services.types import AnnotationMeta
from amsdal_ml.mcp_server.server_model_explorer.services.types import AnnotationTypeName
from amsdal_ml.mcp_server.server_model_explorer.services.types import TransactionParameterKind
from amsdal_ml.mcp_server.server_model_explorer.services.types import TransactionParameterMetaByName
from amsdal_ml.mcp_server.server_model_explorer.services.types import TransactionScope

PRIMITIVE_TYPE_NAMES: set[str] = {
    AnnotationTypeName.STR.value,
    AnnotationTypeName.INT.value,
    AnnotationTypeName.FLOAT.value,
    AnnotationTypeName.BOOL.value,
    AnnotationTypeName.ANY.value,
}
LIST_ORIGINS: set[str] = {'list', 'List'}
DICT_ORIGINS: set[str] = {'dict', 'Dict'}


async def collect_transaction_catalog(*, scope: TransactionScope) -> dict[str, TransactionSpec]:
    """
    Build a typed transaction catalog with parameter annotation metadata.

    Reads transaction definitions from TransactionApi, classifies each parameter
    annotation into supported kinds, then combines this metadata with the
    transactions returned by scope.

    Args:
        scope: Catalog scope filter (all, user, contrib).

    Returns:
        dict[str, TransactionSpec]: Mapping of transaction name to normalized spec.
    """
    model_registry = _get_model_registry()
    model_names = set(model_registry.keys())

    scope_filter: TransactionType | None
    if scope == TransactionScope.ALL:
        scope_filter = None
    elif scope == TransactionScope.USER:
        scope_filter = TransactionType.USER
    else:
        scope_filter = TransactionType.CONTRIB

    metadata_by_name: dict[str, TransactionParameterMetaByName] = {}
    scope_by_name: dict[str, TransactionType] = {}

    for definition, _, transaction_scope in TransactionApi.get_transaction_definitions_with_type():
        scope_by_name[definition.name] = transaction_scope
        parameter_meta: TransactionParameterMetaByName = {}

        for arg in definition.args.args:
            annotation = _annotation_node_to_text(arg.annotation)
            annotation_meta = _classify_annotation(arg.annotation, model_names=model_names)
            parameter_meta[arg.arg] = (annotation, annotation_meta)

        metadata_by_name[definition.name] = parameter_meta

    response: _TransactionListResponse = await TransactionApi.get_transactions(scope=scope_filter)
    catalog: dict[str, TransactionSpec] = {}

    for row in response.rows:
        row_metadata = metadata_by_name.get(row.title, {})
        params: list[TransactionParameterSpec] = []

        for param_name, prop in row.properties.items():
            annotation, annotation_meta = row_metadata.get(
                param_name,
                (
                    prop.type,
                    AnnotationMeta(
                        kind=TransactionParameterKind.UNKNOWN,
                        model_name=None,
                        nullable=False,
                    ),
                ),
            )
            params.append(
                TransactionParameterSpec(
                    name=param_name,
                    annotation=annotation,
                    kind=annotation_meta.kind,
                    model_name=annotation_meta.model_name,
                    nullable=annotation_meta.nullable,
                )
            )

        row_scope = scope_by_name.get(row.title, TransactionType.USER)
        catalog[row.title] = TransactionSpec(
            name=row.title,
            description=row.description,
            scope=row_scope,
            parameters=params,
        )

    return catalog


def is_system_file_model(model_cls: type[Model]) -> bool:
    """Return True when the model class is the system ``File`` model."""
    return model_cls is File


def resolve_transaction_name(
    catalog: dict[str, TransactionSpec],
    transaction_name: str,
) -> str:
    """
    Resolve transaction name by exact or case-insensitive match.

    Raises:
        ValueError: If no matching transaction exists in the catalog.
    """
    if transaction_name in catalog:
        return transaction_name

    target = transaction_name.strip().lower()
    for name in catalog:
        if name.lower() == target:
            return name

    msg = f"Transaction '{transaction_name}' not found"
    raise ValueError(msg)


def _classify_annotation(
    node: ast.expr | None,
    *,
    model_names: set[str],
) -> AnnotationMeta:
    """
    Classify parameter annotation AST into internal transaction parameter kind.

    Supported classes:
    - primitive names: ``str``, ``int``, ``float``, ``bool``, ``Any``
    - model names from the provided registry
    - dict/list origins
    - nullable wrappers via ``Optional[T]`` and ``T | None``

    Args:
        node: Annotation AST node.
        model_names: Known model class names used to detect model parameters.

    Returns:
        AnnotationMeta: Detected kind, model name (if any), and nullable flag.
    """
    if node is None:
        return AnnotationMeta(
            kind=TransactionParameterKind.UNKNOWN,
            model_name=None,
            nullable=False,
        )

    name = _annotation_name(node)
    if name in PRIMITIVE_TYPE_NAMES:
        return AnnotationMeta(
            kind=TransactionParameterKind.PRIMITIVE,
            model_name=None,
            nullable=False,
        )
    if name in model_names:
        return AnnotationMeta(
            kind=TransactionParameterKind.MODEL,
            model_name=name,
            nullable=False,
        )

    optional_inner = _extract_optional_inner(node)
    if optional_inner is not None:
        inner_meta = _classify_annotation(optional_inner, model_names=model_names)
        return AnnotationMeta(
            kind=inner_meta.kind,
            model_name=inner_meta.model_name,
            nullable=True,
        )

    if not isinstance(node, ast.Subscript):
        return AnnotationMeta(
            kind=TransactionParameterKind.UNKNOWN,
            model_name=None,
            nullable=False,
        )

    origin_name = _annotation_name(node.value)
    if origin_name in DICT_ORIGINS:
        return AnnotationMeta(
            kind=TransactionParameterKind.DICT,
            model_name=None,
            nullable=False,
        )

    if origin_name == AnnotationTypeName.OPTIONAL.value:
        inner_meta = _classify_annotation(node.slice, model_names=model_names)
        return AnnotationMeta(
            kind=inner_meta.kind,
            model_name=inner_meta.model_name,
            nullable=True,
        )

    if origin_name in LIST_ORIGINS:
        return AnnotationMeta(
            kind=TransactionParameterKind.PRIMITIVE,
            model_name=None,
            nullable=False,
        )

    return AnnotationMeta(
        kind=TransactionParameterKind.UNKNOWN,
        model_name=None,
        nullable=False,
    )


def _annotation_name(node: ast.expr | None) -> str | None:
    """Extract a simple type name from ``ast.Name`` or ``ast.Attribute`` nodes."""
    if node is None:
        return None
    if isinstance(node, ast.Name):
        return node.id
    if isinstance(node, ast.Attribute):
        return node.attr
    return None


def _annotation_node_to_text(node: ast.expr | None) -> str:
    """
    Convert an annotation AST node into a readable string.

    Returns ``'Any'`` when annotation is missing or cannot be unparsed.
    """
    if node is None:
        return 'Any'

    try:
        return ast.unparse(node)
    except Exception:
        return 'Any'


def _extract_optional_inner(node: ast.expr | None) -> ast.expr | None:
    """
    Extract the non-None side from ``T | None`` annotations.

    Returns:
        ast.expr | None: Inner annotation expression when node is a nullable
        PEP 604 union, otherwise ``None``.
    """
    if not isinstance(node, ast.BinOp) or not isinstance(node.op, ast.BitOr):
        return None

    left_is_none = isinstance(node.left, ast.Constant) and node.left.value is None
    right_is_none = isinstance(node.right, ast.Constant) and node.right.value is None

    if left_is_none and not right_is_none:
        return node.right
    if right_is_none and not left_is_none:
        return node.left
    return None
