"""Abstract SMS provider base, registry, and routing logic.

All concrete SMS provider implementations must satisfy the ``AbstractSmsProvider``
protocol and be registered via ``register_provider()``.

Routing algorithm:
  1. Extract the E.164 dial code from the destination mobile number.
  2. Find all active providers that explicitly list this dial code in
     ``supported_country_codes`` (exact list of "+XX" strings).
     If a provider has an empty list it means "all countries".
  3. Among matched providers sort by priority (highest first), pick first.
  4. If no provider found, fall back to the first active "all-countries" provider.
  5. If still none → ``SmsProviderNotFound`` is raised.
"""

from __future__ import annotations

import re
import structlog
from dataclasses import dataclass, field
from decimal import Decimal
from typing import Protocol, runtime_checkable

_log = structlog.get_logger("simorgh.notifications.sms")


# ---------------------------------------------------------------------------
# Result dataclass
# ---------------------------------------------------------------------------

@dataclass
class SmsSendResult:
    """Outcome of a single SMS send attempt."""

    success: bool
    provider_message_id: str = ""
    cost: Decimal | None = None
    error: str = ""
    raw_response: dict = field(default_factory=dict)


# ---------------------------------------------------------------------------
# Provider protocol
# ---------------------------------------------------------------------------

@runtime_checkable
class AbstractSmsProvider(Protocol):
    """Minimal interface every SMS provider must implement."""

    #: unique machine-readable identifier, e.g. "sms_ir"
    slug: str
    #: human-readable name, e.g. "SMS.ir"
    name: str
    #: dial codes this provider handles; empty list means all countries
    supported_country_codes: list[str]
    #: higher = preferred when multiple providers match
    priority: int

    def send(
        self,
        *,
        mobile: str,
        text: str,
        tenant_id: int | None = None,
    ) -> SmsSendResult:
        """Send a plain-text SMS."""
        ...

    def send_otp(
        self,
        *,
        mobile: str,
        code: str,
        tenant_id: int | None = None,
        extra: dict | None = None,
    ) -> SmsSendResult:
        """Send an OTP/verification SMS using a provider template."""
        ...

    def supports_country_code(self, dial_code: str) -> bool:
        """Return True if this provider handles the given E.164 dial code."""
        ...


# ---------------------------------------------------------------------------
# Provider registry
# ---------------------------------------------------------------------------

_REGISTRY: dict[str, AbstractSmsProvider] = {}
_REGISTRY_LOADED: bool = False  # lazily set to True after first DB load


def _load_providers_from_db() -> None:
    """Populate the registry from the database (called lazily on first use).

    Importing here avoids circular imports at module load time and ensures
    we never query the DB during ``AppConfig.ready()``.
    """
    global _REGISTRY_LOADED
    if _REGISTRY_LOADED:
        return
    _REGISTRY_LOADED = True  # mark before queries to avoid recursion

    try:
        # Inline import to avoid circular dependency at module level
        from simorgh.apps.notifications.sms._loader import load_providers_into_registry
        load_providers_into_registry()
    except Exception as exc:
        _log.warning("sms.lazy_load_failed", reason=str(exc))


def register_provider(provider: AbstractSmsProvider) -> None:
    """Register an SMS provider implementation.

    Can be called multiple times with the same slug to update/replace the
    provider (useful for testing overrides).
    """
    _REGISTRY[provider.slug] = provider
    _log.info("sms.provider_registered", slug=provider.slug, name=provider.name)


def get_provider_by_slug(slug: str) -> AbstractSmsProvider:
    _ensure_providers_loaded()
    try:
        return _REGISTRY[slug]
    except KeyError as exc:
        raise LookupError(f"No SMS provider registered with slug {slug!r}") from exc


def get_all_providers() -> list[AbstractSmsProvider]:
    _ensure_providers_loaded()
    return list(_REGISTRY.values())


def _ensure_providers_loaded() -> None:
    """Ensure providers have been loaded from the DB (lazy, once per process)."""
    if not _REGISTRY_LOADED:
        _load_providers_from_db()


