"""
Settings and configuration management using Pydantic.

Supports:
- YAML/TOML configuration files
- Environment variable overrides
- Profile-based configurations
"""

import os
from pathlib import Path
from typing import Any, Dict, List, Literal, Optional, Union

import yaml
from pydantic import BaseModel, Field, field_validator
from pydantic_settings import BaseSettings


class AppSettings(BaseModel):
    """Application-level settings."""
    name: str = "ReadAnyBook"
    version: str = "0.1.0"
    log_level: str = "INFO"
    temp_dir: str = "/tmp/readanybook"


class IngestionSettings(BaseModel):
    """Document ingestion settings."""
    supported_formats: List[str] = [".pdf", ".epub", ".html", ".tex", ".md"]
    max_file_size_mb: int = 100
    extract_images: bool = False
    ocr_enabled: bool = False


class ChunkingSettings(BaseModel):
    """Text chunking settings."""
    chunk_size: int = 1000
    chunk_overlap: int = 200
    min_chunk_size: int = 100
    max_chunk_size: int = 2000
    separators: List[str] = ["\n\n", "\n", ". ", " "]
    preserve_math: bool = True
    preserve_code: bool = True


class EmbeddingSettings(BaseModel):
    """Embedding model settings."""
    model_name: str = "BAAI/bge-small-en-v1.5"
    model_type: str = "sentence-transformers"
    batch_size: int = 32
    normalize: bool = True
    device: str = "auto"


class ChromaSettings(BaseModel):
    """Chroma-specific settings."""
    host: Optional[str] = None
    port: Optional[int] = None


class QdrantSettings(BaseModel):
    """Qdrant-specific settings."""
    host: str = "localhost"
    port: int = 6333
    grpc_port: int = 6334


class WeaviateSettings(BaseModel):
    """Weaviate-specific settings."""
    host: str = "localhost"
    port: int = 8080


class VectorDBSettings(BaseModel):
    """Vector database settings."""
    backend: Literal["chroma", "qdrant", "weaviate"] = "chroma"
    persist_directory: str = "./data/vectordb"
    collection_name: str = "readanybook"
    distance_metric: str = "cosine"
    chroma: ChromaSettings = ChromaSettings()
    qdrant: QdrantSettings = QdrantSettings()
    weaviate: WeaviateSettings = WeaviateSettings()


class BM25Settings(BaseModel):
    """BM25 sparse retrieval settings."""
    k1: float = 1.5
    b: float = 0.75


class RetrievalSettings(BaseModel):
    """Retrieval settings."""
    strategy: Literal["dense", "sparse", "hybrid"] = "hybrid"
    top_k: int = 10
    rerank: bool = True
    rerank_model: str = "cross-encoder/ms-marco-MiniLM-L-6-v2"
    rerank_top_k: int = 5
    bm25: BM25Settings = BM25Settings()


class HuggingFaceSettings(BaseModel):
    """HuggingFace-specific LLM settings."""
    trust_remote_code: bool = True
    torch_dtype: str = "auto"


class VLLMSettings(BaseModel):
    """vLLM-specific settings."""
    tensor_parallel_size: int = 1


class OllamaSettings(BaseModel):
    """Ollama-specific settings."""
    host: str = "http://localhost:11434"


class OpenAISettings(BaseModel):
    """OpenAI-compatible API settings."""
    api_base: Optional[str] = None
    api_key: Optional[str] = None


class LLMSettings(BaseModel):
    """LLM settings."""
    backend: Literal["huggingface", "vllm", "ollama", "openai"] = "huggingface"
    model_name: str = "mistralai/Mistral-7B-Instruct-v0.2"
    max_tokens: int = 2048
    temperature: float = 0.7
    top_p: float = 0.9
    device: str = "auto"
    quantization: Optional[Literal["4bit", "8bit"]] = "4bit"
    huggingface: HuggingFaceSettings = HuggingFaceSettings()
    vllm: VLLMSettings = VLLMSettings()
    ollama: OllamaSettings = OllamaSettings()
    openai: OpenAISettings = OpenAISettings()


class ConceptsGenerationSettings(BaseModel):
    """Concept extraction settings."""
    max_concepts: int = 50
    include_definitions: bool = True
    include_intuitions: bool = True


class FormulasGenerationSettings(BaseModel):
    """Formula extraction settings."""
    max_formulas: int = 100
    normalize_latex: bool = True
    include_derivations: bool = False
    group_by_topic: bool = True


class ModelsGenerationSettings(BaseModel):
    """Model summarization settings."""
    include_assumptions: bool = True
    include_objectives: bool = True
    include_training: bool = True


class AlgorithmsGenerationSettings(BaseModel):
    """Algorithm synthesis settings."""
    include_pseudocode: bool = True
    include_complexity: bool = True
    include_examples: bool = True


