import enum
import json
import sys
from pathlib import Path
from typing import Any, Optional, Union

import yaml
from pydantic import AnyUrl, Field, field_validator, model_validator
from pydantic_settings import (
    BaseSettings,
    PydanticBaseSettingsSource,
    SettingsConfigDict,
)
from typing_extensions import Self


class UvicornSettings(BaseSettings):
    model_config = SettingsConfigDict(
        env_prefix="UVICORN_", env_file=".env", extra="allow"
    )

    host: str = "0.0.0.0"
    port: int = 5001
    reload: bool = False
    root_path: str = ""
    proxy_headers: bool = True
    timeout_keep_alive: int = 60
    ssl_certfile: Optional[Path] = None
    ssl_keyfile: Optional[Path] = None
    ssl_keyfile_password: Optional[str] = None
    workers: Union[int, None] = None


class LogLevel(str, enum.Enum):
    WARNING = "WARNING"
    INFO = "INFO"
    DEBUG = "DEBUG"


class AsyncEngine(str, enum.Enum):
    LOCAL = "local"
    KFP = "kfp"
    RQ = "rq"


class YamlConfigSettingsSource(PydanticBaseSettingsSource):
    """
    A settings source that loads configuration from a YAML or JSON file.
    The file path is specified via the DOCLING_SERVE_CONFIG_FILE environment variable.
    """

    def get_field_value(self, field: Any, field_name: str) -> tuple[Any, str, bool]:
        # Not used in this implementation
        return None, "", False

    def __call__(self) -> dict[str, Any]:
        """Load configuration from YAML or JSON file if config_file is set."""
        import os

        # Check for config_file in environment variable
        config_path_str = os.environ.get("DOCLING_SERVE_CONFIG_FILE")
        if not config_path_str:
            return {}

        config_path = Path(config_path_str)
        if not config_path.exists():
            return {}

        try:
            with open(config_path) as f:
                if config_path.suffix in [".yaml", ".yml"]:
                    data = yaml.safe_load(f)
                elif config_path.suffix == ".json":
                    data = json.load(f)
                else:
                    return {}
            return data if isinstance(data, dict) else {}
        except Exception:
            return {}

    def __repr__(self) -> str:
        return f"{self.__class__.__name__}()"