class SmsProviderNotFound(Exception):
    """Raised when no registered provider can handle the destination number."""


# ---------------------------------------------------------------------------
# Dial code extraction helpers
# ---------------------------------------------------------------------------

# Map of dial code prefix → E.164 dial code string
# Covers all ITU-T assigned codes; longer codes checked first to avoid false
# matches (e.g. "+1" should not match "+1868" which is Trinidad).
_DIAL_CODE_PREFIXES: list[tuple[str, str]] = sorted(
    [
        ("+1", "+1"),       # US / Canada / Caribbean (NANP)
        ("+7", "+7"),       # Russia / Kazakhstan
        ("+20", "+20"),     # Egypt
        ("+27", "+27"),     # South Africa
        ("+30", "+30"),     # Greece
        ("+31", "+31"),     # Netherlands
        ("+32", "+32"),     # Belgium
        ("+33", "+33"),     # France
        ("+34", "+34"),     # Spain
        ("+36", "+36"),     # Hungary
        ("+39", "+39"),     # Italy
        ("+40", "+40"),     # Romania
        ("+41", "+41"),     # Switzerland
        ("+43", "+43"),     # Austria
        ("+44", "+44"),     # UK
        ("+45", "+45"),     # Denmark
        ("+46", "+46"),     # Sweden
        ("+47", "+47"),     # Norway
        ("+48", "+48"),     # Poland
        ("+49", "+49"),     # Germany
        ("+51", "+51"),     # Peru
        ("+52", "+52"),     # Mexico
        ("+53", "+53"),     # Cuba
        ("+54", "+54"),     # Argentina
        ("+55", "+55"),     # Brazil
        ("+56", "+56"),     # Chile
        ("+57", "+57"),     # Colombia
        ("+58", "+58"),     # Venezuela
        ("+60", "+60"),     # Malaysia
        ("+61", "+61"),     # Australia
        ("+62", "+62"),     # Indonesia
        ("+63", "+63"),     # Philippines
        ("+64", "+64"),     # New Zealand
        ("+65", "+65"),     # Singapore
        ("+66", "+66"),     # Thailand
        ("+81", "+81"),     # Japan
        ("+82", "+82"),     # South Korea
        ("+84", "+84"),     # Vietnam
        ("+86", "+86"),     # China
        ("+90", "+90"),     # Turkey
        ("+91", "+91"),     # India
        ("+92", "+92"),     # Pakistan
        ("+93", "+93"),     # Afghanistan
        ("+94", "+94"),     # Sri Lanka
        ("+95", "+95"),     # Myanmar
        ("+98", "+98"),     # Iran  ← SMS.ir
        ("+212", "+212"),   # Morocco
        ("+213", "+213"),   # Algeria
        ("+216", "+216"),   # Tunisia
        ("+218", "+218"),   # Libya
        ("+220", "+220"),   # Gambia
        ("+221", "+221"),   # Senegal
        ("+222", "+222"),   # Mauritania
        ("+223", "+223"),   # Mali
        ("+224", "+224"),   # Guinea
        ("+225", "+225"),   # Ivory Coast
        ("+226", "+226"),   # Burkina Faso
        ("+227", "+227"),   # Niger
        ("+228", "+228"),   # Togo
        ("+229", "+229"),   # Benin
        ("+230", "+230"),   # Mauritius
        ("+231", "+231"),   # Liberia
        ("+232", "+232"),   # Sierra Leone
        ("+233", "+233"),   # Ghana
        ("+234", "+234"),   # Nigeria
        ("+235", "+235"),   # Chad
        ("+236", "+236"),   # Central African Republic
        ("+237", "+237"),   # Cameroon
        ("+238", "+238"),   # Cape Verde
        ("+239", "+239"),   # São Tomé and Príncipe
        ("+240", "+240"),   # Equatorial Guinea
        ("+241", "+241"),   # Gabon
        ("+242", "+242"),   # Congo
        ("+243", "+243"),   # DR Congo
        ("+244", "+244"),   # Angola
        ("+245", "+245"),   # Guinea-Bissau
        ("+246", "+246"),   # British Indian Ocean Territory
        ("+247", "+247"),   # Ascension Island
        ("+248", "+248"),   # Seychelles
        ("+249", "+249"),   # Sudan
        ("+250", "+250"),   # Rwanda
        ("+251", "+251"),   # Ethiopia
        ("+252", "+252"),   # Somalia
        ("+253", "+253"),   # Djibouti
        ("+254", "+254"),   # Kenya
        ("+255", "+255"),   # Tanzania
        ("+256", "+256"),   # Uganda
        ("+257", "+257"),   # Burundi
        ("+258", "+258"),   # Mozambique
        ("+260", "+260"),   # Zambia
        ("+261", "+261"),   # Madagascar
        ("+262", "+262"),   # Réunion
        ("+263", "+263"),   # Zimbabwe
        ("+264", "+264"),   # Namibia
        ("+265", "+265"),   # Malawi
        ("+266", "+266"),   # Lesotho
        ("+267", "+267"),   # Botswana
        ("+268", "+268"),   # Swaziland
        ("+269", "+269"),   # Comoros
        ("+291", "+291"),   # Eritrea
        ("+297", "+297"),   # Aruba
        ("+298", "+298"),   # Faroe Islands
        ("+299", "+299"),   # Greenland
        ("+350", "+350"),   # Gibraltar
        ("+351", "+351"),   # Portugal
        ("+352", "+352"),   # Luxembourg
        ("+353", "+353"),   # Ireland
        ("+354", "+354"),   # Iceland
        ("+355", "+355"),   # Albania
        ("+356", "+356"),   # Malta
        ("+357", "+357"),   # Cyprus
        ("+358", "+358"),   # Finland
        ("+359", "+359"),   # Bulgaria
        ("+370", "+370"),   # Lithuania
        ("+371", "+371"),   # Latvia
        ("+372", "+372"),   # Estonia
        ("+373", "+373"),   # Moldova
        ("+374", "+374"),   # Armenia
        ("+375", "+375"),   # Belarus
        ("+376", "+376"),   # Andorra
        ("+377", "+377"),   # Monaco
        ("+378", "+378"),   # San Marino
        ("+380", "+380"),   # Ukraine
        ("+381", "+381"),   # Serbia
        ("+382", "+382"),   # Montenegro
        ("+385", "+385"),   # Croatia
        ("+386", "+386"),   # Slovenia
        ("+387", "+387"),   # Bosnia and Herzegovina
        ("+389", "+389"),   # North Macedonia
        ("+420", "+420"),   # Czech Republic
        ("+421", "+421"),   # Slovakia
        ("+423", "+423"),   # Liechtenstein
        ("+500", "+500"),   # Falkland Islands
        ("+501", "+501"),   # Belize
        ("+502", "+502"),   # Guatemala
        ("+503", "+503"),   # El Salvador
        ("+504", "+504"),   # Honduras
        ("+505", "+505"),   # Nicaragua
        ("+506", "+506"),   # Costa Rica
        ("+507", "+507"),   # Panama
        ("+508", "+508"),   # Saint Pierre and Miquelon
        ("+509", "+509"),   # Haiti
        ("+590", "+590"),   # Guadeloupe
        ("+591", "+591"),   # Bolivia
        ("+592", "+592"),   # Guyana
        ("+593", "+593"),   # Ecuador
        ("+594", "+594"),   # French Guiana
        ("+595", "+595"),   # Paraguay
        ("+596", "+596"),   # Martinique
        ("+597", "+597"),   # Suriname
        ("+598", "+598"),   # Uruguay
        ("+599", "+599"),   # Netherlands Antilles
        ("+670", "+670"),   # East Timor
        ("+672", "+672"),   # Norfolk Island
        ("+673", "+673"),   # Brunei
        ("+674", "+674"),   # Nauru
        ("+675", "+675"),   # Papua New Guinea
        ("+676", "+676"),   # Tonga
        ("+677", "+677"),   # Solomon Islands
        ("+678", "+678"),   # Vanuatu
        ("+679", "+679"),   # Fiji
        ("+680", "+680"),   # Palau
        ("+682", "+682"),   # Cook Islands
        ("+685", "+685"),   # Samoa
        ("+686", "+686"),   # Kiribati
        ("+687", "+687"),   # New Caledonia
        ("+688", "+688"),   # Tuvalu
        ("+689", "+689"),   # French Polynesia
        ("+690", "+690"),   # Tokelau
        ("+691", "+691"),   # Micronesia
        ("+692", "+692"),   # Marshall Islands
        ("+850", "+850"),   # North Korea
        ("+852", "+852"),   # Hong Kong
        ("+853", "+853"),   # Macau
        ("+855", "+855"),   # Cambodia
        ("+856", "+856"),   # Laos
        ("+880", "+880"),   # Bangladesh
        ("+886", "+886"),   # Taiwan
        ("+960", "+960"),   # Maldives
        ("+961", "+961"),   # Lebanon
        ("+962", "+962"),   # Jordan
        ("+963", "+963"),   # Syria
        ("+964", "+964"),   # Iraq
        ("+965", "+965"),   # Kuwait
        ("+966", "+966"),   # Saudi Arabia
        ("+967", "+967"),   # Yemen
        ("+968", "+968"),   # Oman
        ("+970", "+970"),   # Palestinian Territory
        ("+971", "+971"),   # UAE
        ("+972", "+972"),   # Israel
        ("+973", "+973"),   # Bahrain
        ("+974", "+974"),   # Qatar
        ("+975", "+975"),   # Bhutan
        ("+976", "+976"),   # Mongolia
        ("+977", "+977"),   # Nepal
        ("+992", "+992"),   # Tajikistan
        ("+993", "+993"),   # Turkmenistan
        ("+994", "+994"),   # Azerbaijan
        ("+995", "+995"),   # Georgia
        ("+996", "+996"),   # Kyrgyzstan
        ("+998", "+998"),   # Uzbekistan
    ],
    key=lambda x: -len(x[0]),  # longest prefix first for accurate matching
)


