# Copyright (c) 2025 Patrick Motsch
# All rights reserved.
"""
Shared utilities for model attributes and labels.
"""

from pydantic import BaseModel, Field, ConfigDict
from pydantic_core import PydanticUndefined
from typing import Dict, Any, List, Type, Optional, Union
import inspect
import importlib
import os
import logging

logger = logging.getLogger(__name__)


# Define the AttributeDefinition class here instead of importing it
class AttributeDefinition(BaseModel):
    """Definition of a model attribute with its metadata."""

    name: str
    type: str
    label: str
    description: Optional[str] = None
    required: bool = False
    default: Any = None
    options: Optional[Union[str, List[Any]]] = None  # Can be a string reference (e.g., "user.role") or a list of options
    validation: Optional[Dict[str, Any]] = None
    ui: Optional[Dict[str, Any]] = None
    # New frontend metadata fields
    readonly: bool = False
    editable: bool = True
    visible: bool = True
    order: int = 0
    placeholder: Optional[str] = None


# Global registry for model labels
MODEL_LABELS: Dict[str, Dict[str, Dict[str, str]]] = {}


def registerModelLabels(modelName: str, modelLabel: Dict[str, str], labels: Dict[str, Dict[str, str]]):
    """
    Register labels for a model's attributes and the model itself.

    Args:
        modelName: Name of the model class
        modelLabel: Dictionary mapping language codes to model labels
                    e.g. {"en": "Prompt", "fr": "Invite"}
        labels: Dictionary mapping attribute names to their translations
                e.g. {"name": {"en": "Name", "fr": "Nom"}}
    """
    MODEL_LABELS[modelName] = {"model": modelLabel, "attributes": labels}


def getModelLabels(modelName: str, language: str = "en") -> Dict[str, str]:
    """
    Get labels for a model's attributes in the specified language.

    Args:
        modelName: Name of the model class
        language: Language code (default: "en")

    Returns:
        Dictionary mapping attribute names to their labels in the specified language
    """
    modelData = MODEL_LABELS.get(modelName, {})
    attributeLabels = modelData.get("attributes", {})

    return {
        attr: translations.get(language, translations.get("en", attr))
        for attr, translations in attributeLabels.items()
    }


def getModelLabel(modelName: str, language: str = "en") -> str:
    """
    Get the label for a model in the specified language.

    Args:
        modelName: Name of the model class
        language: Language code (default: "en")

    Returns:
        Model label in the specified language, or model name if no label exists
    """
    modelData = MODEL_LABELS.get(modelName, {})
    modelLabel = modelData.get("model", {})
    return modelLabel.get(language, modelLabel.get("en", modelName))