class DoclingServeSettings(BaseSettings):
    model_config = SettingsConfigDict(
        env_prefix="DOCLING_SERVE_",
        env_file=".env",
        env_parse_none_str="",
        extra="allow",
    )

    # Config file support
    config_file: Optional[Path] = None

    enable_ui: bool = False
    api_host: str = "localhost"
    log_level: Optional[LogLevel] = None
    artifacts_path: Optional[Path] = None
    static_path: Optional[Path] = None
    scratch_path: Optional[Path] = None
    single_use_results: bool = True
    result_removal_delay: float = 300  # 5 minutes
    load_models_at_boot: bool = True
    options_cache_size: int = 2
    enable_remote_services: bool = False
    allow_external_plugins: bool = False
    allow_custom_vlm_config: bool = False
    allow_custom_picture_description_config: bool = False
    allow_custom_code_formula_config: bool = False
    show_version_info: bool = True
    enable_management_endpoints: bool = False

    api_key: str = ""

    max_document_timeout: float = 3_600 * 24 * 7  # 7 days
    max_num_pages: int = sys.maxsize
    max_file_size: int = sys.maxsize

    # Threading pipeline
    queue_max_size: Optional[int] = None
    ocr_batch_size: Optional[int] = None
    layout_batch_size: Optional[int] = None
    table_batch_size: Optional[int] = None
    batch_polling_interval_seconds: Optional[float] = None

    sync_poll_interval: int = 2  # seconds
    max_sync_wait: int = 120  # 2 minutes

    cors_origins: list[str] = ["*"]
    cors_methods: list[str] = ["*"]
    cors_headers: list[str] = ["*"]

    eng_kind: AsyncEngine = AsyncEngine.LOCAL
    # Local engine
    eng_loc_num_workers: int = 2
    eng_loc_share_models: bool = False
    # RQ engine
    eng_rq_redis_url: str = ""
    eng_rq_results_prefix: str = "docling:results"
    eng_rq_sub_channel: str = "docling:updates"
    eng_rq_results_ttl: int = 3_600 * 4  # 4 hours default
    eng_rq_failure_ttl: int = 3_600 * 4  # 4 hours default
    eng_rq_redis_max_connections: int = 50  # Connection pool size
    eng_rq_redis_socket_timeout: Optional[float] = None  # Socket timeout in seconds
    eng_rq_redis_socket_connect_timeout: Optional[float] = (
        None  # Socket connect timeout in seconds
    )
    zombie_reaper_interval: float = 300.0
    zombie_reaper_max_age: float = 3600.0
    # KFP engine
    eng_kfp_endpoint: Optional[AnyUrl] = None
    eng_kfp_token: Optional[str] = None
    eng_kfp_ca_cert_path: Optional[str] = None
    eng_kfp_self_callback_endpoint: Optional[str] = None
    eng_kfp_self_callback_token_path: Optional[Path] = None
    eng_kfp_self_callback_ca_cert_path: Optional[Path] = None

    eng_kfp_experimental: bool = False

    # OpenTelemetry settings
    otel_enable_metrics: bool = True
    otel_enable_traces: bool = False
    otel_enable_prometheus: bool = True
    otel_enable_otlp_metrics: bool = False
    otel_service_name: str = "docling-serve"

    # Metrics
    metrics_port: Optional[int] = None

    # === DoclingConverterManagerConfig Parameters ===
    # TODO: Don't overwrite the default of docling-jobkit. This requires first some restructure in jobkit.

    # VLM Pipeline Control
    default_vlm_preset: str = "granite_docling"
    allowed_vlm_presets: Optional[list[str]] = None
    custom_vlm_presets: dict[str, Any] = Field(default_factory=dict)
    allowed_vlm_engines: Optional[list[str]] = None

    # Picture Description Control
    default_picture_description_preset: str = "smolvlm"
    allowed_picture_description_presets: Optional[list[str]] = None
    custom_picture_description_presets: dict[str, Any] = Field(default_factory=dict)
    allowed_picture_description_engines: Optional[list[str]] = None

    # Code/Formula Control
    default_code_formula_preset: str = "default"
    allowed_code_formula_presets: Optional[list[str]] = None
    custom_code_formula_presets: dict[str, Any] = Field(default_factory=dict)
    allowed_code_formula_engines: Optional[list[str]] = None

    # Table Structure Control
    default_table_structure_kind: str = "docling_tableformer"
    allowed_table_structure_kinds: Optional[list[str]] = None

    # Layout Control
    default_layout_kind: str = "docling_layout_default"
    allowed_layout_kinds: Optional[list[str]] = None

    @classmethod
    def settings_customise_sources(
        cls,
        settings_cls: type[BaseSettings],
        init_settings: PydanticBaseSettingsSource,
        env_settings: PydanticBaseSettingsSource,
        dotenv_settings: PydanticBaseSettingsSource,
        file_secret_settings: PydanticBaseSettingsSource,
    ) -> tuple[PydanticBaseSettingsSource, ...]:
        """
        Customize settings sources to include YAML/JSON config file support.
        Priority order: init > env > dotenv > yaml_config > file_secret
        """
        return (
            init_settings,
            env_settings,
            dotenv_settings,
            YamlConfigSettingsSource(settings_cls),
            file_secret_settings,
        )

    @field_validator(
        "custom_vlm_presets",
        "custom_picture_description_presets",
        "custom_code_formula_presets",
        mode="before",
    )
    @classmethod
    def parse_dict_from_json(cls, v: Any) -> dict[str, Any]:
        """Parse dict parameters from JSON-serialized ENV variables."""
        if v is None or v == "":
            return {}
        if isinstance(v, dict):
            return v
        if isinstance(v, str):
            try:
                parsed = json.loads(v)
                if isinstance(parsed, dict):
                    return parsed
                return {}
            except json.JSONDecodeError:
                return {}
        return {}

    @field_validator(
        "allowed_vlm_presets",
        "allowed_vlm_engines",
        "allowed_picture_description_presets",
        "allowed_picture_description_engines",
        "allowed_code_formula_presets",
        "allowed_code_formula_engines",
        "allowed_table_structure_kinds",
        "allowed_layout_kinds",
        mode="before",
    )
    @classmethod
    def parse_list_from_json_or_csv(cls, v: Any) -> Optional[list[str]]:
        """Parse list parameters from JSON arrays or comma-separated strings."""
        if v is None or v == "":
            return None
        if isinstance(v, list):
            return v
        if isinstance(v, str):
            # Try JSON first
            try:
                parsed = json.loads(v)
                if isinstance(parsed, list):
                    return [str(item) for item in parsed]
            except json.JSONDecodeError:
                pass
            # Fall back to comma-separated
            items = [item.strip() for item in v.split(",") if item.strip()]
            return items if items else None
        return None

    @field_validator("log_level", mode="before")
    @classmethod
    def validate_log_level(cls, v: Optional[str]) -> Optional[str]:
        """Validate and normalize log level to uppercase for case-insensitive support."""
        if v is None:
            return v
        if isinstance(v, str):
            return v.upper()
        return v

    @model_validator(mode="after")
    def engine_settings(self) -> Self:
        # Validate KFP engine settings
        if self.eng_kind == AsyncEngine.KFP:
            if self.eng_kfp_endpoint is None:
                raise ValueError("KFP endpoint is required when using the KFP engine.")

        if self.eng_kind == AsyncEngine.KFP:
            if not self.eng_kfp_experimental:
                raise ValueError(
                    "KFP is not yet working. To enable the development version, you must set DOCLING_SERVE_ENG_KFP_EXPERIMENTAL=true."
                )

        if self.eng_kind == AsyncEngine.RQ:
            if not self.eng_rq_redis_url:
                raise ValueError("RQ Redis url is required when using the RQ engine.")

        return self


uvicorn_settings = UvicornSettings()
docling_serve_settings = DoclingServeSettings()
