from __future__ import annotations

import inspect
import json
import logging
from typing import Any

from amsdal_models.classes.model import Model
from amsdal_utils.models.enums import ModuleType

from amsdal_ml.mcp_server.mcp_auth import authorize_class
from amsdal_ml.mcp_server.mcp_auth import with_amsdal_context
from amsdal_ml.mcp_server.server_model_explorer import mcp_server
from amsdal_ml.mcp_server.server_model_explorer.services.crud_utils import extract_referenced_model_name
from amsdal_ml.mcp_server.server_model_explorer.services.crud_utils import get_action
from amsdal_ml.mcp_server.server_model_explorer.services.crud_utils import get_api_queryset
from amsdal_ml.mcp_server.server_model_explorer.services.crud_utils import get_property_source
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 TransactionSpec
from amsdal_ml.mcp_server.server_model_explorer.services.transaction_utils import collect_transaction_catalog
from amsdal_ml.mcp_server.server_model_explorer.services.types import CrudOperation
from amsdal_ml.mcp_server.server_model_explorer.services.types import FieldValuesResponse
from amsdal_ml.mcp_server.server_model_explorer.services.types import ModelRelationshipsResponse
from amsdal_ml.mcp_server.server_model_explorer.services.types import ModelSchemaView
from amsdal_ml.mcp_server.server_model_explorer.services.types import ReferencedByRelationshipInfo
from amsdal_ml.mcp_server.server_model_explorer.services.types import RelationshipInfo
from amsdal_ml.mcp_server.server_model_explorer.services.types import TransactionCatalogResponse
from amsdal_ml.mcp_server.server_model_explorer.services.types import TransactionParameterView
from amsdal_ml.mcp_server.server_model_explorer.services.types import TransactionScope
from amsdal_ml.mcp_server.server_model_explorer.services.types import TransactionView
from amsdal_ml.utils.query_utils import clean_data

logger = logging.getLogger(__name__)


@mcp_server.tool(
    name='list_available_models',
    description=(
        'Use this FIRST to discover available data models (database tables). '
        'Returns model names, scopes, field lists, and FK relationship summaries. '
        'Optionally filter by scope (core, contrib, user). '
        'Start here, then use get_model_schema or get_model_relationships for details.'
    ),
)
@with_amsdal_context
async def get_list_models(scope: ModuleType | None = None) -> str:
    registry = _get_model_registry()
    data = []
    for name, info in registry.items():
        if scope is not None:
            if info.scope != scope:
                continue
        model = info.model
        relationships: list[str] = []
        for field_name, field_obj in model.model_fields.items():
            ref_model_name = extract_referenced_model_name(field_obj.annotation)
            if ref_model_name is not None:
                relationships.append(f'{field_name} -> {ref_model_name}')
        entry: dict[str, Any] = {
            'model_name': name,
            'scope': info.scope.value,
            'fields': list(model.model_fields.keys()),
            'relationships': relationships,
        }
        data.append(entry)
    return json.dumps(data)


@mcp_server.tool(
    name='list_available_transactions',
    description=(
        'Lists all available AMSDAL transactions, meaning named custom database operations/functions, '
        'not ordinary table records. Use this ONLY to discover executable AMSDAL transactions before '
        'running one via perform_transaction_operation. Do NOT use this for listing model rows or business '
        'records such as orders, payments, or transaction entries; for that use '
        'list_available_models and/or CRUD tools. '
        'Filter by scope: all (default), user (custom user-defined), or contrib (contributed/system-level). '
        'Interpret user intent carefully: if the user says "my transactions", "my custom transactions", '
        'or otherwise refers to their own transactions, prefer scope="user". If the user asks for '
        '"system transactions", "contrib transactions", shared/platform transactions, or anything '
        'not clearly personal, '
        'prefer scope="contrib". Each transaction shows name, description, scope, and parameters with types '
        '(primitive/model/dict). Typical triggers: "what transactions are available?", "list AMSDAL transactions", '
        '"show custom database operations", "what functions can I execute?", "show my transactions", '
        '"show system transactions", "show contrib transactions".'
    ),
)
@with_amsdal_context
async def list_available_transactions(
    scope: TransactionScope = TransactionScope.ALL,
) -> TransactionCatalogResponse | str:
    try:
        catalog = await collect_transaction_catalog(scope=scope)
    except Exception as exc:
        logger.exception('Failed to list transactions: scope=%s', scope.value)
        return f'Error loading transactions: {exc!s}'

    normalized_items = [
        item if isinstance(item, TransactionSpec) else TransactionSpec.model_validate(item) for item in catalog.values()
    ]

    return TransactionCatalogResponse(
        transactions=[
            TransactionView(
                name=item.name,
                description=item.description,
                scope=item.scope,
                parameters=[
                    TransactionParameterView(
                        name=parameter.name,
                        annotation=parameter.annotation,
                        kind=parameter.kind,
                        model_name=parameter.model_name,
                        nullable=parameter.nullable,
                    )
                    for parameter in item.parameters
                ],
            )
            for item in normalized_items
        ]
    )


