"""
Core types, models, and utilities shared across all search server implementations.

This module provides:
- IndexConfig dataclass for index configuration
- Request/Response Pydantic models for the unified API
- Utility functions for data loading and processing
"""

import json
import re
from dataclasses import dataclass
from typing import Any, Optional

import yaml
from pydantic import BaseModel, Field


# =============================================================================
# Configuration
# =============================================================================


@dataclass
class IndexConfig:
    """Configuration for a single search index."""

    name: str
    description: str
    data_file: str
    id_column: str
    text_column: str
    searchable_columns: list[str]
    metadata_columns: Optional[list[str]] = None
    index_path: Optional[str] = None
    include_fields: Optional[list[str]] = None
    exclude_fields: Optional[list[str]] = None


@dataclass
class TenantConfig:
    """Configuration for a single tenant."""

    name: str
    description: str
    indices: list[IndexConfig]


def _parse_index_configs(indices_config: list[dict]) -> list[IndexConfig]:
    if not indices_config:
        raise ValueError("At least one index must be defined in YAML config")

    index_configs = []
    for idx_config in indices_config:
        required_fields = [
            "name",
            "description",
            "data_file",
            "id_column",
            "text_column",
            "searchable_columns",
        ]
        missing_fields = [field for field in required_fields if field not in idx_config]
        if missing_fields:
            raise ValueError(f"Missing required fields for index: {missing_fields}")

        index_config = IndexConfig(
            name=idx_config["name"],
            description=idx_config["description"],
            data_file=idx_config["data_file"],
            id_column=idx_config["id_column"],
            text_column=idx_config["text_column"],
            searchable_columns=idx_config["searchable_columns"],
            metadata_columns=idx_config.get("metadata_columns"),
            index_path=idx_config.get("index_path"),
            include_fields=idx_config.get("include_fields"),
            exclude_fields=idx_config.get("exclude_fields"),
        )
        index_configs.append(index_config)

    return index_configs


def load_config_from_yaml(config_path: str) -> tuple[str, str, list[IndexConfig], dict]:
    """Load server and indices configuration from YAML file.

    Expected YAML structure:
    server:
      name: "my_search_server"
      description: "Multi-index search server"
      mount: "/my-cool-mcp-server" # defaults to /mcp/
      host: "0.0.0.0"  # optional
      port: 8000       # optional

    indices:
      - name: "books"
        description: "Programming books collection"
        data_file: "data/books.jsonl"
        id_column: "id"
        text_column: "content"
        searchable_columns: ["title", "content", "author"]
        metadata_columns: ["publisher", "edition"]  # optional
        index_path: "data/books_index"  # optional
        include_fields: ["title", "content"]  # optional
        exclude_fields: ["metadata"]  # optional

    Returns:
        tuple: (server_name, server_description, list_of_index_configs, server_config)
    """
    with open(config_path, "r", encoding="utf-8") as f:
        config = yaml.safe_load(f)

    server_config = config.get("server", {})
    server_name = server_config.get("name")
    server_description = server_config.get("description")

    if not server_name:
        raise ValueError("Server name is required in YAML config")
    if not server_description:
        raise ValueError("Server description is required in YAML config")

    indices_config = config.get("indices", [])
    index_configs = _parse_index_configs(indices_config)

    return server_name, server_description, index_configs, server_config


