"""MCP tool handlers for Odoo operations.

This module implements MCP tools for performing operations on Odoo data.
Tools are different from resources - they can have side effects and perform
actions like creating, updating, or deleting records.
"""

from datetime import datetime
from typing import Any, Dict, List, Optional, Union

from mcp.server.fastmcp import FastMCP

from .access_control import AccessControlError, AccessController
from .config import OdooConfig
from .error_handling import (
    NotFoundError,
    ValidationError,
)
from .error_sanitizer import ErrorSanitizer
from .logging_config import get_logger, perf_logger
from .odoo_connection import OdooConnection, OdooConnectionError

logger = get_logger(__name__)

# Legacy error type alias for backward compatibility
ToolError = ValidationError


class OdooToolHandler:
    """Handles MCP tool requests for Odoo operations."""

    def __init__(
        self,
        app: FastMCP,
        connection: OdooConnection,
        access_controller: AccessController,
        config: OdooConfig,
    ):
        """Initialize tool handler.

        Args:
            app: FastMCP application instance
            connection: Odoo connection instance
            access_controller: Access control instance
            config: Odoo configuration instance
        """
        self.app = app
        self.connection = connection
        self.access_controller = access_controller
        self.config = config

        # Register tools
        self._register_tools()

    def _format_datetime(self, value: str) -> str:
        """Format datetime values to ISO 8601 with timezone."""
        if not value or not isinstance(value, str):
            return value

        # Handle Odoo's compact datetime format (YYYYMMDDTHH:MM:SS)
        if len(value) == 17 and "T" in value and "-" not in value:
            try:
                dt = datetime.strptime(value, "%Y%m%dT%H:%M:%S")
                return dt.strftime("%Y-%m-%dT%H:%M:%S+00:00")
            except ValueError:
                pass

        # Handle standard Odoo datetime format (YYYY-MM-DD HH:MM:SS)
        if " " in value and len(value) == 19:
            try:
                dt = datetime.strptime(value, "%Y-%m-%d %H:%M:%S")
                return dt.strftime("%Y-%m-%dT%H:%M:%S+00:00")
            except ValueError:
                pass

        return value

    def _process_record_dates(self, record: Dict[str, Any], model: str) -> Dict[str, Any]:
        """Process datetime fields in a record to ensure proper formatting."""
        # Common datetime field names in Odoo
        known_datetime_fields = {
            "create_date",
            "write_date",
            "date",
            "datetime",
            "date_start",
            "date_end",
            "date_from",
            "date_to",
            "date_order",
            "date_invoice",
            "date_due",
            "last_update",
            "last_activity",
            "activity_date_deadline",
        }

        # First try to get field metadata
        fields_info = None
        try:
            fields_info = self.connection.fields_get(model)
        except Exception:
            # Field metadata unavailable, will use fallback detection
            pass

        # Process each field in the record
        for field_name, field_value in record.items():
            if not isinstance(field_value, str):
                continue

            should_format = False

            # Check if field is identified as datetime from metadata
            if fields_info and isinstance(fields_info, dict) and field_name in fields_info:
                field_type = fields_info[field_name].get("type")
                if field_type == "datetime":
                    should_format = True

            # Check if field name suggests it's a datetime field
            if not should_format and field_name in known_datetime_fields:
                should_format = True

            # Check if field name ends with common datetime suffixes
            if not should_format and any(
                field_name.endswith(suffix) for suffix in ["_date", "_datetime", "_time"]
            ):
                should_format = True

            # Pattern-based detection for datetime-like strings
            if not should_format and (
                (
                    len(field_value) == 17 and "T" in field_value and "-" not in field_value
                )  # 20250607T21:55:52
                or (
                    len(field_value) == 19 and " " in field_value and field_value.count("-") == 2
                )  # 2025-06-07 21:55:52
            ):
                should_format = True

            # Apply formatting if needed
            if should_format:
                formatted = self._format_datetime(field_value)
                if formatted != field_value:
                    record[field_name] = formatted

        return record

    def _should_include_field_by_default(self, field_name: str, field_info: Dict[str, Any]) -> bool:
        """Determine if a field should be included in default response.

        Args:
            field_name: Name of the field
            field_info: Field metadata from fields_get()

        Returns:
            True if field should be included in default response
        """
        # Always include essential fields
        always_include = {"id", "name", "display_name", "active"}
        if field_name in always_include:
            return True

        # Exclude system/technical fields by prefix
        exclude_prefixes = ("_", "message_", "activity_", "website_message_")
        if field_name.startswith(exclude_prefixes):
            return False

        # Exclude specific technical fields
        exclude_fields = {
            "write_date",
            "create_date",
            "write_uid",
            "create_uid",
            "__last_update",
            "access_token",
            "access_warning",
            "access_url",
        }
        if field_name in exclude_fields:
            return False

        # Get field type
        field_type = field_info.get("type", "")

        # Exclude binary and large fields
        if field_type in ("binary", "image", "html"):
            return False

        # Exclude expensive computed fields (non-stored)
        if field_info.get("compute") and not field_info.get("store", True):
            return False

        # Exclude one2many and many2many fields (can be large)
        if field_type in ("one2many", "many2many"):
            return False

        # Include required fields
        if field_info.get("required"):
            return True

        # Include simple stored fields that are searchable
        if field_info.get("store", True) and field_info.get("searchable", True):
            if field_type in (
                "char",
                "text",
                "boolean",
                "integer",
                "float",
                "date",
                "datetime",
                "selection",
                "many2one",
            ):
                return True

        return False

    def _get_smart_default_fields(self, model: str) -> Optional[List[str]]:
        """Get smart default fields for a model.

        Args:
            model: The Odoo model name

        Returns:
            List of field names to include by default, or None if unable to determine
        """
        try:
            # Get all field definitions
            fields_info = self.connection.fields_get(model)

            # Apply smart filtering
            default_fields = [
                field_name
                for field_name, field_info in fields_info.items()
                if self._should_include_field_by_default(field_name, field_info)
            ]

            # Ensure we have at least some fields
            if not default_fields:
                default_fields = ["id", "name", "display_name"]

            # Sort fields for consistent output
            # Priority order: id, name, display_name, then alphabetical
            priority_fields = ["id", "name", "display_name", "active"]
            other_fields = sorted(f for f in default_fields if f not in priority_fields)

            final_fields = [f for f in priority_fields if f in default_fields] + other_fields

            logger.debug(
                f"Smart default fields for {model}: {len(final_fields)} of {len(fields_info)} fields"
            )
            return final_fields

        except Exception as e:
            logger.warning(f"Could not determine default fields for {model}: {e}")
            # Return None to indicate we should get all fields
            return None

    def _register_tools(self):
        """Register all tool handlers with FastMCP."""

        @self.app.tool()
        async def search_records(
            model: str,
            domain: Optional[List[Union[str, List[Any]]]] = None,
            fields: Optional[List[str]] = None,
            limit: int = 10,
            offset: int = 0,
            order: Optional[str] = None,
        ) -> Dict[str, Any]:
            """Search for records in an Odoo model.

            Args:
                model: The Odoo model name (e.g., 'res.partner')
                domain: Odoo domain filter (e.g., [['is_company', '=', True]])
                fields: Field selection options:
                    - None (default): Returns smart selection of common fields
                    - ["field1", "field2", ...]: Returns only specified fields
                    - ["__all__"]: Returns ALL fields (warning: may cause serialization errors)
                limit: Maximum number of records to return
                offset: Number of records to skip
                order: Sort order (e.g., 'name asc')

            Returns:
                Dictionary with 'records' list and 'total' count
            """
            return await self._handle_search_tool(model, domain, fields, limit, offset, order)

        @self.app.tool()
        async def get_record(
            model: str,
            record_id: int,
            fields: Optional[List[str]] = None,
        ) -> Dict[str, Any]:
            """Get a specific record by ID with smart field selection.

            This tool supports selective field retrieval to optimize performance and response size.
            By default, returns a smart selection of commonly-used fields based on the model's field metadata.

            Args:
                model: The Odoo model name (e.g., 'res.partner')
                record_id: The record ID
                fields: Field selection options:
                    - None (default): Returns smart selection of common fields
                    - ["field1", "field2", ...]: Returns only specified fields
                    - ["__all__"]: Returns ALL fields (warning: can be very large)

            Workflow for field discovery:
            1. To see all available fields for a model, use the resource:
               read("odoo://res.partner/fields")
            2. Then request specific fields:
               get_record("res.partner", 1, fields=["name", "email", "phone"])

            Examples:
                # Get smart defaults (recommended)
                get_record("res.partner", 1)

                # Get specific fields only
                get_record("res.partner", 1, fields=["name", "email", "phone"])

                # Get ALL fields (use with caution)
                get_record("res.partner", 1, fields=["__all__"])

            Returns:
                Dictionary with record data containing requested fields.
                When using smart defaults, includes _metadata with field statistics.
            """
            return await self._handle_get_record_tool(model, record_id, fields)

        @self.app.tool()
        async def list_models() -> Dict[str, List[Dict[str, Any]]]:
            """List all models enabled for MCP access with their allowed operations.

            Returns:
                Dictionary containing a list of model information dictionaries.
                Each model includes:
                - model: Technical name (e.g., 'res.partner')
                - name: Display name (e.g., 'Contact')
                - operations: Dict of allowed operations (read, write, create, unlink)

            Example response:
                {
                    "models": [
                        {
                            "model": "res.partner",
                            "name": "Contact",
                            "operations": {
                                "read": true,
                                "write": true,
                                "create": true,
                                "unlink": false
                            }
                        }
                    ]
                }
            """
            return await self._handle_list_models_tool()

        @self.app.tool()
        async def create_record(
            model: str,
            values: Dict[str, Any],
        ) -> Dict[str, Any]:
            """Create a new record in an Odoo model.

            Args:
                model: The Odoo model name (e.g., 'res.partner')
                values: Field values for the new record

            Returns:
                Dictionary with created record details
            """
            return await self._handle_create_record_tool(model, values)

        @self.app.tool()
        async def update_record(
            model: str,
            record_id: int,
            values: Dict[str, Any],
        ) -> Dict[str, Any]:
            """Update an existing record.

            Args:
                model: The Odoo model name (e.g., 'res.partner')
                record_id: The record ID to update
                values: Field values to update

            Returns:
                Dictionary with updated record details
            """
            return await self._handle_update_record_tool(model, record_id, values)

        @self.app.tool()
        async def delete_record(
            model: str,
            record_id: int,
        ) -> Dict[str, Any]:
            """Delete a record.

            Args:
                model: The Odoo model name (e.g., 'res.partner')
                record_id: The record ID to delete

            Returns:
                Dictionary with deletion confirmation
            """
            return await self._handle_delete_record_tool(model, record_id)

    async def _handle_search_tool(
        self,
        model: str,
        domain: Optional[List[Union[str, List[Any]]]],
        fields: Optional[List[str]],
        limit: int,
        offset: int,
        order: Optional[str],
    ) -> Dict[str, Any]:
        """Handle search tool request."""
        try:
            with perf_logger.track_operation("tool_search", model=model):
                # Check model access
                self.access_controller.validate_model_access(model, "read")

                # Ensure we're connected
                if not self.connection.is_authenticated:
                    raise ValidationError("Not authenticated with Odoo")

                # Set defaults
                if domain is None:
                    domain = []
                if limit <= 0 or limit > self.config.max_limit:
                    limit = self.config.default_limit

                # Get total count
                total_count = self.connection.search_count(model, domain)

                # Search for records
                record_ids = self.connection.search(
                    model, domain, limit=limit, offset=offset, order=order
                )

                # Determine which fields to fetch
                fields_to_fetch = fields
                if fields is None:
                    # Use smart field selection to avoid serialization issues
                    fields_to_fetch = self._get_smart_default_fields(model)
                    logger.debug(
                        f"Using smart defaults for {model} search: {len(fields_to_fetch) if fields_to_fetch else 'all'} fields"
                    )
                elif fields == ["__all__"]:
                    # Explicit request for all fields
                    fields_to_fetch = None  # Odoo interprets None as all fields
                    logger.debug(f"Fetching all fields for {model} search")

                # Read records
                records = []
                if record_ids:
                    records = self.connection.read(model, record_ids, fields_to_fetch)
                    # Process datetime fields in each record
                    records = [self._process_record_dates(record, model) for record in records]

                return {
                    "records": records,
                    "total": total_count,
                    "limit": limit,
                    "offset": offset,
                    "model": model,
                }

        except AccessControlError as e:
            raise ToolError(f"Access denied: {e}") from e
        except OdooConnectionError as e:
            raise ToolError(f"Connection error: {e}") from e
        except Exception as e:
            logger.error(f"Error in search_records tool: {e}")
            sanitized_msg = ErrorSanitizer.sanitize_message(str(e))
            raise ToolError(f"Search failed: {sanitized_msg}") from e

    async def _handle_get_record_tool(
        self,
        model: str,
        record_id: int,
        fields: Optional[List[str]],
    ) -> Dict[str, Any]:
        """Handle get record tool request."""
        try:
            with perf_logger.track_operation("tool_get_record", model=model):
                # Check model access
                self.access_controller.validate_model_access(model, "read")

                # Ensure we're connected
                if not self.connection.is_authenticated:
                    raise ValidationError("Not authenticated with Odoo")

                # Determine which fields to fetch
                fields_to_fetch = fields
                use_smart_defaults = False
                total_fields = None

                if fields is None:
                    # Use smart field selection
                    fields_to_fetch = self._get_smart_default_fields(model)
                    use_smart_defaults = True
                    logger.debug(
                        f"Using smart defaults for {model}: {len(fields_to_fetch) if fields_to_fetch else 'all'} fields"
                    )
                elif fields == ["__all__"]:
                    # Explicit request for all fields
                    fields_to_fetch = None  # Odoo interprets None as all fields
                    logger.debug(f"Fetching all fields for {model}")
                else:
                    # Specific fields requested
                    logger.debug(f"Fetching specific fields for {model}: {fields}")

                # Read the record
                records = self.connection.read(model, [record_id], fields_to_fetch)

                if not records:
                    raise ToolError(f"Record not found: {model} with ID {record_id}")

                # Process datetime fields in the record
                record = self._process_record_dates(records[0], model)

                # Add metadata when using smart defaults
                if use_smart_defaults:
                    try:
                        # Get total field count for metadata
                        all_fields_info = self.connection.fields_get(model)
                        total_fields = len(all_fields_info)
                    except Exception:
                        pass  # Don't fail if we can't get field count

                    record["_metadata"] = {
                        "fields_returned": (
                            len(record) - 1 if "_metadata" in record else len(record)
                        ),
                        "field_selection_method": "smart_defaults",
                        "note": "Limited fields returned for performance. Use fields=['__all__'] for all fields or see odoo://{}/fields for available fields.".format(
                            model
                        ),
                    }
                    if total_fields:
                        record["_metadata"]["total_fields_available"] = total_fields

                return record

        except ToolError:
            # Re-raise ToolError without modification to preserve specific error messages
            raise
        except NotFoundError as e:
            raise ToolError(str(e)) from e
        except AccessControlError as e:
            raise ToolError(f"Access denied: {e}") from e
        except OdooConnectionError as e:
            raise ToolError(f"Connection error: {e}") from e
        except Exception as e:
            logger.error(f"Error in get_record tool: {e}")
            sanitized_msg = ErrorSanitizer.sanitize_message(str(e))
            raise ToolError(f"Failed to get record: {sanitized_msg}") from e

    async def _handle_list_models_tool(self) -> Dict[str, List[Dict[str, Any]]]:
        """Handle list models tool request with permissions."""
        try:
            with perf_logger.track_operation("tool_list_models"):
                # Get basic model list
                models = self.access_controller.get_enabled_models()

                # Enrich with permissions for each model
                enriched_models = []
                for model_info in models:
                    model_name = model_info["model"]
                    try:
                        # Get permissions for this model
                        permissions = self.access_controller.get_model_permissions(model_name)
                        enriched_model = {
                            "model": model_name,
                            "name": model_info["name"],
                            "operations": {
                                "read": permissions.can_read,
                                "write": permissions.can_write,
                                "create": permissions.can_create,
                                "unlink": permissions.can_unlink,
                            },
                        }
                        enriched_models.append(enriched_model)
                    except Exception as e:
                        # If we can't get permissions for a model, include it with all operations false
                        logger.warning(f"Failed to get permissions for {model_name}: {e}")
                        enriched_model = {
                            "model": model_name,
                            "name": model_info["name"],
                            "operations": {
                                "read": False,
                                "write": False,
                                "create": False,
                                "unlink": False,
                            },
                        }
                        enriched_models.append(enriched_model)

                # Return proper JSON structure with enriched models array
                return {"models": enriched_models}
        except ToolError:
            # Re-raise ToolError without modification to preserve specific error messages
            raise
        except Exception as e:
            logger.error(f"Error in list_models tool: {e}")
            sanitized_msg = ErrorSanitizer.sanitize_message(str(e))
            raise ToolError(f"Failed to list models: {sanitized_msg}") from e

    async def _handle_create_record_tool(
        self,
        model: str,
        values: Dict[str, Any],
    ) -> Dict[str, Any]:
        """Handle create record tool request."""
        try:
            with perf_logger.track_operation("tool_create_record", model=model):
                # Check model access
                self.access_controller.validate_model_access(model, "create")

                # Ensure we're connected
                if not self.connection.is_authenticated:
                    raise ValidationError("Not authenticated with Odoo")

                # Validate required fields
                if not values:
                    raise ValidationError("No values provided for record creation")

                # Create the record
                record_id = self.connection.create(model, values)

                # Read the created record to return full details
                records = self.connection.read(model, [record_id])
                if not records:
                    raise ToolError(f"Failed to read created record: {model} with ID {record_id}")

                return {
                    "success": True,
                    "record": records[0],
                    "message": f"Successfully created {model} record with ID {record_id}",
                }

        except ToolError:
            # Re-raise ToolError without modification to preserve specific error messages
            raise
        except AccessControlError as e:
            raise ToolError(f"Access denied: {e}") from e
        except OdooConnectionError as e:
            raise ToolError(f"Connection error: {e}") from e
        except Exception as e:
            logger.error(f"Error in create_record tool: {e}")
            sanitized_msg = ErrorSanitizer.sanitize_message(str(e))
            raise ToolError(f"Failed to create record: {sanitized_msg}") from e

    async def _handle_update_record_tool(
        self,
        model: str,
        record_id: int,
        values: Dict[str, Any],
    ) -> Dict[str, Any]:
        """Handle update record tool request."""
        try:
            with perf_logger.track_operation("tool_update_record", model=model):
                # Check model access
                self.access_controller.validate_model_access(model, "write")

                # Ensure we're connected
                if not self.connection.is_authenticated:
                    raise ValidationError("Not authenticated with Odoo")

                # Validate input
                if not values:
                    raise ValidationError("No values provided for record update")

                # Check if record exists
                existing = self.connection.read(model, [record_id])
                if not existing:
                    raise NotFoundError(f"Record not found: {model} with ID {record_id}")

                # Update the record
                success = self.connection.write(model, [record_id], values)

                # Read the updated record to return full details
                records = self.connection.read(model, [record_id])
                if not records:
                    raise ToolError(f"Failed to read updated record: {model} with ID {record_id}")

                return {
                    "success": success,
                    "record": records[0],
                    "message": f"Successfully updated {model} record with ID {record_id}",
                }

        except ToolError:
            # Re-raise ToolError without modification to preserve specific error messages
            raise
        except NotFoundError as e:
            raise ToolError(str(e)) from e
        except AccessControlError as e:
            raise ToolError(f"Access denied: {e}") from e
        except OdooConnectionError as e:
            raise ToolError(f"Connection error: {e}") from e
        except Exception as e:
            logger.error(f"Error in update_record tool: {e}")
            sanitized_msg = ErrorSanitizer.sanitize_message(str(e))
            raise ToolError(f"Failed to update record: {sanitized_msg}") from e

    async def _handle_delete_record_tool(
        self,
        model: str,
        record_id: int,
    ) -> Dict[str, Any]:
        """Handle delete record tool request."""
        try:
            with perf_logger.track_operation("tool_delete_record", model=model):
                # Check model access
                self.access_controller.validate_model_access(model, "unlink")

                # Ensure we're connected
                if not self.connection.is_authenticated:
                    raise ValidationError("Not authenticated with Odoo")

                # Check if record exists
                existing = self.connection.read(model, [record_id])
                if not existing:
                    raise NotFoundError(f"Record not found: {model} with ID {record_id}")

                # Store some info about the record before deletion
                record_name = existing[0].get(
                    "name", existing[0].get("display_name", f"ID {record_id}")
                )

                # Delete the record
                success = self.connection.unlink(model, [record_id])

                return {
                    "success": success,
                    "deleted_id": record_id,
                    "deleted_name": record_name,
                    "message": f"Successfully deleted {model} record '{record_name}' (ID: {record_id})",
                }

        except ToolError:
            # Re-raise ToolError without modification to preserve specific error messages
            raise
        except NotFoundError as e:
            raise ToolError(str(e)) from e
        except AccessControlError as e:
            raise ToolError(f"Access denied: {e}") from e
        except OdooConnectionError as e:
            raise ToolError(f"Connection error: {e}") from e
        except Exception as e:
            logger.error(f"Error in delete_record tool: {e}")
            sanitized_msg = ErrorSanitizer.sanitize_message(str(e))
            raise ToolError(f"Failed to delete record: {sanitized_msg}") from e


def register_tools(
    app: FastMCP,
    connection: OdooConnection,
    access_controller: AccessController,
    config: OdooConfig,
) -> OdooToolHandler:
    """Register all Odoo tools with the FastMCP app.

    Args:
        app: FastMCP application instance
        connection: Odoo connection instance
        access_controller: Access control instance
        config: Odoo configuration instance

    Returns:
        The tool handler instance
    """
    handler = OdooToolHandler(app, connection, access_controller, config)
    logger.info("Registered Odoo MCP tools")
    return handler