@mcp_server.tool(
    name='get_model_schema',
    description=(
        'Retrieves the full JSON Schema for a specific model including field types, '
        'required/optional flags, nested structures, and computed @property source code. '
        'Use this after list_available_models to understand exact structure before performing CRUD operations.'
    ),
)
@with_amsdal_context
async def get_model_schema(model_name: str) -> ModelSchemaView | dict[str, str]:
    registry = _get_model_registry()
    model_info = registry.get(model_name)
    if model_info:
        model = model_info.model
        json_schema = model.model_json_schema()
        properties: dict[str, str] = {}
        for name, attr in inspect.getmembers(model):
            if not isinstance(attr, property):
                continue
            property_source = get_property_source(model, name)
            if property_source is None:
                continue
            properties[name] = property_source
        return {'model': model_name, 'schema': json_schema, 'properties': properties}
    return {'error': f'Model {model_name} not found'}


@mcp_server.tool(
    name='get_model_relationships',
    description=(
        'Shows foreign key relationships for a model — both outgoing (this model references another) '
        'and incoming (other models reference this one). '
        'Use this to understand how models connect before performing multi-model operations. '
        'Outgoing relationships are "many_to_one" (FK field on this model). '
        'Incoming relationships are "one_to_many" (other models have FK to this model). '
        'A model with 2+ outgoing FKs is likely a junction table representing a many-to-many relationship.'
    ),
)
@with_amsdal_context
async def get_model_relationships(model_name: str) -> ModelRelationshipsResponse | dict[str, str]:
    registry = _get_model_registry()
    model_info = registry.get(model_name)
    if model_info is None:
        return {'error': f'Model {model_name} not found'}
    model_cls = model_info.model

    relationships: list[RelationshipInfo] = []
    for field_name, field_obj in model_cls.model_fields.items():
        ref_model_name = extract_referenced_model_name(field_obj.annotation)
        if ref_model_name is not None:
            relationships.append(
                {
                    'field': field_name,
                    'references_model': ref_model_name,
                    'type': 'many_to_one',
                    'required': field_obj.is_required(),
                }
            )

    referenced_by: list[ReferencedByRelationshipInfo] = []
    for other_name, other_info in registry.items():
        if other_name == model_name:
            continue
        other_cls = other_info.model
        for field_name, field_obj in other_cls.model_fields.items():
            ref_model_name = extract_referenced_model_name(field_obj.annotation)
            if ref_model_name == model_name:
                referenced_by.append(
                    {
                        'model': other_name,
                        'field': field_name,
                        'type': 'one_to_many',
                    }
                )

    return {
        'model': model_name,
        'relationships': relationships,
        'referenced_by': referenced_by,
    }


@mcp_server.tool(
    name='get_model_field_values_by_ids',
    description=(
        'Returns field values for specific object ids, including computed properties. '
        'Supports history: multiple values can be returned for the same object id across versions. '
        'The first returned value is the latest (current) value for that field and id. '
        'Use this to inspect the actual values after you understand the model schema. '
        'Use this as the primary tool when the user asks to show/verify an object by id (especially before confirming '
        'an update or delete). Pass the object id and a suitable existing field (or a field explicitly mentioned by '
        'the user). Prefer this over perform_crud_operation with operation=read for direct id lookups.'
    ),
)
@with_amsdal_context
async def get_model_field_values_by_ids(
    model_name: str,
    field_name: str,
    object_ids: list[str],
) -> FieldValuesResponse | dict[str, str]:
    registry = _get_model_registry()
    model_info = registry.get(model_name)
    if model_info is None:
        return {'error': f'Model {model_name} not found'}
    model_cls = model_info.model

    try:
        action = get_action(CrudOperation.READ)
        await authorize_class(model_cls, action)
    except Exception as e:
        return {'error': str(e)}

    try:
        queryset = get_api_queryset(model_cls).filter(object_id__in=object_ids)
        results: list[Model] = await queryset.aexecute()
    except Exception as exc:
        logger.exception(
            'Failed to load records: model=%s field=%s object_ids=%s',
            model_name,
            field_name,
            object_ids,
        )
        return {'error': f'Failed to load records: {exc!s}'}

    values: list[dict[str, Any]] = []
    for item in results:
        raw_value = getattr(item, field_name, None)
        if inspect.isawaitable(raw_value):
            raw_value = await raw_value
        object_id = getattr(item, 'object_id', None)
        values.append(
            {
                'object_id': str(object_id) if object_id is not None else '',
                'value': clean_data(raw_value),
            }
        )

    return {
        'model': model_name,
        'field': field_name,
        'values': values,
    }