def normalise_mobile(mobile: str) -> str:
    """Normalise a mobile number to E.164 format (e.g. '09123456789' → '+989123456789').

    Handles common Iranian local format (09xx) and international format.
    """
    mobile = re.sub(r"[\s\-().]", "", mobile)
    if mobile.startswith("00"):
        return "+" + mobile[2:]
    if mobile.startswith("0") and not mobile.startswith("+"):
        # Heuristic: assume Iran local format  09xxxxxxxxx → +989xxxxxxxxx
        return "+98" + mobile[1:]
    if not mobile.startswith("+"):
        return "+" + mobile
    return mobile


def extract_dial_code(mobile: str) -> str | None:
    """Return the ITU-T dial code for a normalised E.164 number, or None."""
    e164 = normalise_mobile(mobile)
    for prefix, code in _DIAL_CODE_PREFIXES:
        if e164.startswith(prefix):
            return code
    return None


# ---------------------------------------------------------------------------
# Provider routing
# ---------------------------------------------------------------------------

def get_provider_for_mobile(mobile: str) -> AbstractSmsProvider:
    """Return the highest-priority registered provider that handles *mobile*.

    Raises ``SmsProviderNotFound`` if no suitable provider is registered.
    """
    _ensure_providers_loaded()
    dial_code = extract_dial_code(mobile)

    candidates: list[AbstractSmsProvider] = []
    fallback: list[AbstractSmsProvider] = []

    for provider in _REGISTRY.values():
        codes = getattr(provider, "supported_country_codes", [])
        if codes:  # explicit country list
            if dial_code and dial_code in codes:
                candidates.append(provider)
        else:  # empty list = all countries
            fallback.append(provider)

    if candidates:
        return max(candidates, key=lambda p: getattr(p, "priority", 0))

    if fallback:
        return max(fallback, key=lambda p: getattr(p, "priority", 0))

    raise SmsProviderNotFound(
        f"No registered SMS provider can handle number {mobile!r} "
        f"(dial code: {dial_code!r}). "
        f"Registered providers: {list(_REGISTRY.keys())}"
    )
