"""OpenSearch backend configuration."""

from __future__ import annotations

import os
from dataclasses import dataclass, field


@dataclass(frozen=True)
class OpenSearchConfig:
    """OpenSearch backend configuration.

    Example:
        ```python
        # From environment variables
        config = OpenSearchConfig.from_env()

        # Explicit configuration
        config = OpenSearchConfig(
            host="localhost",
            port=9200,
            embedding_model="text-embedding-3-small",
        )

        # AWS OpenSearch Service
        config = OpenSearchConfig(
            host="search-domain.us-east-1.es.amazonaws.com",
            port=443,
            use_ssl=True,
            use_aws_sigv4=True,
            aws_region="us-east-1",
        )
        ```
    """

    # === Connection ===
    host: str = "localhost"
    port: int = 9200
    use_ssl: bool = False
    verify_certs: bool = True
    ca_certs: str | None = None

    # Authentication
    username: str | None = None
    password: str | None = None

    # AWS OpenSearch Service
    use_aws_sigv4: bool = False
    aws_region: str | None = None
    aws_service: str = "es"  # "es" for OpenSearch, "aoss" for Serverless

    # === Cluster (High Availability) ===
    nodes: tuple[str, ...] | None = None  # Multiple nodes for HA
    sniff_on_start: bool = False
    sniff_on_node_failure: bool = True
    sniff_timeout: float = 10.0
    sniffer_timeout: float = 60.0

    # === Embedding ===
    embedding_model: str = "text-embedding-3-small"
    embedding_dimension: int = 1536
    openai_api_key: str | None = None
    embedding_batch_size: int = 100

    # === Index Settings ===
    index_prefix: str = "gnosisllm"
    number_of_shards: int = 5
    number_of_replicas: int = 1
    refresh_interval: str = "1s"

    # Pipeline names (auto-generated if not set)
    ingest_pipeline_name: str | None = None
    search_pipeline_name: str | None = None

    # === k-NN Settings ===
    knn_engine: str = "lucene"  # lucene (recommended for OpenSearch 2.9+), faiss
    knn_space_type: str = "l2"  # l2, cosinesimil, innerproduct
    knn_algo_param_ef_search: int = 512
    knn_algo_param_ef_construction: int = 512
    knn_algo_param_m: int = 16

    # === Neural Search (OpenSearch 2.9+) ===
    # model_id is the deployed ML model in OpenSearch for embeddings
    model_id: str | None = None
    model_group_id: str | None = None
    embedding_field: str = "content_embedding"  # Field name for embeddings

    # === Agentic Search ===
    # Agent IDs from 'gnosisllm-knowledge agentic setup'
    flow_agent_id: str | None = None
    conversational_agent_id: str | None = None
    # LLM for agent reasoning (OpenAI model ID)
    agentic_llm_model: str = "gpt-4o"
    # Agent execution limits
    agentic_max_iterations: int = 5
    agentic_timeout_seconds: int = 60
    # Conversation memory settings
    memory_window_size: int = 10  # Messages to keep in context

    # === Timeouts ===
    connect_timeout: float = 5.0
    read_timeout: float = 30.0
    bulk_timeout: float = 120.0

    # === Bulk Indexing ===
    bulk_batch_size: int = 500
    bulk_max_concurrent: int = 3

    @property
    def url(self) -> str:
        """Get the full OpenSearch URL."""
        scheme = "https" if self.use_ssl else "http"
        return f"{scheme}://{self.host}:{self.port}"

    @property
    def knowledge_index_name(self) -> str:
        """Get the default knowledge index name."""
        return f"{self.index_prefix}-knowledge"

    @property
    def agentic_memory_index_name(self) -> str:
        """Get the agentic memory index name."""
        return f"{self.index_prefix}-memory"

    def get_index_name(self, collection_id: str | None = None) -> str:
        """Get index name for a collection.

        Args:
            collection_id: Optional collection ID for multi-tenant indexing.

        Returns:
            Index name pattern.
        """
        if collection_id:
            return f"{self.index_prefix}-{collection_id}"
        return self.knowledge_index_name

    @classmethod
    def from_env(cls) -> OpenSearchConfig:
        """Create config from environment variables.

        Environment variables:
            OPENSEARCH_HOST: Host (default: localhost)
            OPENSEARCH_PORT: Port (default: 9200)
            OPENSEARCH_USE_SSL: Use SSL (default: false)
            OPENSEARCH_VERIFY_CERTS: Verify certificates (default: true)
            OPENSEARCH_USERNAME: Username
            OPENSEARCH_PASSWORD: Password
            OPENSEARCH_USE_AWS_SIGV4: Use AWS Sig v4 auth (default: false)
            AWS_REGION: AWS region for Sig v4
            OPENSEARCH_NODES: Comma-separated list of nodes
            EMBEDDING_MODEL: OpenAI embedding model
            EMBEDDING_DIMENSION: Embedding vector dimension
            OPENAI_API_KEY: OpenAI API key
            OPENSEARCH_INDEX_PREFIX: Index name prefix
            OPENSEARCH_SHARDS: Number of shards
            OPENSEARCH_REPLICAS: Number of replicas
            OPENSEARCH_FLOW_AGENT_ID: Flow agent ID for agentic search
            OPENSEARCH_CONVERSATIONAL_AGENT_ID: Conversational agent ID
            AGENTIC_LLM_MODEL: LLM model for agent reasoning (default: gpt-4o)
            AGENTIC_MAX_ITERATIONS: Maximum agent iterations (default: 5)
            AGENTIC_TIMEOUT_SECONDS: Agent execution timeout (default: 60)

        Returns:
            Configuration from environment.
        """
        nodes_str = os.getenv("OPENSEARCH_NODES", "")
        nodes = tuple(nodes_str.split(",")) if nodes_str else None

        return cls(
            host=os.getenv("OPENSEARCH_HOST", "localhost"),
            port=int(os.getenv("OPENSEARCH_PORT", "9200")),
            use_ssl=os.getenv("OPENSEARCH_USE_SSL", "").lower() == "true",
            verify_certs=os.getenv("OPENSEARCH_VERIFY_CERTS", "true").lower() == "true",
            username=os.getenv("OPENSEARCH_USERNAME"),
            password=os.getenv("OPENSEARCH_PASSWORD"),
            use_aws_sigv4=os.getenv("OPENSEARCH_USE_AWS_SIGV4", "").lower() == "true",
            aws_region=os.getenv("AWS_REGION"),
            nodes=nodes,
            embedding_model=os.getenv("EMBEDDING_MODEL", "text-embedding-3-small"),
            embedding_dimension=int(os.getenv("EMBEDDING_DIMENSION", "1536")),
            openai_api_key=os.getenv("OPENAI_API_KEY"),
            index_prefix=os.getenv("OPENSEARCH_INDEX_PREFIX", "gnosisllm"),
            number_of_shards=int(os.getenv("OPENSEARCH_SHARDS", "5")),
            number_of_replicas=int(os.getenv("OPENSEARCH_REPLICAS", "1")),
            model_id=os.getenv("OPENSEARCH_MODEL_ID"),
            ingest_pipeline_name=os.getenv("OPENSEARCH_INGEST_PIPELINE"),
            search_pipeline_name=os.getenv("OPENSEARCH_SEARCH_PIPELINE"),
            # Agentic search configuration
            flow_agent_id=os.getenv("OPENSEARCH_FLOW_AGENT_ID"),
            conversational_agent_id=os.getenv("OPENSEARCH_CONVERSATIONAL_AGENT_ID"),
            agentic_llm_model=os.getenv("AGENTIC_LLM_MODEL", "gpt-4o"),
            agentic_max_iterations=int(os.getenv("AGENTIC_MAX_ITERATIONS", "5")),
            agentic_timeout_seconds=int(os.getenv("AGENTIC_TIMEOUT_SECONDS", "60")),
        )
