from dataclasses import dataclass, field
from typing import Annotated, Any, Literal, TypeAlias

from inspect_ai._util.zip_common import ZipCompressionMethod
from inspect_ai.event import Timeline
from inspect_ai.event._event import Event
from inspect_ai.log import EventsData
from inspect_ai.model._chat_message import ChatMessage
from pydantic import BaseModel, ConfigDict, Field, JsonValue
from typing_extensions import TypeAliasType

from .._project.types import ProjectConfig
from .._query.condition import Condition
from .._query.order_by import OrderBy
from .._recorder.active_scans_store import ActiveScanInfo
from .._recorder.recorder import Status as RecorderStatus
from .._recorder.summary import Summary
from .._scanner.result import Error, Result
from .._scanspec import ScanSpec
from .._transcript.types import EventFilter, MessageFilter, TranscriptInfo


@dataclass
class Pagination:
    limit: int
    cursor: dict[str, Any] | None
    direction: Literal["forward", "backward"]


@dataclass
class TranscriptsResponse:
    items: list[TranscriptInfo]
    total_count: int
    next_cursor: dict[str, Any] | None = None


@dataclass
class IPCDataFrame:
    """Data frame serialized as Arrow IPC format."""

    format: Literal["arrow.feather"] = "arrow.feather"
    """Type of serialized data frame."""

    version: int = 2
    """Version of serialization format."""

    encoding: Literal["base64"] = "base64"
    """Encoding of serialized data frame."""

    data: str | None = None
    """Data frame serialized as Arrow IPC format."""

    row_count: int | None = None
    """Number of rows in data frame."""

    column_names: list[str] | None = None
    """List of column names in data frame."""


@dataclass
class IPCSerializableResults(RecorderStatus):
    """Scan results as serialized data frames."""

    scanners: dict[str, IPCDataFrame]
    """Dict of scanner name to serialized data frame."""

    def __init__(
        self,
        complete: bool,
        spec: ScanSpec,
        location: str,
        summary: Summary,
        errors: list[Error],
        scanners: dict[str, IPCDataFrame],
    ) -> None:
        super().__init__(complete, spec, location, summary, errors)
        self.scanners = scanners


ScanStatus: TypeAlias = RecorderStatus


class ScanRow(BaseModel):
    """Flat scan row for API response - maps directly to client grid columns.

    Fields are either:
    - Extracted from source types (using model_fields to avoid duplication)
    - Transformed/flattened from nested structures
    - Computed aggregates
    """

    # === Fields extracted from source types (no type duplication) ===
    # fmt: off
    scan_id: ScanSpec.model_fields["scan_id"].annotation  # type: ignore[valid-type]  # noqa: F821
    scan_name: ScanSpec.model_fields["scan_name"].annotation  # type: ignore[valid-type]  # noqa: F821
    scan_file: ScanSpec.model_fields["scan_file"].annotation  # type: ignore[valid-type]  # noqa: F821
    timestamp: ScanSpec.model_fields["timestamp"].annotation  # type: ignore[valid-type]  # noqa: F821
    packages: ScanSpec.model_fields["packages"].annotation  # type: ignore[valid-type]  # noqa: F821
    metadata: ScanSpec.model_fields["metadata"].annotation  # type: ignore[valid-type]  # noqa: F821
    scan_args: ScanSpec.model_fields["scan_args"].annotation  # type: ignore[valid-type]  # noqa: F821
    # fmt: on
    location: str  # from Status dataclass

    # === Transformed/flattened fields ===
    status: Literal["active", "error", "complete", "incomplete"]
    scanners: str  # comma-separated scanner names
    model: str | None  # extracted from ModelConfig.model
    tags: str  # comma-separated tags
    revision_version: str | None
    revision_commit: str | None
    revision_origin: str | None

    # === Computed aggregate fields ===
    total_results: int
    total_errors: int
    total_tokens: int

    # === For active scan progress (None if not active) ===
    active_completion_pct: int | None = None

    # transcript count
    transcript_count: int


@dataclass
class PaginatedRequest:
    """Base request with filter, order_by, and pagination."""

    filter: Condition | None = None
    order_by: OrderBy | list[OrderBy] | None = None
    pagination: Pagination | None = None


@dataclass
class TranscriptsRequest(PaginatedRequest):
    """Request body for POST /transcripts endpoint."""

    pass


@dataclass
class DistinctRequest:
    """Request body for POST /transcripts/{dir}/distinct endpoint."""

    column: str
    filter: Condition | None = None


@dataclass
class ScansRequest(PaginatedRequest):
    """Request body for POST /scans endpoint."""

    pass


@dataclass
class ScansResponse:
    """Response body for POST /scans endpoint."""

    items: list[ScanRow]
    total_count: int
    next_cursor: dict[str, Any] | None = None


