"""Base response wrapper models for standardized API envelopes.

All API endpoints should wrap their responses using the base classes defined here.
This ensures a consistent response shape across the entire API, reducing frontend
branching logic and integration bugs.

Response Patterns:

1. **Paginated List** — Use `PaginatedListResponse[T]` for endpoints returning paginated items.
   Example: GET /api/jails, GET /api/dashboard/bans

   ```python
   class MyListResponse(PaginatedListResponse[MyItem]):
       pass

   # Returns:
   {
     "items": [...],
     "pagination": {
       "page": 1,
       "page_size": 20,
       "total": 100,
       "total_pages": 5,
       "has_next_page": true,
       "has_prev_page": false
     }
   }
   ```

2. **Simple Collection** — Use `CollectionResponse[T]` for non-paginated collections.
   Example: GET /api/bans/active

   ```python
   class MyCollectionResponse(CollectionResponse[MyItem]):
       pass

   # Returns:
   {
     "items": [...],
     "total": 50
   }
   ```

3. **Single Item Detail** — Use domain model directly wrapped in a named field.
   Example: GET /api/jails/{name}, GET /api/dashboard/status

   ```python
   class MyDetailResponse(BaseModel):
       jail: Jail  # or: status: ServerStatus, settings: ServerSettings
       # Optional extra fields (ignore_list, warnings, etc.)

   # Returns:
   {
     "jail": {...},
     "ignore_list": [...]
   }
   ```

4. **Command/Action Result** — Use `CommandResponse` for success/acknowledgement.
   Example: POST /api/jails/{name}/start, POST /api/bans

   ```python
   class MyCommandResponse(CommandResponse):
       jail: str  # Optional: target identifier

   # Returns:
   {
     "message": "Jail 'sshd' started.",
     "success": true,
     "jail": "sshd"
   }
   ```

5. **Aggregated Data** — Use domain-specific aggregation models with metadata.
   Example: GET /api/dashboard/bans/by-jail

   ```python
   class MyAggregationResponse(BaseModel):
       jails: list[JailBanCount]  # or: countries, buckets, etc.
       total: int
       # Optional: filters, time_range metadata

   # Returns:
   {
     "jails": [...],
     "total": 1234
   }
   ```

Note on field naming:
- Paginated/collection responses always use "items" for the data array.
- Detail responses use domain-specific field names (jail, status, settings).
- Aggregation responses use domain-specific field names (jails, countries, buckets).
- All responses with multiple items include a "total" field.
"""

from typing import Generic, Literal, TypeVar

from pydantic import BaseModel, ConfigDict, Field
from typing_extensions import TypedDict

T = TypeVar("T")


class BanGuiBaseModel(BaseModel):
    """Project-wide Pydantic base model.

    Enforces the canonical **snake_case** API field naming policy:
    all JSON wire-format field names use ``snake_case`` on both the backend
    (Python) and the frontend (TypeScript interfaces).  No ``alias_generator``
    is applied — field names are serialized exactly as written.

    Rules:
    - Every model in ``app/models/`` must inherit from this class.
    - Field names must be ``snake_case`` in Python *and* in the JSON payload.
    - The corresponding TypeScript interface fields must also be ``snake_case``.
    - Never add a ``camelCase`` alias generator to individual models — any
      serialization change must go through this base class so all models
      update at once.
    """

    model_config = ConfigDict(strict=True)