def getModelAttributeDefinitions(modelClass: Type[BaseModel] = None, userLanguage: str = "en") -> Dict[str, Any]:
    """
    Get attribute definitions for a model class.

    Args:
        modelClass: The model class to get attributes for
        userLanguage: Language code for translations (default: "en")

    Returns:
        Dictionary containing model label and attribute definitions
    """
    if not modelClass:
        return {}

    attributes = []
    model_name = modelClass.__name__
    labels = getModelLabels(model_name, userLanguage)
    model_label = getModelLabel(model_name, userLanguage)

    # Pydantic v2 only
    fields = modelClass.model_fields
    for name, field in fields.items():
        # Extract frontend metadata from field info
        # In Pydantic v2, the field from model_fields.items() IS the FieldInfo object
        # FieldInfo objects have the 'extra' dict directly on them
        field_info = field
        # Check both direct attributes and extra field for frontend metadata
        frontend_type = None
        frontend_readonly = False
        frontend_required = field.is_required()
        frontend_options = None
        frontend_visible = True  # Default visible
        frontend_fk_source = None  # FK dropdown source (e.g., "/api/users/")
        frontend_fk_display_field = None  # Which field of the FK target to display (e.g., "username", "name")

        if field_info:
            # Try direct attributes first (though these won't exist for custom kwargs)
            frontend_type = getattr(field_info, "frontend_type", None)
            frontend_readonly = getattr(field_info, "frontend_readonly", False)
            frontend_required = getattr(
                field_info, "frontend_required", frontend_required
            )
            frontend_options = getattr(field_info, "frontend_options", None)

            # If not found, check json_schema_extra (Pydantic v2 stores custom kwargs here)
            if frontend_type is None and hasattr(field_info, "json_schema_extra"):
                json_extra = field_info.json_schema_extra
                if isinstance(json_extra, dict):
                    frontend_type = json_extra.get("frontend_type")
                elif callable(json_extra):
                    # If it's a callable, we can't easily extract from it, skip
                    pass

            # Check extra field (Pydantic v2 stores custom kwargs here)
            # This is the main location where frontend_type is stored
            if hasattr(field_info, "extra"):
                extra_dict = field_info.extra
                if isinstance(extra_dict, dict):
                    if frontend_type is None:
                        frontend_type = extra_dict.get("frontend_type")
            
            # Also extract other fields from extra if it exists
            if hasattr(field_info, "extra") and isinstance(field_info.extra, dict):
                extra_dict = field_info.extra
                if not frontend_readonly:
                    frontend_readonly = extra_dict.get("frontend_readonly", False)
                if frontend_required == field.is_required():
                    frontend_required = extra_dict.get("frontend_required", frontend_required)
                if frontend_options is None:
                    frontend_options = extra_dict.get("frontend_options")
            
            # Also check json_schema_extra for other fields
            if hasattr(field_info, "json_schema_extra"):
                json_extra = field_info.json_schema_extra
                if isinstance(json_extra, dict):
                    if not frontend_readonly and "frontend_readonly" in json_extra:
                        frontend_readonly = json_extra.get("frontend_readonly", False)
                    if frontend_required == field.is_required() and "frontend_required" in json_extra:
                        frontend_required = json_extra.get("frontend_required", frontend_required)
                    if frontend_options is None and "frontend_options" in json_extra:
                        frontend_options = json_extra.get("frontend_options")
                    # Extract frontend_visible (default True, can be set to False to hide field)
                    if "frontend_visible" in json_extra:
                        frontend_visible = json_extra.get("frontend_visible", True)
                    # Extract frontend_fk_source for FK dropdown references
                    if "frontend_fk_source" in json_extra:
                        frontend_fk_source = json_extra.get("frontend_fk_source")
                    # Extract frontend_fk_display_field - which field of FK target to display
                    if "frontend_fk_display_field" in json_extra:
                        frontend_fk_display_field = json_extra.get("frontend_fk_display_field")

        # Use frontend type if available, otherwise detect from Python type
        if frontend_type:
            field_type = frontend_type
        else:
            # Check if it's TextMultilingual type
            annotation_str = str(field.annotation)
            # Check both the module path and class name for TextMultilingual
            if ('TextMultilingual' in annotation_str or 
                (hasattr(field.annotation, '__name__') and field.annotation.__name__ == 'TextMultilingual') or
                'datamodelUtils.TextMultilingual' in annotation_str or
                'datamodels.datamodelUtils.TextMultilingual' in annotation_str):
                field_type = 'multilingual'
            elif hasattr(field.annotation, "__name__"):
                annotation_name = field.annotation.__name__
                # Check if it's a Dict type (for JSON/object fields)
                if annotation_name == 'Dict' or annotation_str.startswith('typing.Dict') or annotation_str.startswith('Dict['):
                    field_type = 'object'  # Will be rendered as textarea for JSON editing
                else:
                    field_type = annotation_name
            else:
                field_type = str(field.annotation)
        
        # Extract default value from field
        # In Pydantic v2, FieldInfo has a 'default' attribute
        field_default = None
        if hasattr(field_info, 'default'):
            default_value = field_info.default
            # Handle default_factory (callable that generates default)
            if hasattr(field_info, 'default_factory') and callable(field_info.default_factory):
                # Don't call it here - it's meant to be called per-instance
                # Instead, store a marker that indicates it exists
                field_default = None  # Frontend should use first option or specific logic
            elif default_value is not ... and default_value is not PydanticUndefined:
                # Ellipsis or PydanticUndefined means no default
                # Convert enum to its value if it's an enum
                if hasattr(default_value, 'value'):
                    field_default = default_value.value
                else:
                    field_default = default_value

        # Safely get description
        description = ""
        try:
            if hasattr(field_info, "description") and field_info.description:
                description = str(field_info.description)
        except Exception:
            pass

        # Hide "id" fields by default unless explicitly set to visible
        # Also hide fields ending with "Id" that are FK references (unless they have fkSource)
        if name == "id":
            frontend_visible = False  # Never show primary key in forms/tables
        
        attr_def = {
            "name": name,
            "type": field_type,
            "required": frontend_required,
            "description": description,
            "label": labels.get(name, name),
            "placeholder": f"Please enter {labels.get(name, name)}",
            "editable": not frontend_readonly,
            "visible": frontend_visible,
            "order": len(attributes),
            "readonly": frontend_readonly,
            "options": frontend_options,
            "default": field_default,
        }
        
        # Add FK source for dropdown rendering if specified
        if frontend_fk_source:
            attr_def["fkSource"] = frontend_fk_source
            # Also add display field if specified (which field of FK target to show)
            if frontend_fk_display_field:
                attr_def["fkDisplayField"] = frontend_fk_display_field
        
        attributes.append(attr_def)

    return {"model": model_label, "attributes": attributes}