def load_multi_tenant_config_from_yaml(
    config_path: str,
) -> tuple[str, str, list[TenantConfig], dict]:
    """Load multi-tenant server configuration from YAML file.

    Expected YAML structure:
    server:
      name: "multi_tenant_search"
      description: "Multi-tenant search server"
      host: "0.0.0.0"  # optional
      port: 8000       # optional
      base_mcp_port: 9001  # optional

    tenants:
      - name: "tenant_a"
        description: "Tenant A indices"
        indices:
          - name: "docs"
            description: "Documents"
            data_file: "data/docs.jsonl"
            id_column: "id"
            text_column: "content"
            searchable_columns: ["title", "content"]
    """
    with open(config_path, "r", encoding="utf-8") as f:
        config = yaml.safe_load(f)

    server_config = config.get("server", {})
    server_name = server_config.get("name")
    server_description = server_config.get("description")

    if not server_name:
        raise ValueError("Server name is required in YAML config")
    if not server_description:
        raise ValueError("Server description is required in YAML config")

    tenants_config = config.get("tenants", [])
    if not tenants_config:
        raise ValueError("At least one tenant must be defined in YAML config")

    tenant_configs: list[TenantConfig] = []
    for tenant in tenants_config:
        tenant_name = tenant.get("name")
        tenant_description = tenant.get("description")
        if not tenant_name:
            raise ValueError("Tenant name is required in YAML config")
        if not tenant_description:
            raise ValueError(
                f"Tenant description is required for tenant '{tenant_name}'"
            )

        indices_config = tenant.get("indices", [])
        index_configs = _parse_index_configs(indices_config)
        tenant_configs.append(
            TenantConfig(
                name=tenant_name,
                description=tenant_description,
                indices=index_configs,
            )
        )

    return server_name, server_description, tenant_configs, server_config


# =============================================================================
# Data Loading Utilities
# =============================================================================


def load_jsonl_data(file_path: str) -> list[dict]:
    """Load data from a JSONL file with auto-conversion for legacy schema."""
    data = []
    with open(file_path, "r", encoding="utf-8") as f:
        for line in f:
            line = line.strip()
            if line:
                record = json.loads(line)

                # Auto-convert legacy schema fields
                if "text" in record and "content" not in record:
                    record["content"] = record["text"]

                if "summary" in record and "description" not in record:
                    record["description"] = record["summary"]

                data.append(record)
    return data


def ensure_id_column(data: list[dict], id_column: str) -> list[dict]:
    """Ensure the specified id_column exists, adding it with index values if missing."""
    if not data:
        return data

    if id_column not in data[0]:
        if id_column == "id":
            for i, record in enumerate(data):
                record["id"] = str(i)
        else:
            print(f"Warning: Column '{id_column}' not found. Adding with index values.")
            for i, record in enumerate(data):
                record[id_column] = str(i)

    return data


# =============================================================================
# Text Processing Utilities
# =============================================================================


def preview_text(text: str, max_tokens: int = 200) -> str:
    """Create a preview of text content, preserving newlines.

    Args:
        text: The text to preview
        max_tokens: Maximum number of tokens (words) to include

    Returns:
        Truncated text with [...] suffix if truncated
    """
    if not text:
        return "[Document has no content.]"

    # Remove leading whitespace/dashes
    text = text.strip("\n -")

    # Split into tokens and separators, keeping both
    parts = re.split(r"(\s+)", text)

    # Separate tokens (even indices) from separators (odd indices)
    tokens = []
    separators = []

    for i, part in enumerate(parts):
        if i % 2 == 0:  # Even indices are tokens
            if part:  # Skip empty tokens
                tokens.append(part)
        else:  # Odd indices are whitespace separators
            separators.append(part)

    # If under limit, return original text
    if len(tokens) <= max_tokens:
        return text

    # Rebuild with truncated tokens, preserving newlines
    result = []
    for i in range(max_tokens):
        result.append(tokens[i])
        if i < len(separators):
            sep = separators[i]
            # Preserve newlines, normalize spaces
            if "\n" in sep:
                result.append("\n")
            else:
                result.append(" ")

    return "".join(result).rstrip() + " [...]"


def normalize_field_value(value: Any) -> str:
    """Convert field value to string, handling JSON/list columns."""
    if value is None:
        return ""
    elif isinstance(value, (list, tuple)):
        return ", ".join(str(item) for item in value if item is not None)
    elif isinstance(value, dict):
        return ", ".join([f"{k}: {v}" for k, v in value.items()])
    else:
        return str(value)


def normalize_metadata_value(value: Any) -> list[str]:
    """Normalize metadata field values for exact-match filtering (case-insensitive)."""
    if value is None:
        return []
    if isinstance(value, (list, tuple, set)):
        values = [str(item).strip() for item in value if item is not None]
    elif isinstance(value, dict):
        values = [normalize_field_value(value).strip()]
    else:
        values = [str(value).strip()]
    return [v.lower() for v in values if v]