class GenerationSettings(BaseModel):
    """Generation pipeline settings."""
    concepts: ConceptsGenerationSettings = ConceptsGenerationSettings()
    formulas: FormulasGenerationSettings = FormulasGenerationSettings()
    models: ModelsGenerationSettings = ModelsGenerationSettings()
    algorithms: AlgorithmsGenerationSettings = AlgorithmsGenerationSettings()


class MarginsSettings(BaseModel):
    """LaTeX margin settings."""
    top: str = "1.5cm"
    bottom: str = "1.5cm"
    left: str = "1.5cm"
    right: str = "1.5cm"


class LaTeXSettings(BaseModel):
    """LaTeX document settings."""
    template: str = "default"
    compiler: Literal["pdflatex", "xelatex", "lualatex", "tectonic"] = "pdflatex"
    output_format: str = "pdf"
    page_limit: int = 12
    font_size: int = 10
    paper_size: str = "a4paper"
    margins: MarginsSettings = MarginsSettings()
    columns: int = 2
    include_toc: bool = False
    include_references: bool = True


class EvaluationSettings(BaseModel):
    """Evaluation settings."""
    enabled: bool = True
    metrics: List[str] = [
        "retrieval_precision",
        "retrieval_recall",
        "answer_relevance",
        "faithfulness",
        "latex_validity",
    ]
    sample_size: int = 100


class TracingSettings(BaseModel):
    """OpenTelemetry tracing settings."""
    enabled: bool = False
    endpoint: str = "http://localhost:4317"
    service_name: str = "readanybook"


class ObservabilitySettings(BaseModel):
    """Logging and tracing settings."""
    structured_logging: bool = True
    log_format: str = "json"
    log_file: str = "./logs/readanybook.log"
    tracing: TracingSettings = TracingSettings()


class Settings(BaseSettings):
    """
    Main settings class for ReadAnyBook.
    
    Supports loading from:
    - YAML configuration files
    - Environment variables (prefixed with READANYBOOK_)
    - Programmatic overrides
    """
    
    app: AppSettings = AppSettings()
    ingestion: IngestionSettings = IngestionSettings()
    chunking: ChunkingSettings = ChunkingSettings()
    embedding: EmbeddingSettings = EmbeddingSettings()
    vectordb: VectorDBSettings = VectorDBSettings()
    retrieval: RetrievalSettings = RetrievalSettings()
    llm: LLMSettings = LLMSettings()
    generation: GenerationSettings = GenerationSettings()
    latex: LaTeXSettings = LaTeXSettings()
    evaluation: EvaluationSettings = EvaluationSettings()
    observability: ObservabilitySettings = ObservabilitySettings()
    
    class Config:
        env_prefix = "READANYBOOK_"
        env_nested_delimiter = "__"
    
    @classmethod
    def from_yaml(cls, path: Union[str, Path]) -> "Settings":
        """Load settings from a YAML file."""
        path = Path(path)
        if not path.exists():
            raise FileNotFoundError(f"Configuration file not found: {path}")
        
        with open(path, "r") as f:
            config_dict = yaml.safe_load(f)
        
        return cls(**config_dict)
    
    @classmethod
    def from_profile(cls, profile: str, base_config: Optional[Path] = None) -> "Settings":
        """
        Load settings with a specific profile applied.
        
        Profiles can be: technical_book, math_paper, nontechnical_book
        """
        if base_config:
            settings = cls.from_yaml(base_config)
        else:
            settings = cls()
        
        # Get default config path
        default_config = Path(__file__).parent.parent / "config" / "default.yaml"
        if default_config.exists():
            with open(default_config, "r") as f:
                config_dict = yaml.safe_load(f)
            
            profiles = config_dict.get("profiles", {})
            if profile in profiles:
                profile_config = profiles[profile]
                settings = settings.apply_overrides(profile_config)
        
        return settings
    
    def apply_overrides(self, overrides: Dict[str, Any]) -> "Settings":
        """Apply configuration overrides and return a new Settings instance."""
        current_dict = self.model_dump()
        
        def deep_merge(base: dict, override: dict) -> dict:
            result = base.copy()
            for key, value in override.items():
                if key in result and isinstance(result[key], dict) and isinstance(value, dict):
                    result[key] = deep_merge(result[key], value)
                else:
                    result[key] = value
            return result
        
        merged = deep_merge(current_dict, overrides)
        return Settings(**merged)
    
    def to_yaml(self, path: Union[str, Path]) -> None:
        """Save settings to a YAML file."""
        path = Path(path)
        path.parent.mkdir(parents=True, exist_ok=True)
        
        with open(path, "w") as f:
            yaml.dump(self.model_dump(), f, default_flow_style=False, sort_keys=False)


def get_default_settings() -> Settings:
    """Get default settings, optionally loading from config file."""
    default_config = Path(__file__).parent.parent / "config" / "default.yaml"
    
    if default_config.exists():
        return Settings.from_yaml(default_config)
    
    return Settings()
