"""Cross-cutting types and models for Cognite Functions.

This module contains types, protocols, and models that are shared across
multiple components of the framework. It focuses on cross-cutting concerns
rather than module-specific implementations.

Core Framework Types:
    - JSONLike, DataDict, TypedParam, TypedResponse: Universal data types
    - Handler: Protocol definition for traditional function handlers
    - RouteHandler: Type alias for flexible route handler functions
    - HTTPMethod: Shared enumeration for HTTP operations

Cognite Integration:
    - FunctionCallInfo: Cognite-specific metadata for function calls
    - CogniteTypedError, CogniteTypedResponse: Standardized response formats
    - RequestData: Parsed request data structure

Design Philosophy:
    This module serves as the "shared vocabulary" of the framework, containing
    types that need to be consistent across routing, application logic, and
    response handling. Module-specific types (like RouteInfo) belong in their
    respective modules to maintain clear boundaries and reduce coupling.
"""

from collections.abc import Callable, Mapping, Sequence
from enum import Enum
from typing import TYPE_CHECKING, Any, Protocol, TypeAlias, TypedDict
from urllib.parse import parse_qs, urlparse

from cognite.client import CogniteClient
from pydantic import BaseModel, Field
from typing_extensions import TypeAliasType


class FunctionCallInfo(TypedDict):
    """Function call information."""

    function_id: str
    call_id: str

    # If the call is scheduled
    schedule_id: str | None
    scheduled_time: str | None


if TYPE_CHECKING:
    # Recursive types for type checking (pyright). These work with pyright, but not for Pydantic (RecursionError)
    Json: TypeAlias = Mapping[str, "Json"] | Sequence["Json"] | str | int | float | bool | None
    TypedParam: TypeAlias = (
        CogniteClient
        | BaseModel
        | Sequence["TypedParam"]
        | Mapping[str, "TypedParam"]
        | FunctionCallInfo
        | str
        | int
        | float
        | bool
        | None
    )
    TypedResponse: TypeAlias = (
        BaseModel | Sequence["TypedResponse"] | Mapping[str, "TypedResponse"] | str | int | float | bool | None
    )

else:
    # Recursive types for runtime (Pydantic)
    # These work for Pydantic, but not with pyright. Note the use of `Mapping` and
    # `Sequence` to make them covariant. For more information, see
    # https://docs.pydantic.dev/2.11/concepts/types/#named-recursive-types
    Json = TypeAliasType(
        "Json",
        "Mapping[str, Json] | Sequence[Json] | str | int | float | bool | None",
    )
    TypedParam = TypeAliasType(
        "TypedParam",
        "CogniteClient | BaseModel | Sequence[TypedParam] | Mapping[str, TypedParam] | FunctionCallInfo | str | int | float | bool | None",  # noqa: E501
    )
    TypedResponse = TypeAliasType(
        "TypedResponse",
        "BaseModel | Sequence[TypedResponse] | Mapping[str, TypedResponse] | str | int | float | bool | None",
    )


# Type aliases for better readability
DataDict: TypeAlias = Mapping[str, Json]

# Route handler type - flexible function signature with dependency injection
# Handlers can declare any subset of (client, secrets, function_call_info)
# and the framework will inject only the ones declared via signature introspection
RouteHandler = Callable[..., TypedResponse]


class HTTPMethod(str, Enum):
    """HTTP methods."""

    GET = "GET"
    POST = "POST"
    PUT = "PUT"
    DELETE = "DELETE"

    def __str__(self) -> str:
        """Return the string value of the HTTP method."""
        return self.value


class Handler(Protocol):
    """Handler function type.

    This is the traditional function handler type used in Cognite Functions.
    """

    def __call__(
        self,
        *,
        client: CogniteClient,
        data: DataDict,
        secrets: Mapping[str, Any] | None = None,
        function_call_info: FunctionCallInfo | None = None,
    ) -> Json:
        """Call the handler."""
        ...


class CogniteTypedError(BaseModel):
    """Structured error response."""

    model_config = {
        "json_schema_extra": {
            "examples": [
                {
                    "success": False,
                    "error_type": "ValidationError",
                    "message": "Invalid input data",
                    "details": {"field": "value"},
                }
            ]
        }
    }

    success: bool = False
    error_type: str
    message: str
    details: Mapping[str, Any] | None = None


class CogniteTypedResponse(BaseModel):
    """Wrapper for successful responses."""

    model_config = {"json_schema_extra": {"examples": [{"success": True, "data": {"result": "example response"}}]}}

    success: bool = True
    data: TypedResponse


class RequestData(BaseModel):
    """Parsed request data from a Cognite Function call."""

    path: str = "/"
    """Raw path with query string (e.g., "/items/123?include_tax=true")"""
    method: HTTPMethod = HTTPMethod.POST
    """HTTP method (GET, POST, etc.)"""
    body: Mapping[str, Any] = Field(default_factory=dict)
    """Request body data"""

    # Computed fields that are parsed from path
    clean_path: str = ""
    """Just the path part (e.g., "/items/123")"""
    query: Mapping[str, str | Sequence[str]] = Field(default_factory=dict)
    """Parsed query parameters"""

    def model_post_init(self, __context: Any) -> None:
        """Parse path and query string after Pydantic validation."""
        # Parse the full path to extract clean path and query params
        parsed = urlparse(self.path)

        # Set the clean path (without query string)
        self.clean_path = parsed.path or "/"

        # Parse query parameters
        query_params = parse_qs(parsed.query)
        query: dict[str, str | list[str]] = {}
        for key, value_list in query_params.items():
            if len(value_list) == 1:
                query[key] = value_list[0]
            else:
                query[key] = value_list  # Keep as list if multiple values

        self.query = query