def getModelClasses() -> Dict[str, Type[BaseModel]]:
    """
    Dynamically get all model classes from all model modules.

    Returns:
        Dict[str, Type[BaseModel]]: Dictionary of model class names to their classes
    """
    modelClasses = {}

    # Get the interfaces directory path
    interfaces_dir = os.path.join(
        os.path.dirname(os.path.dirname(__file__)), "interfaces"
    )

    # Find all model files in interfaces directory
    for fileName in os.listdir(interfaces_dir):
        if fileName.endswith("Model.py"):
            # Convert fileName to module name (e.g., gatewayModel.py -> gatewayModel)
            module_name = fileName[:-3]

            # Import the module dynamically
            module = importlib.import_module(f"modules.interfaces.{module_name}")

            # Get all classes from the module
            for name, obj in inspect.getmembers(module):
                if (
                    inspect.isclass(obj)
                    and issubclass(obj, BaseModel)
                    and obj != BaseModel
                ):
                    modelClasses[name] = obj

    # Also get models from datamodels directory
    datamodels_dir = os.path.join(
        os.path.dirname(os.path.dirname(__file__)), "datamodels"
    )

    # Find all model files in datamodels directory
    for fileName in os.listdir(datamodels_dir):
        if fileName.startswith("datamodel") and fileName.endswith(".py"):
            # Convert fileName to module name (e.g., datamodelUtils.py -> datamodelUtils)
            module_name = fileName[:-3]
            try:
                # Import the module dynamically
                module = importlib.import_module(f"modules.datamodels.{module_name}")
                # Get all classes from the module
                for name, obj in inspect.getmembers(module):
                    if (
                        inspect.isclass(obj)
                        and issubclass(obj, BaseModel)
                        and obj != BaseModel
                    ):
                        modelClasses[name] = obj
            except Exception as e:
                logger.warning(f"Error importing module {module_name}: {str(e)}", exc_info=True)
                # Continue with other modules even if one fails

    return modelClasses


class AttributeResponse(BaseModel):
    """Response model for entity attributes"""

    attributes: List[AttributeDefinition]

    model_config = ConfigDict(
        json_schema_extra={
            "example": {
                "attributes": [
                    {
                        "name": "username",
                        "label": "Username",
                        "type": "string",
                        "required": True,
                        "placeholder": "Please enter username",
                        "editable": True,
                        "visible": True,
                        "order": 0,
                    }
                ]
            }
        }
    )