class PaginationMetadata(BanGuiBaseModel):
    """Pagination metadata embedded in paginated list responses.

    Contains page information and computed fields to support frontend pagination controls.
    Supports both offset-based and cursor-based pagination modes.

    Fields:
        page: Current page number (1-based). Set to 1 for cursor pagination.
        page_size: Number of items per page.
        total: Total number of items matching the query (across all pages).
               For cursor pagination, this is -1 (unknown without full scan).
        total_pages: Computed total number of pages.
                     For cursor pagination, this is -1 (unknown without full scan).
        has_next_page: Whether there is a next page after this one.
        has_prev_page: Whether there is a previous page before this one.
                       Always False for cursor pagination (cannot navigate backward without storing history).
        cursor: Opaque cursor token for fetching the next page (cursor pagination only).
                None for offset pagination or when there are no more pages.
        pagination_mode: Pagination mode used by the endpoint. 'offset' uses page/page_size;
                        'cursor' uses cursor tokens for navigation.

    Example (offset pagination):
        ```python
        pagination = PaginationMetadata(
            page=2,
            page_size=50,
            total=150,
            total_pages=3,
            has_next_page=True,
            has_prev_page=True,
            cursor=None,
            pagination_mode="offset",
        )
        ```

    Example (cursor pagination):
        ```python
        pagination = PaginationMetadata(
            page=1,
            page_size=50,
            total=-1,
            total_pages=-1,
            has_next_page=True,
            has_prev_page=False,
            cursor="eyJpZCI6IDQyN30=",
            pagination_mode="cursor",
        )
        ```
    """

    page: int = Field(..., ge=1, description="Current page number (1-based). Set to 1 for cursor pagination.")
    page_size: int = Field(..., ge=1, description="Number of items per page.")
    total: int = Field(..., description="Total number of items matching the query. -1 if unknown (cursor pagination).")
    total_pages: int = Field(..., description="Computed total number of pages. -1 if unknown (cursor pagination).")
    has_next_page: bool = Field(..., description="Whether there is a next page after this one.")
    has_prev_page: bool = Field(..., description="Whether there is a previous page before this one.")
    cursor: str | None = Field(
        default=None,
        description="Opaque cursor token for fetching the next page (cursor pagination only).",
    )
    pagination_mode: Literal["offset", "cursor"] = Field(
        default="offset",
        description="Pagination mode used by the endpoint. 'offset' uses page/page_size; 'cursor' uses cursor tokens.",
    )


class PaginatedListResponse(BanGuiBaseModel, Generic[T]):
    """Standardized paginated list response.

    Use this as a base for all endpoints that return paginated collections.
    Automatically includes pagination metadata to support frontend paging UIs.

    Fields:
        items: The data items for the current page.
        pagination: Pagination metadata with computed derived fields.

    Example:
        ```python
        class UserListResponse(PaginatedListResponse[User]):
            pass

        # Returns:
        {
          "items": [...],
          "pagination": {
            "page": 2,
            "page_size": 50,
            "total": 150,
            "total_pages": 3,
            "has_next_page": true,
            "has_prev_page": true
          }
        }
        ```
    """

    items: list[T] = Field(default_factory=list, description="Data items for the current page.")
    pagination: PaginationMetadata = Field(..., description="Pagination metadata with computed derived fields.")


class CollectionResponse(BanGuiBaseModel, Generic[T]):
    """Standardized non-paginated collection response.

    Use this for endpoints that return a collection without pagination support.
    Simpler than PaginatedListResponse, but still provides consistent wrapping.

    Fields:
        items: The data items in the collection.
        total: Total number of items.

    Example:
        ```python
        class ActiveBansResponse(CollectionResponse[ActiveBan]):
            pass

        # Returns:
        {
          "items": [...],
          "total": 42
        }
        ```
    """

    items: list[T] = Field(default_factory=list, description="Collection items.")
    total: int = Field(..., ge=0, description="Total number of items.")


class CommandResponse(BanGuiBaseModel):
    """Standardized command/action result response.

    Use this for endpoints that execute commands (start, stop, reload, ban, unban, etc.).
    Always includes a success indicator and human-readable message.

    Fields:
        message: Human-readable result message or error description.
        success: Whether the command succeeded (default True).

    Example:
        ```python
        class StartJailResponse(CommandResponse):
            jail: str  # Optional: target identifier

        # Returns:
        {
          "message": "Jail 'sshd' started.",
          "success": true,
          "jail": "sshd"
        }
        ```
    """

    message: str = Field(..., description="Human-readable result or error message.")
    success: bool = Field(
        default=True,
        description="Whether the command succeeded (false for errors in non-exception handlers).",
    )


