from __future__ import annotations

from pathlib import Path
from typing import Literal
from urllib.parse import urlsplit

from pydantic import Field, SecretStr, ValidationError, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict

from .errors import ConfigurationError
from .runtime import CallContext


class Settings(BaseSettings):
    model_config = SettingsConfigDict(
        extra="ignore",
        case_sensitive=False,
        hide_input_in_errors=True,
        env_file_encoding="utf-8",
        enable_decoding=False,
    )
    deepjev_mode: Literal["mock", "live"] = "mock"
    jev_provider: Literal["vercel_gateway", "typesafe_direct", "openrouter", "mock"] = (
        "vercel_gateway"
    )
    jev_model: str = "typesafe-ai/jev"
    jev_fallback_enabled: bool = True
    openrouter_jev_model: str = "typesafe/jev-1.13"
    openrouter_decisions_url: str = "https://openrouter.ai/api/alpha/decisions"
    ai_gateway_api_key: SecretStr = Field(default=SecretStr(""), repr=False, exclude=True)
    vercel_ai_gateway_base_url: str = "https://ai-gateway.vercel.sh/v4/ai"
    typesafe_api_key: SecretStr = Field(default=SecretStr(""), repr=False, exclude=True)
    typesafe_base_url: str = "https://api.typesafe.ai/v1"
    embedding_provider: Literal["openrouter", "mock"] = "openrouter"
    openrouter_api_key: SecretStr = Field(default=SecretStr(""), repr=False, exclude=True)
    openrouter_base_url: str = "https://openrouter.ai/api/v1"
    embedding_model: str = "google/gemini-embedding-2"
    embedding_dimensions: int = Field(default=3072, ge=1, le=65536)
    embedding_encoding_format: Literal["float"] = "float"
    embedding_max_input_tokens: int = Field(default=8192, ge=1)
    embedding_batch_size: int = Field(default=2, ge=1, le=256)
    embedding_upstream_only: str = ""  # comma-separated OpenRouter upstream slugs; empty = any
    embedding_normalize: bool = True
    embedding_similarity_metric: Literal["cosine"] = "cosine"
    embedding_query_input_type: str | None = None
    embedding_document_input_type: str | None = None
    embedding_query_prefix: str = "task: search result | query: "
    embedding_document_prefix: str = "title: none | text: "
    embedding_space_version: int = Field(default=2, ge=1)
    proposer_provider: Literal["openrouter", "mock"] = "openrouter"
    semantic_proposer_model: str = "qwen/qwen3.8-27b"
    surface_proposer_model: str = "qwen/qwen3.8-27b"
    proposer_temperature: float = Field(default=0, ge=0, le=2, allow_inf_nan=False)
    proposer_reasoning_enabled: bool | None = False
    proposer_response_mode: Literal["json_schema", "json_object"] = "json_schema"
    semantic_proposer_max_tokens: int = Field(default=2048, ge=32, le=16384)
    surface_proposer_max_tokens: int = Field(default=1024, ge=32, le=16384)
    run_max_generation_calls: int = Field(default=4, ge=0)
    run_max_semantic_proposer_calls: int = Field(default=2, ge=0)
    run_max_surface_proposer_calls: int = Field(default=2, ge=0)
    run_max_schema_repairs: int = Field(default=1, ge=0, le=1)
    service_tokens_json: SecretStr = Field(default=SecretStr(""), repr=False, exclude=True)
    service_requests_per_minute: int = Field(default=30, ge=1, le=1000)
    service_max_concurrency: int = Field(default=4, ge=1, le=32)
    http_timeout_seconds: float = Field(default=20, gt=0, allow_inf_nan=False)
    http_max_attempts: int = Field(default=3, ge=1, le=10)
    run_deadline_seconds: float = Field(default=60, gt=0, allow_inf_nan=False)
    run_max_jev_calls: int = Field(default=12, ge=1)
    run_max_embedding_calls: int = Field(default=4, ge=1)
    run_max_http_attempts: int = Field(default=24, ge=1)
    run_max_cost_usd: float | None = Field(default=None, gt=0, allow_inf_nan=False)
    choice_policy: Literal["probability_margin", "provider_confidence"] = "probability_margin"
    choice_min_probability: float = Field(default=0.60, ge=0, le=1)
    choice_min_margin: float = Field(default=0.15, ge=0, le=1)
    choice_min_provider_confidence: float = Field(default=0.60, ge=0, le=1)
    relevance_threshold: float = Field(default=0.65, ge=0, le=1)
    support_threshold: float = Field(default=0.85, ge=0, le=1)
    retrieval_mode: Literal["lexical", "dense", "hybrid"] = "hybrid"
    document_search_strategy: Literal["lexical", "dense", "hybrid"] = "dense"
    retrieval_candidate_k: int = Field(default=16, ge=1, le=64)
    retrieval_expanded_k: int = Field(default=32, ge=1, le=64)
    retrieval_rrf_k: int = Field(default=60, ge=1)
    evidence_batch_size: int = Field(default=8, ge=1, le=32)
    max_semantic_actions: int = Field(default=4, ge=1, le=32)
    deepjev_task: str = "grounded_explain"
    deepjev_db_path: Path = Path("data/deepjev.sqlite3")
    deepjev_runs_dir: Path = Path("runs")
    deepjev_vector_dir: Path = Path("data/vectors/gemini-embedding-2-3072-v2")
    project_root: Path = Field(default_factory=Path.cwd, exclude=True)

    @field_validator(
        "run_max_cost_usd",
        "embedding_query_input_type",
        "embedding_document_input_type",
        "proposer_reasoning_enabled",
        mode="before",
    )
    @classmethod
    def empty_optional(cls, value):
        return None if value == "" else value

    @field_validator(
        "vercel_ai_gateway_base_url",
        "openrouter_base_url",
        "typesafe_base_url",
        "openrouter_decisions_url",
    )
    @classmethod
    def secure_url(cls, value: str) -> str:
        url = urlsplit(value)
        if (
            url.scheme != "https"
            or not url.hostname
            or url.username
            or url.password
            or url.query
            or url.fragment
        ):
            raise ValueError("base URL must be HTTPS without credentials, query or fragment")
        return value.rstrip("/")

    def context(self) -> CallContext:
        return CallContext.with_timeout(
            self.run_deadline_seconds,
            max_http_attempts=self.run_max_http_attempts,
            max_decision_calls=self.run_max_jev_calls,
            max_embedding_calls=self.run_max_embedding_calls,
            max_generation_calls=self.run_max_generation_calls,
            max_semantic_proposer_calls=self.run_max_semantic_proposer_calls,
            max_surface_proposer_calls=self.run_max_surface_proposer_calls,
            max_schema_repairs=self.run_max_schema_repairs,
            max_cost_usd=self.run_max_cost_usd,
        )

    def resolve_path(self, path: Path) -> Path:
        return path if path.is_absolute() else self.project_root / path


def load_settings(root: Path, *, env_file: Path | None = None, **overrides) -> Settings:
    root = root.resolve()
    path = env_file if env_file is not None else root / ".env"
    if not path.is_absolute():
        path = root / path
    if env_file is not None and not path.is_file():
        raise ConfigurationError("지정한 환경설정 파일이 없습니다.")
    try:
        return Settings(_env_file=path, project_root=root, **overrides)
    except ValidationError as exc:
        # Pydantic's .errors() contains input values: expose field names only.
        fields = sorted({str(error["loc"][0]) for error in exc.errors()})
        raise ConfigurationError("환경설정 값 확인 필요: " + ", ".join(fields)) from None
