"""Utilities for extracting and formatting information about transform fields (parameters)."""

import collections
import inspect
from collections.abc import Callable, Sequence
from types import UnionType
from typing import Any, Literal, NotRequired, Union, get_args, get_origin

import cv2
import numpy as np

from src.transforms.constants import CV2ConstantHandler

# Type aliases
FieldInfo = dict[str, Any]
FieldDefault = int | float | str | bool | None | Callable[[], Any]


class FieldExtractor:
    """Extracts and formats information about transform fields (parameters)."""

    def __init__(self) -> None:
        self.cv2_handler = CV2ConstantHandler()

    def extract_transform_info(self, transform_class: type) -> dict[str, FieldInfo]:
        """Extract field information from a transform class."""
        # Directly access __init__ signature
        try:
            init_signature = inspect.signature(transform_class.__init__)  # type: ignore [misc]
        except ValueError:  # Handle cases where __init__ might not be directly inspectable (e.g., builtins)
            return {}

        init_defaults = self.extract_init_defaults(transform_class)
        fields = {}

        for name, param in init_signature.parameters.items():
            if name in {"self", "p", "strict"}:
                continue

            fields[name] = {
                "type": self._get_field_type(param.annotation, name),
                "default": self.format_default_value(
                    init_defaults.get(name),
                    name,
                ),
            }

        return fields

    def _eval_string_annotation(self, type_str: str) -> Any:  # noqa: ANN401
        """Evaluate a string type annotation safely."""
        try:
            context = {
                "Union": Union,
                "Sequence": Sequence,
                "Callable": Callable,
                "Literal": Literal,
                "Any": Any,
                "int": int,
                "str": str,
                "float": float,
                "bool": bool,
                "None": None,
                "dict": dict,
                "tuple": tuple,
                "np": np,
                "ndarray": np.ndarray,
                "cv2": cv2,
            }
            return eval(type_str, context)  # noqa: S307
        except Exception:
            return type_str  # Return original string if eval fails

    def _handle_literal_type(self, args: tuple[Any, ...], field_name: str) -> list[str]:
        """Handle Literal types, including CV2 constants."""
        if field_name in {"interpolation", "mask_interpolation"}:
            return [f"cv2.{self.cv2_handler.get_constant_name(arg, 'INTER_')}" for arg in args if isinstance(arg, int)]
        if field_name == "border_mode":
            return [f"cv2.{self.cv2_handler.get_constant_name(arg, 'BORDER_')}" for arg in args if isinstance(arg, int)]
        return [str(arg) for arg in args]

    def _handle_sequence_type(self, args: tuple[Any, ...], field_name: str) -> str:
        """Handle Sequence types."""
        inner_type = self._get_field_type(args[0], field_name)
        # Ensure inner_type is a string for formatting
        inner_type_str = str(inner_type) if not isinstance(inner_type, list) else " | ".join(inner_type)
        return f"tuple[{inner_type_str}, ...]"

    def _handle_union_type(self, args: tuple[Any, ...], field_name: str) -> str:
        """Handle Union types."""
        types: list[str] = []
        for arg in args:
            formatted_type = self._get_field_type(arg, field_name)
            if isinstance(formatted_type, list):
                types.extend(formatted_type)
            else:
                types.append(formatted_type)
        return " | ".join(str(t) for t in types)

    def _handle_callable_type(self, args: tuple[Any, ...], field_name: str) -> str:
        """Handle Callable types."""
        if not args or len(args) < 2:
            return "Callable"
        params, return_type = args
        params_str = "Any" if params is ... else " | ".join(str(self._get_field_type(p, field_name)) for p in params)
        return_str = str(self._get_field_type(return_type, field_name))
        return f"(x: {params_str}) => {return_str}"

    def _get_field_type(self, type_: Any, field_name: str) -> str | list[str]:  # noqa: ANN401
        """Get the type string or list for a field annotation."""
        # 1. Evaluate string annotation if necessary
        if isinstance(type_, str):
            type_ = self._eval_string_annotation(type_)
            # If evaluation failed, type_ is still the original string
            if isinstance(type_, str):
                return type_

        # 2. Get origin and args
        origin = get_origin(type_)
        args = get_args(type_)

        # 3. Handle specific origins
        if origin is Literal:
            return self._handle_literal_type(args, field_name)
        if origin is Sequence:
            return self._handle_sequence_type(args, field_name)
        if origin in (Union, UnionType):
            return self._handle_union_type(args, field_name)
        if origin is Callable:
            return self._handle_callable_type(args, field_name)

        # 4. Handle special cases not covered by origin
        if type_ is int and field_name == "border_mode":
            return list(self.cv2_handler.border_names)

        # 5. Fallback to generic formatting
        formatted_type = self._format_type(type_)
        # Ensure correct return type (str or list[str])
        if isinstance(formatted_type, list):
            return formatted_type
        return str(formatted_type)

    def format_default_value(self, value: Any, field_name: str | None = None) -> Any:  # noqa: ANN401
        """Format default value with special handling for CV2 constants and callables."""
        if callable(value) and not isinstance(value, type):
            return f"<function {value.__name__}>"

        if value is None:
            return None

        # Handle CV2 constants
        if isinstance(value, int):
            if field_name == "border_mode" and self.cv2_handler.is_border_mode(value):
                return f"cv2.{self.cv2_handler.get_constant_name(value, 'BORDER_')}"
            if field_name in {"interpolation", "mask_interpolation"} and self.cv2_handler.is_interpolation(value):
                return f"cv2.{self.cv2_handler.get_constant_name(value, 'INTER_')}"

        return value

    def extract_init_defaults(self, transform_class: type) -> dict[str, Any]:
        """Extract default values from __init__ method."""
        # Access __init__ directly, handling potential issues
        try:
            init_signature = inspect.signature(transform_class.__init__)  # type: ignore [misc]
        except ValueError:
            return {}
        return {
            name: param.default
            for name, param in init_signature.parameters.items()
            if param.default is not param.empty and name not in {"self", "p", "strict"}
        }

    def _format_type(self, type_: Any) -> str | list[str]:  # noqa: ANN401
        """Format a type into its string representation."""
        if type_ is None or type_ is type(None):
            return "None"
            # Handle basic types
        if type_ in (int, float, bool, str):
            return type_.__name__

        if type_ is Any:
            return "Any"

        # Handle numpy ndarray specifically
        if type_ is np.ndarray:
            return "np.ndarray"

        # Handle TypedDict
        if hasattr(type_, "__annotations__") and hasattr(type_, "__total__"):
            return self._format_typed_dict(type_)

        # Handle generic types
        origin = get_origin(type_)
        args = get_args(type_)

        # Handle Union types recursively
        if origin in (Union, UnionType):
            return " | ".join(str(self._format_type(arg)) for arg in args)

        if origin in (collections.abc.Callable, Callable):
            return self._format_callable(args)

        if origin is None:
            if isinstance(type_, str):
                return type_
            return type_.__name__ if hasattr(type_, "__name__") else str(type_)

        return self._format_generic(origin, args)

    def _format_non_generic(self, type_: Any) -> str:  # noqa: ANN401
        """Format non-generic types."""
        if isinstance(type_, str):
            return type_
        return type_.__name__ if hasattr(type_, "__name__") else str(type_)

    def _format_callable(self, args: tuple[Any, ...]) -> str:
        """Format Callable type."""
        if not args or len(args) < 2:
            return "Callable"  # Return generic Callable if args are missing
        params, return_type = args

        # params is a tuple containing a list of parameter types or Ellipsis
        if params is ...:
            params_str = "Any"  # Use Any for Ellipsis
        elif isinstance(params, (list, tuple)):
            params_str = " | ".join(str(self._format_type(p)) for p in params)  # Join with |
        else:
            params_str = str(self._format_type(params))

        return_str = str(self._format_type(return_type))

        return f"(x: {params_str}) => {return_str}"  # Revert to original format

    def _format_typed_dict(self, type_: Any) -> str:  # noqa: ANN401
        """Format TypedDict type."""
        value_types = set()
        for annotation in type_.__annotations__.values():
            # Handle NotRequired wrapper
            current_type = annotation
            if get_origin(current_type) is NotRequired:
                current_type = get_args(current_type)[0]
            # Handle ForwardRef
            if hasattr(current_type, "__forward_arg__"):
                type_str = current_type.__forward_arg__
                if type_str.startswith("NotRequired["):
                    type_str = type_str[len("NotRequired[") : -1]
                value_types.add(type_str)
            else:
                formatted_type = self._format_type(current_type)
                if isinstance(formatted_type, list):
                    # Handle list return from _format_type (e.g., Literals)
                    value_types.add(f"Literal[{', '.join(formatted_type)}]")
                else:
                    value_types.add(formatted_type)

        return f"dict[str, {' | '.join(sorted(value_types))}]"

    def _format_generic(self, origin: Any, args: tuple[Any, ...]) -> str | list[str]:  # noqa: ANN401
        """Format generic types."""
        if origin in (list, Sequence):
            return f"tuple[{self._format_type(args[0])}, ...]"  # Revert list/Sequence to tuple[..., ...]

        if origin is dict:
            key_type = self._format_type(args[0])
            value_type = self._format_type(args[1])
            return f"dict[{key_type}, {value_type}]"

        if origin in (Union, UnionType):
            return " | ".join(str(self._format_type(arg)) for arg in args)

        if origin is Literal:
            return [str(arg) for arg in args]  # Return list for Literal

        if origin is tuple:
            if len(args) == 2 and args[1] is ...:
                return f"tuple[{self._format_type(args[0])}, ...]"
            # Revert tuple formatting to use comma separation
            return f"tuple[{', '.join(str(self._format_type(arg)) for arg in args)}]"

        # Fallback for other generic types (might need adjustment)
        origin_name = getattr(origin, "__name__", str(origin))
        args_repr = ", ".join(str(self._format_type(arg)) for arg in args)
        return f"{origin_name}[{args_repr}]"