# =============================================================================
# Request/Response Models (Unified API)
# =============================================================================


class SearchRequest(BaseModel):
    """Request model for search endpoint."""

    index: str = Field(..., description="Name of the index to search")
    queries: list[str] = Field(..., description="List of search query strings")
    limit: int = Field(5, description="Maximum number of results to return")
    include_content: bool = Field(
        False,
        description="If true, include full content dict in results; if false, only preview fields",
    )
    metadata_filters: dict[str, list[str]] | None = Field(
        None,
        description=(
            "Optional metadata filters: {field: [value1, value2]} (OR within field, AND across fields)"
        ),
    )


class FetchRequest(BaseModel):
    """Request model for fetch endpoint."""

    index: str = Field(..., description="Name of the index to fetch from")
    document_ids: list[str] = Field(..., description="List of document IDs to retrieve")
    filters: list[dict[str, Any]] | None = Field(
        None,
        description=(
            "Optional list of filter specs (one per document). Each filter can have: "
            "start_line/end_line for line range, or grep_pattern/context_lines for regex search, "
            "or {} for no filtering"
        ),
    )


class BatchCommand(BaseModel):
    """A single command in a batch request."""

    name: str = Field(..., description="Command name: 'search' or 'fetch'")
    index: str = Field(..., description="Index name")
    # Search-specific fields
    queries: list[str] | None = Field(None, description="Search queries (for search)")
    limit: int = Field(5, description="Result limit (for search)")
    include_content: bool = Field(
        False, description="Include full content (for search)"
    )
    metadata_filters: dict[str, list[str]] | None = Field(
        None, description="Metadata filters (for search)"
    )
    # Fetch-specific fields
    document_ids: list[str] | None = Field(None, description="Document IDs (for fetch)")
    filters: list[dict[str, Any]] | None = Field(
        None, description="Filters (for fetch)"
    )


class BatchRequest(BaseModel):
    """Request model for batch operations."""

    commands: list[BatchCommand] = Field(..., description="List of commands to execute")


class SearchResultBase(BaseModel):
    """Base search result with preview fields only."""

    id: str
    score: float
    title: str | None = None
    description: str | None = None
    preview: str


class SearchResultFull(SearchResultBase):
    """Full search result with complete content dict."""

    content: dict[str, Any]


class SearchResponse(BaseModel):
    """Response model for search endpoint."""

    index: str
    queries: list[str]
    results: list[dict[str, Any]]  # Can be SearchResultBase or SearchResultFull
    total: int


class FetchResponse(BaseModel):
    """Response model for fetch endpoint."""

    index: str
    documents: list[dict[str, Any]]
    found_ids: list[str]
    missing_ids: list[str]


class BatchResponse(BaseModel):
    """Response model for batch operations."""

    results: list[dict[str, Any]]


class IndexInfo(BaseModel):
    """Information about a single index."""

    name: str
    description: str
    document_count: int
    searchable_columns: list[str] | None = None
    metadata_columns: list[str] | None = None


class IndicesResponse(BaseModel):
    """Response model for listing indices."""

    indices: list[IndexInfo]


class HealthResponse(BaseModel):
    """Response model for health check."""

    status: str
    indices: list[str]
    server: str | None = None


# =============================================================================
# Exports
# =============================================================================

__all__ = [
    # Configuration
    "IndexConfig",
    "load_config_from_yaml",
    # Data utilities
    "load_jsonl_data",
    "ensure_id_column",
    # Text utilities
    "preview_text",
    "normalize_field_value",
    "normalize_metadata_value",
    # Request models
    "SearchRequest",
    "FetchRequest",
    "BatchCommand",
    "BatchRequest",
    # Response models
    "SearchResultBase",
    "SearchResultFull",
    "SearchResponse",
    "FetchResponse",
    "BatchResponse",
    "IndexInfo",
    "IndicesResponse",
    "HealthResponse",
]