class ErrorResponse(BanGuiBaseModel):
    """Standardized error response envelope for all API errors.

    Use this for all error responses to ensure consistent client-side error handling.
    The error code enables machine-readable branching, while detail provides
    human-readable context. Metadata offers optional structured context.

    The correlation_id field enables tracing this error back through logs on both
    frontend and backend, enabling correlation across distributed systems.

    Fields:
        code: Machine-readable error code (e.g., "jail_not_found", "invalid_input").
        detail: Human-readable error description for display to users.
        metadata: Optional structured context (e.g., field names, constraint violations).
        correlation_id: Unique ID for correlating this error with request logs.

    Example:
        ```python
        # 404 Not Found
        {
          "code": "jail_not_found",
          "detail": "Jail 'sshd' not found",
          "metadata": {"jail_name": "sshd"},
          "correlation_id": "550e8400-e29b-41d4-a716-446655440000"
        }

        # 400 Bad Request - Validation Error
        {
          "code": "invalid_input",
          "detail": "Invalid IP address format",
          "metadata": {"field": "ip", "value": "999.999.999.999"},
          "correlation_id": "550e8400-e29b-41d4-a716-446655440000"
        }

        # 409 Conflict
        {
          "code": "jail_already_active",
          "detail": "Jail is already active: 'sshd'",
          "metadata": {"jail_name": "sshd", "current_status": "active"},
          "correlation_id": "550e8400-e29b-41d4-a716-446655440000"
        }
        ```
    """

    code: str = Field(..., description="Machine-readable error code for client-side branching.")
    detail: str = Field(..., description="Human-readable error description.")
    metadata: "ErrorMetadata" = Field(
        default_factory=dict,
        description="Optional structured context for the error.",
    )
    correlation_id: str | None = Field(
        default=None,
        description="Unique ID for correlating this error with request logs on both frontend and backend.",
    )


# ErrorMetadata must be defined after ErrorResponse due to Pydantic forward-ref resolution
# but before use at type-check time. This ordering is intentional.


class ErrorMetadata(TypedDict, total=False):
    """Typed metadata fields for error responses.

    Allows type-safe access to known metadata keys in exception handlers.
    Keys are optional — exceptions return only relevant fields.

    Fields:
        jail_name: Name of the jail involved in the error.
        filename: Config filename involved in the error.
        filter_name: Name of the filter involved in the error.
        action_name: Name of the action involved in the error.
        source_id: ID of a blocklist source involved in the error.
        url: URL involved in a blocklist error.
        ip: IP address involved in the error.
        pattern: Regex pattern that caused an error.
        error: Regex compilation error message.
        pattern_length: Actual length of an oversized pattern.
        max_length: Maximum allowed length for a pattern.
        timeout_seconds: Timeout value for regex compilation.
        retry_after_seconds: Seconds to wait before retrying (rate limit errors).
        socket_path: fail2ban socket path for connection errors.
        current_status: Current jail status for conflict errors.
        actual_length: Actual pattern length (alias for pattern_length).
        message: Generic error message string.
    """

    jail_name: str
    filename: str
    filter_name: str
    action_name: str
    source_id: int
    url: str
    ip: str
    pattern: str
    error: str
    pattern_length: int
    max_length: int
    timeout_seconds: int
    retry_after_seconds: float
    socket_path: str
    current_status: str
    actual_length: int
    message: str
    field_errors: int
    first_field: str


class ComponentHealth(BanGuiBaseModel):
    """Health status of a single application component.

    Fields:
        name: Human-readable component name.
        healthy: True when the component is operational.
        message: Optional detail message (e.g., error description).
    """

    name: str = Field(..., description="Component name.")
    healthy: bool = Field(..., description="True when the component is operational.")
    message: str | None = Field(
        default=None,
        description="Optional detail message, e.g. error description.",
    )


