from __future__ import annotations

import os
from collections.abc import Mapping
from dataclasses import dataclass
from pathlib import Path
from types import MappingProxyType

from dotenv import load_dotenv


class ConfigurationError(ValueError):
    """Raised when application configuration is missing or invalid."""


@dataclass(frozen=True, slots=True)
class Config:
    bargheman_token: str
    eitaayar_token: str
    bill_chat_ids: Mapping[str, tuple[str, ...]]
    state_file: Path
    lookahead_days: int = 5
    request_timeout_seconds: float = 15.0
    max_events_per_bill: int = 25

    @property
    def bill_ids(self) -> tuple[str, ...]:
        return tuple(self.bill_chat_ids)

    @classmethod
    def from_env(cls) -> Config:
        load_dotenv(override=False)

        values = {
            "BARGHEMAN_TOKEN": os.getenv("BARGHEMAN_TOKEN", "").strip(),
            "EITAAYAR_TOKEN": os.getenv("EITAAYAR_TOKEN", "").strip(),
        }
        missing = [name for name, value in values.items() if not value]

        raw_routes = os.getenv("BILL_CHAT_MAP", "").strip()
        if raw_routes:
            bill_chat_ids = _bill_chat_map(raw_routes)
        else:
            chat_id = os.getenv("CHAT_ID", "").strip()
            raw_bill_ids = os.getenv("BILL_IDS", "")
            bill_ids = tuple(
                dict.fromkeys(part.strip() for part in raw_bill_ids.split(",") if part.strip())
            )
            if not chat_id:
                missing.append("CHAT_ID")
            if not bill_ids:
                missing.append("BILL_IDS")
            elif invalid := [bill_id for bill_id in bill_ids if not bill_id.isdecimal()]:
                raise ConfigurationError(
                    f"BILL_IDS must contain only decimal IDs: {', '.join(invalid)}"
                )
            bill_chat_ids = {bill_id: (chat_id,) for bill_id in bill_ids}

        if missing:
            raise ConfigurationError(
                f"Missing required environment variables: {', '.join(missing)}"
            )

        return cls(
            bargheman_token=values["BARGHEMAN_TOKEN"],
            eitaayar_token=values["EITAAYAR_TOKEN"],
            bill_chat_ids=MappingProxyType(bill_chat_ids),
            state_file=Path(os.getenv("STATE_FILE", "state/outages.json").strip()),
            lookahead_days=_integer_setting("LOOKAHEAD_DAYS", 5, minimum=1, maximum=30),
            request_timeout_seconds=_float_setting(
                "REQUEST_TIMEOUT_SECONDS", 15.0, minimum=1.0, maximum=120.0
            ),
            max_events_per_bill=_integer_setting("MAX_EVENTS_PER_BILL", 25, minimum=1, maximum=100),
        )


def _bill_chat_map(raw_routes: str) -> dict[str, tuple[str, ...]]:
    routes: dict[str, list[str]] = {}
    malformed: list[str] = []
    for raw_route in raw_routes.split(","):
        route = raw_route.strip()
        if not route:
            continue
        bill_id, separator, chat_id = route.partition(":")
        bill_id = bill_id.strip()
        chat_id = chat_id.strip()
        if not separator or not bill_id or not chat_id:
            malformed.append(route)
            continue
        if not bill_id.isdecimal():
            raise ConfigurationError(f"BILL_CHAT_MAP bill IDs must be decimal: {bill_id}")
        chat_ids = routes.setdefault(bill_id, [])
        if chat_id not in chat_ids:
            chat_ids.append(chat_id)
    if malformed:
        raise ConfigurationError(
            "BILL_CHAT_MAP entries must use BILL_ID:CHAT_ID format: " + ", ".join(malformed)
        )
    if not routes:
        raise ConfigurationError("BILL_CHAT_MAP must contain at least one route")
    return {bill_id: tuple(chat_ids) for bill_id, chat_ids in routes.items()}


def _integer_setting(name: str, default: int, *, minimum: int, maximum: int) -> int:
    raw_value = os.getenv(name, str(default)).strip()
    try:
        value = int(raw_value)
    except ValueError as error:
        raise ConfigurationError(f"{name} must be an integer") from error
    if not minimum <= value <= maximum:
        raise ConfigurationError(f"{name} must be between {minimum} and {maximum}")
    return value


def _float_setting(name: str, default: float, *, minimum: float, maximum: float) -> float:
    raw_value = os.getenv(name, str(default)).strip()
    try:
        value = float(raw_value)
    except ValueError as error:
        raise ConfigurationError(f"{name} must be a number") from error
    if not minimum <= value <= maximum:
        raise ConfigurationError(f"{name} must be between {minimum:g} and {maximum:g}")
    return value
