from __future__ import annotations

from typing import Any, Dict, Optional, Literal, Type, Mapping

from fsspec import AbstractFileSystem
from pydantic import (
    BaseModel,
    ConfigDict,
    Field,
    HttpUrl,
    PrivateAttr,
    SecretStr,
    ValidationError,
    field_validator,
    model_validator,
)

from sibi_flux.logger import Logger
from .utils import parse_iso_date

BackendName = Literal["sqlalchemy", "parquet", "http"]


class BackendParamsBase(BaseModel):
    """
    Strict, typed backend parameters.

    Changes:
    - extra="ignore": Allows passing a larger config dict (e.g. from a YAML file)
      without crashing on unrelated keys.
    """

    model_config = ConfigDict(
        extra="ignore",  # changed from 'forbid' to allow passing full env/config dicts
        frozen=True,
        arbitrary_types_allowed=True,
    )

    backend: BackendName
    debug: bool = False

    # Logger is transient and shouldn't be part of the model schema validation usually,
    # but we allow passing it in.
    logger: Optional[Logger] = Field(default=None, exclude=True)

    fs: Optional[AbstractFileSystem] = Field(default=None, exclude=True)
    field_map: Optional[Mapping[str, str]] = None
    logger_extra: Mapping[str, Any] = Field(
        default_factory=lambda: {"sibi_flux_component": __name__}
    )

    # Cache for the computed logger
    _logger_instance: Optional[Logger] = PrivateAttr(default=None)

    @property
    def log(self) -> Logger:
        """
        Lazy loader for the logger.
        This avoids mutation during validation, respecting frozen=True.
        """
        if self._logger_instance is None:
            # logic: use passed logger -> or create default
            lg = self.logger or Logger.default_logger(
                logger_name=self.__class__.__name__
            )
            lg.set_level(Logger.DEBUG if self.debug else Logger.INFO)
            # PrivateAttr can be set even on frozen models
            self._logger_instance = lg
        return self._logger_instance


class SqlAlchemyBackendParams(BackendParamsBase):
    backend: Literal["sqlalchemy"] = "sqlalchemy"

    connection_url: SecretStr = Field(..., description="Database DSN (masked in logs)")
    table: str = Field(min_length=1)  # Replaces custom validator for empty string
    legacy_filters: bool = False
    sticky_filters: Mapping[str, Any] = Field(default_factory=dict)

    @model_validator(mode="after")
    def _validate_dsn(self) -> "SqlAlchemyBackendParams":
        if not self.connection_url.get_secret_value().strip():
            raise ValueError("connection_url cannot be empty/whitespace.")
        return self


class ParquetBackendParams(BackendParamsBase):
    backend: Literal["parquet"] = "parquet"

    parquet_storage_path: str = Field(min_length=1)
    fs: AbstractFileSystem

    parquet_filename: Optional[str] = None
    parquet_start_date: Optional[str] = None
    parquet_end_date: Optional[str] = None
    parquet_max_age_minutes: int = Field(default=0, ge=0)  # Replaces custom validator
    partition_on: Optional[list[str]] = None
    load_parquet: bool = False

    @field_validator("parquet_filename")
    @classmethod
    def _clean_filename(cls, v: Optional[str]) -> Optional[str]:
        if v is None:
            return None
        return v.strip() or None

    @field_validator("partition_on")
    @classmethod
    def _validate_partition(cls, v: Optional[list[str]]) -> Optional[list[str]]:
        if v is None:
            return None
        # Remove empty strings
        cleaned = [x.strip() for x in v if x and x.strip()]
        if not cleaned:
            raise ValueError("partition_on cannot be empty if provided.")
        if len(cleaned) != len(set(cleaned)):
            raise ValueError("partition_on cannot contain duplicates.")
        return cleaned

    @model_validator(mode="after")
    def _validate_dates(self) -> "ParquetBackendParams":
        # Ensure utils.parse_iso_date is imported or available
        s_date = parse_iso_date(self.parquet_start_date, "parquet_start_date")
        e_date = parse_iso_date(self.parquet_end_date, "parquet_end_date")

        if s_date and e_date and e_date < s_date:
            raise ValueError("parquet_end_date must be >= parquet_start_date.")
        return self


class HttpBackendParams(BackendParamsBase):
    backend: Literal["http"] = "http"

    base_url: HttpUrl
    params: Dict[str, Any] = Field(default_factory=dict)
    timeout: Optional[int] = Field(default=300, gt=0)  # Replaces custom validator
    api_key: Optional[SecretStr] = None

    @model_validator(mode="after")
    def _validate_api_key(self) -> "HttpBackendParams":
        if self.api_key and not self.api_key.get_secret_value().strip():
            raise ValueError("api_key cannot be empty/whitespace if provided.")
        return self


def build_backend_params(kwargs: Mapping[str, Any], backend: str) -> BackendParamsBase:
    """
    Factory to build the correct params object.
    Leverages Pydantic's validation to filter kwargs automatically.
    """
    backend_norm = (backend or "sqlalchemy").strip().lower()

    mapping: Dict[str, Type[BackendParamsBase]] = {
        "sqlalchemy": SqlAlchemyBackendParams,
        "parquet": ParquetBackendParams,
        "http": HttpBackendParams,
    }

    model_cls = mapping.get(backend_norm)
    if not model_cls:
        raise ValueError(f"Unsupported backend: {backend!r}")

    # Inject normalized backend name back into kwargs to satisfy literal constraints
    # Copy kwargs to avoid side effects on the input dict
    data = dict(kwargs)
    data["backend"] = backend_norm

    try:
        # We use model_validate which handles parsing (e.g. casting types) better
        return model_cls.model_validate(data)
    except ValidationError as e:
        # Wrap error for cleaner upstream handling
        raise ValueError(f"Invalid configuration for {backend_norm}: {e}") from e