@dataclass
class ActiveScansResponse:
    """Response body for GET /scans/active endpoint."""

    items: dict[str, ActiveScanInfo]


class AppDir(BaseModel):
    """Directory with source tracking."""

    dir: str
    source: Literal["project", "cli"]


class AppConfig(ProjectConfig):
    """Application configuration returned by GET /config."""

    home_dir: str
    project_dir: str
    transcripts: AppDir | None  # type: ignore[assignment]
    scans: AppDir  # type: ignore[assignment]


@dataclass
class ValidationCaseRequest:
    """Request body for creating or updating a validation case."""

    id: str | list[str] | None = None
    """Case ID (required for create, optional for upsert where ID comes from URL path)."""

    target: JsonValue | None = None
    """Target value for the case (mutually exclusive with labels)."""

    labels: dict[str, bool] | None = None
    """Label presence/absence expectations for resultset validation (mutually exclusive with target)."""

    split: str | None = None
    """Optional split name for organizing cases."""

    predicate: str | None = None
    """Optional predicate for comparing scanner result to target."""

    task_id: str | None = None
    """Optional sample identifier (informational, written to file but not used in validation)."""

    task_repeat: int | None = None
    """Optional epoch/repeat number (informational, written to file but not used in validation)."""


@dataclass
class CreateValidationSetRequest:
    """Request body for POST /validations endpoint."""

    path: str
    """Absolute URI for the new file (e.g., 'file:///Users/.../my_set.csv')."""

    cases: list[ValidationCaseRequest] = field(default_factory=list)
    """Initial cases to add to the validation set."""


@dataclass
class RenameValidationSetRequest:
    """Request body for PUT /validations/{uri}/rename endpoint."""

    name: str
    """New name for the validation set (without extension)."""


@dataclass
class ScannerParam:
    """Parameter definition for a scanner factory."""

    name: str
    """Parameter name."""

    schema: dict[str, Any]
    """JSON Schema for the parameter type."""

    required: bool
    """Whether the parameter is required."""

    default: Any | None = None
    """Default value if not required."""


@dataclass
class ScannerInfo:
    """Info about a registered scanner factory."""

    name: str
    """Scanner name."""

    version: int
    """Scanner version."""

    description: str | None
    """First line of scanner docstring."""

    params: list[ScannerParam]
    """Scanner parameters."""


@dataclass
class ScannersResponse:
    """Response body for GET /scanners endpoint."""

    items: list[ScannerInfo]


class MessagesEventsResponse(BaseModel):
    """Response for GET /transcripts/{dir}/{id}/messages-events endpoint."""

    messages: list[ChatMessage]
    events: list[Event]
    timelines: list[Timeline]
    attachments: dict[str, str] | None = None
    events_data: EventsData | None = None

    model_config = ConfigDict(extra="allow")


@dataclass
class DistResponse:
    """Response body for GET /dist endpoint."""

    path: str
    """Absolute filesystem path to the resolved dist directory."""


@dataclass
class StreamMetadata:
    """Metadata yielded first from streaming generators."""

    compression_method: ZipCompressionMethod | None
    uncompressed_size: int | None


class SearchRequestBase(BaseModel):
    """Shared fields for search request variants."""

    query: str

    messages: MessageFilter = Field(default=None)
    """Filter for which message types to include when loading the transcript."""

    events: EventFilter = Field(default=None)
    """Filter for which event types to include when loading the transcript."""

    model_config = ConfigDict(extra="forbid")


class GrepSearchRequest(SearchRequestBase):
    """Request body for grep transcript searches."""

    type: Literal["grep"] = "grep"
    regex: bool = False
    ignore_case: bool = True
    word_boundary: bool = False


class LlmSearchRequest(SearchRequestBase):
    """Request body for LLM transcript searches."""

    type: Literal["llm"] = "llm"
    model: str | None = None


SearchRequest = TypeAliasType(
    "SearchRequest",
    Annotated[GrepSearchRequest | LlmSearchRequest, Field(discriminator="type")],
)


class SearchInputBase(BaseModel):
    """Shared fields for persisted search input history."""

    search_id: str
    query: str
    created_at: str

    model_config = ConfigDict(extra="forbid")


class GrepSearchInput(SearchInputBase):
    """A persisted grep search input."""

    type: Literal["grep"] = "grep"
    regex: bool
    ignore_case: bool
    word_boundary: bool


class LlmSearchInput(SearchInputBase):
    """A persisted LLM search input."""

    type: Literal["llm"] = "llm"
    model: str | None = None


SearchInput = TypeAliasType(
    "SearchInput",
    Annotated[GrepSearchInput | LlmSearchInput, Field(discriminator="type")],
)


@dataclass
class SearchInputListResponse:
    """Response from the list search inputs endpoint."""

    items: list[SearchInput]


@dataclass
class SearchResponse:
    """Response from running a transcript search."""

    id: str
    result: Result
