"""Fail-closed, side-effect-free Telegram production startup checks.

The adapter may call these checks before constructing its network application.
All expected identity values are optional for ordinary profiles; once supplied,
they become exact startup seals and a mismatch is fatal.
"""

from __future__ import annotations

import hashlib
import ipaddress
import json
from collections.abc import Mapping, Sequence
from dataclasses import dataclass
from typing import Protocol, TypeAlias
from urllib.parse import urlsplit

from gateway.config import GatewayPreflightError


JsonValue: TypeAlias = (
    str
    | int
    | float
    | bool
    | None
    | list["JsonValue"]
    | Mapping[str, "JsonValue"]
)
TopicInput: TypeAlias = (
    str | int | bool | None | Sequence[str | int | bool]
)


class TelegramConfig(Protocol):
    @property
    def extra(self) -> Mapping[str, JsonValue]: ...


class AuthenticatedTelegramBot(Protocol):
    id: int
    username: str


class TelegramIdentityAdapter(Protocol):
    @property
    def config(self) -> TelegramConfig: ...

    _diagnostic_production_bot_identity: AuthenticatedTelegramBot | None
    _diagnostic_production_bot_digest: str | None


@dataclass(frozen=True, slots=True)
class TelegramIdentityError(RuntimeError):
    reason: str

    def __str__(self) -> str:
        return self.reason


@dataclass(frozen=True, slots=True)
class ExpectedTelegramIdentity:
    """An exact Telegram bot identity sealed in production configuration."""

    bot_id: int
    username: str


def _production_preflight(
    extra: Mapping[str, JsonValue],
) -> Mapping[str, JsonValue]:
    value = extra.get("production_preflight", {})
    if not isinstance(value, Mapping):
        raise GatewayPreflightError("production_preflight configuration is invalid")
    return value


def expected_bot_identity(
    extra: Mapping[str, JsonValue],
) -> ExpectedTelegramIdentity | None:
    """Parse the optional expected bot ID and username as one atomic seal."""
    preflight = _production_preflight(extra)
    raw_id = preflight.get("expected_bot_id")
    raw_username = preflight.get("expected_bot_username")
    if raw_id is None and raw_username is None:
        return None
    if (
        isinstance(raw_id, bool)
        or not isinstance(raw_id, int)
        or raw_id <= 0
        or not isinstance(raw_username, str)
        or not raw_username.strip()
    ):
        raise GatewayPreflightError("expected bot identity is invalid")
    return ExpectedTelegramIdentity(raw_id, raw_username.strip().removeprefix("@").lower())


def pin_authenticated_bot_identity(
    adapter: TelegramIdentityAdapter, bot: AuthenticatedTelegramBot
) -> None:
    """Validate the authenticated bot against its seal and pin its digest once."""
    expected = expected_bot_identity(adapter.config.extra)
    bot_id = getattr(bot, "id", None)
    username = getattr(bot, "username", None)
    if isinstance(bot_id, bool) or not isinstance(bot_id, int) or bot_id <= 0:
        raise TelegramIdentityError("bot id is invalid")
    if not isinstance(username, str) or not username.strip():
        raise TelegramIdentityError("bot username is invalid")
    normalized_username = username.strip().removeprefix("@").lower()
    if expected is not None and bot_id != expected.bot_id:
        raise TelegramIdentityError("bot id mismatch")
    if expected is not None and normalized_username != expected.username:
        raise TelegramIdentityError("bot username mismatch")
    digest = hashlib.sha256(
        json.dumps(
            {"bot_id": bot_id, "username": normalized_username},
            sort_keys=True,
            separators=(",", ":"),
        ).encode("utf-8")
    ).hexdigest()
    existing = getattr(adapter, "_diagnostic_production_bot_digest", None)
    if existing is not None and existing != digest:
        raise TelegramIdentityError("bot identity digest changed")
    adapter._diagnostic_production_bot_identity = bot
    adapter._diagnostic_production_bot_digest = digest


def validate_candidate_package_identity(
    extra: Mapping[str, JsonValue], candidate: str
) -> None:
    """Require the loaded package candidate to match an optional startup seal."""
    expected = _production_preflight(extra).get("candidate_package_identity")
    if expected is None:
        return
    if (
        not isinstance(expected, str)
        or len(expected) != 64
        or any(character not in "0123456789abcdef" for character in expected)
    ):
        raise GatewayPreflightError("candidate package identity is invalid")
    if candidate != expected:
        raise GatewayPreflightError("candidate package identity mismatch")


def validate_loopback_api_overrides(extra: Mapping[str, JsonValue]) -> None:
    """Reject Telegram test API overrides unless every URL host is loopback."""
    for key in ("base_url", "base_file_url"):
        value = extra.get(key)
        if value in (None, ""):
            continue
        if not isinstance(value, str):
            raise GatewayPreflightError(f"Telegram {key} must be a loopback URL")
        parsed = urlsplit(value)
        try:
            address = ipaddress.ip_address(parsed.hostname or "")
        except ValueError as error:
            raise GatewayPreflightError(
                f"Telegram {key} must use a loopback address"
            ) from error
        if parsed.scheme not in {"http", "https"} or not address.is_loopback:
            raise GatewayPreflightError(
                f"Telegram {key} must use a loopback address"
            )


def validated_topic_ids(raw: TopicInput) -> tuple[int, ...]:
    """Parse a positive, duplicate-free Telegram topic allowlist."""
    if raw in (None, ""):
        return ()
    values = raw if isinstance(raw, (list, tuple)) else str(raw).split(",")
    topics: list[int] = []
    for value in values:
        if isinstance(value, bool):
            raise GatewayPreflightError("Telegram topic ID is invalid")
        try:
            topic = int(str(value).strip())
        except ValueError as error:
            raise GatewayPreflightError("Telegram topic ID is invalid") from error
        if topic <= 0 or topic in topics:
            raise GatewayPreflightError("Telegram topic IDs must be positive and unique")
        topics.append(topic)
    return tuple(topics)


__all__ = [
    "ExpectedTelegramIdentity",
    "TelegramIdentityError",
    "expected_bot_identity",
    "pin_authenticated_bot_identity",
    "validate_candidate_package_identity",
    "validate_loopback_api_overrides",
    "validated_topic_ids",
]