class HealthResponse(BanGuiBaseModel):
    """Standardized response for the health check endpoint.

    Fields:
        status: Application health status — 'ok' when all components are healthy,
            'degraded' when some components are unhealthy but the service can still
            handle requests, 'unavailable' when fail2ban is offline.
        fail2ban: fail2ban daemon status — 'online' or 'offline'.
        database: Database connectivity — 'ok' or 'error'.
        scheduler: Background scheduler status — 'running', 'stopped', or 'unknown'.
        cache: Cache initialization status — 'initialised' or 'uninitialised'.
        external_logging: External logging handler status — 'ok', 'error', or 'disabled'.
        components: Per-component health detail list (empty when all healthy).

    Example:
        ```python
        # Healthy (HTTP 200)
        {
          "status": "ok",
          "fail2ban": "online",
          "database": "ok",
          "scheduler": "running",
          "cache": "initialised",
          "external_logging": "disabled",
          "components": []
        }

        # Unhealthy (HTTP 503)
        {
          "status": "unavailable",
          "fail2ban": "offline",
          "database": "ok",
          "scheduler": "running",
          "cache": "initialised",
          "external_logging": "ok",
          "components": [{"name": "fail2ban", "healthy": false, "message": "Socket not reachable"}]
        }
        ```
    """

    status: Literal["ok", "degraded", "unavailable"] = Field(
        ...,
        description=(
            "Application health status: 'ok' when healthy, 'degraded' when some "
            "components are unhealthy, 'unavailable' when fail2ban is offline."
        ),
    )
    fail2ban: Literal["online", "offline"] = Field(
        ...,
        description="fail2ban daemon status: 'online' when reachable, 'offline' otherwise.",
    )
    database: Literal["ok", "error"] = Field(
        ...,
        description="Database connectivity: 'ok' when accessible, 'error' when not.",
    )
    scheduler: Literal["running", "stopped", "unknown"] = Field(
        ...,
        description="Background scheduler status: 'running', 'stopped', or 'unknown'.",
    )
    cache: Literal["initialised", "uninitialised"] = Field(
        ...,
        description="Cache initialization status: 'initialised' when ready, 'uninitialised' when not.",
    )
    external_logging: Literal["ok", "error", "disabled"] = Field(
        ...,
        description=(
            "External logging handler status: 'ok' when operational, 'error' when "
            "initialization failed, 'disabled' when external logging is not configured."
        ),
    )
    components: list[ComponentHealth] = Field(
        default_factory=list,
        description="Per-component health detail list. Empty when status is 'ok'.",
    )


class FlushLogsResponse(BanGuiBaseModel):
    """Standardized response for the flush-logs command endpoint.

    Fields:
        message: Human-readable result message from fail2ban.

    Example:
        ```python
        {"message": "Success: fail2ban log files were flushed."}
        ```
    """

    message: str = Field(..., description="Human-readable result message from fail2ban.")


class ReadyCheck(BanGuiBaseModel):
    """Result of a single readiness subsystem check.

    Fields:
        name: Subsystem name (e.g., "database", "fail2ban", "config_dir").
        healthy: True when the subsystem is reachable/operational.
        message: Optional error message describing the failure.
    """

    name: str = Field(..., description="Subsystem name.")
    healthy: bool = Field(..., description="True when the subsystem is operational.")
    message: str | None = Field(
        default=None,
        description="Error detail when the check fails.",
    )


class ReadyResponse(BanGuiBaseModel):
    """Structured readiness check response for the ``/health/ready`` endpoint.

    Fields:
        status: "ok" when all checks pass, "error" when at least one failed.
        checks: Per-subsystem result list.
        failed_count: Number of checks that returned healthy=False.

    Example:
        ```python
        # All healthy (HTTP 200)
        {"status": "ok", "checks": [...], "failed_count": 0}

        # Some failed (HTTP 503)
        {"status": "error", "checks": [...], "failed_count": 2}
        ```
    """

    status: Literal["ok", "error"] = Field(
        ...,
        description="'ok' when all checks pass, 'error' when at least one fails.",
    )
    checks: list[ReadyCheck] = Field(
        default_factory=list,
        description="Per-subsystem check results.",
    )
    failed_count: int = Field(
        ...,
        ge=0,
        description="Number of checks that returned healthy=False.",
    )
