# @File: backend/services/sms_sender.py
# @Desc: Unified outbound SMS dispatcher.
#
# Selects the active SMS provider at call time using live API Hub
# credentials (DB > env precedence). Preference order:
#
#     1. Jawally (4jawaly) — preferred for KSA / MENA traffic.
#     2. Twilio            — international fallback.
#     3. Vonage            — secondary fallback.
#
# Providing `send_otp_sms()` as a dedicated helper keeps the MFA code path
# out of the raw provider dispatch logic so OTP-specific concerns (short
# bodies, sender override, Arabic locale) live in one place.
from __future__ import annotations

import logging
from dataclasses import dataclass
from typing import Optional

import httpx

from services.api_hub_credentials import resolve_credential

logger = logging.getLogger(__name__)


@dataclass
class SmsResult:
    """Lightweight return value for SMS sends."""

    ok: bool
    provider: str
    message: str
    raw_status: Optional[int] = None


# ---------- Jawally (4jawaly) ----------
async def _send_via_jawally(phone_e164: str, body: str, sender: Optional[str] = None) -> SmsResult:
    """Send SMS via 4jawaly using Basic-Auth + JSON send endpoint."""
    api_key = await resolve_credential("jawally", "JAWALLY_API_KEY")
    api_secret = await resolve_credential("jawally", "JAWALLY_API_SECRET")
    default_sender = await resolve_credential("jawally", "JAWALLY_SENDER") or "doiteasy"
    if not (api_key and api_secret):
        return SmsResult(ok=False, provider="jawally", message="Jawally credentials missing.")

    # 4jawaly expects numbers without the leading "+".
    number = phone_e164.lstrip("+")
    iso = "SA" if number.startswith("966") else ""
    payload = {
        "messages": [
            {
                "text": body,
                "numbers": [number],
                "sender": sender or default_sender,
            }
        ]
    }
    if iso:
        payload["messages"][0]["number_iso"] = iso

    try:
        async with httpx.AsyncClient(timeout=15.0, auth=(api_key, api_secret)) as client:
            resp = await client.post(
                "https://api-sms.4jawaly.com/api/v1/account/area/sms/send",
                json=payload,
                headers={
                    "Accept": "application/json",
                    "Content-Type": "application/json",
                    "Origin": "*",
                    "Accept-Language": "ar",
                },
            )
    except httpx.RequestError as exc:
        return SmsResult(ok=False, provider="jawally", message=f"Transport error: {exc}")

    ok = 200 <= resp.status_code < 300
    try:
        body_json = resp.json()
    except ValueError:
        body_json = {}
    # 4jawaly returns code == 200 on success within the JSON body.
    inner_code = body_json.get("code") if isinstance(body_json, dict) else None
    if ok and (inner_code in (200, None)):
        return SmsResult(ok=True, provider="jawally", message="Queued.", raw_status=resp.status_code)
    return SmsResult(
        ok=False,
        provider="jawally",
        message=str(body_json.get("message") if isinstance(body_json, dict) else f"HTTP {resp.status_code}"),
        raw_status=resp.status_code,
    )


# ---------- Twilio ----------
async def _send_via_twilio(phone_e164: str, body: str, sender: Optional[str] = None) -> SmsResult:
    sid = await resolve_credential("twilio", "TWILIO_ACCOUNT_SID")
    token = await resolve_credential("twilio", "TWILIO_AUTH_TOKEN")
    from_number = sender or await resolve_credential("twilio", "TWILIO_FROM_NUMBER")
    if not (sid and token and from_number):
        return SmsResult(ok=False, provider="twilio", message="Twilio credentials missing.")
    try:
        async with httpx.AsyncClient(timeout=15.0, auth=(sid, token)) as client:
            resp = await client.post(
                f"https://api.twilio.com/2010-04-01/Accounts/{sid}/Messages.json",
                data={"To": phone_e164, "From": from_number, "Body": body},
            )
    except httpx.RequestError as exc:
        return SmsResult(ok=False, provider="twilio", message=f"Transport error: {exc}")
    if 200 <= resp.status_code < 300:
        return SmsResult(ok=True, provider="twilio", message="Queued.", raw_status=resp.status_code)
    return SmsResult(ok=False, provider="twilio", message=f"HTTP {resp.status_code}", raw_status=resp.status_code)


# ---------- Vonage ----------
async def _send_via_vonage(phone_e164: str, body: str, sender: Optional[str] = None) -> SmsResult:
    key = await resolve_credential("vonage", "VONAGE_API_KEY")
    secret = await resolve_credential("vonage", "VONAGE_API_SECRET")
    if not (key and secret):
        return SmsResult(ok=False, provider="vonage", message="Vonage credentials missing.")
    try:
        async with httpx.AsyncClient(timeout=15.0) as client:
            resp = await client.post(
                "https://rest.nexmo.com/sms/json",
                data={
                    "api_key": key,
                    "api_secret": secret,
                    "to": phone_e164.lstrip("+"),
                    "from": sender or "doiteasy",
                    "text": body,
                },
            )
    except httpx.RequestError as exc:
        return SmsResult(ok=False, provider="vonage", message=f"Transport error: {exc}")
    if 200 <= resp.status_code < 300:
        return SmsResult(ok=True, provider="vonage", message="Queued.", raw_status=resp.status_code)
    return SmsResult(ok=False, provider="vonage", message=f"HTTP {resp.status_code}", raw_status=resp.status_code)


async def _has_jawally() -> bool:
    return bool(
        await resolve_credential("jawally", "JAWALLY_API_KEY")
        and await resolve_credential("jawally", "JAWALLY_API_SECRET")
    )


async def _has_twilio() -> bool:
    return bool(
        await resolve_credential("twilio", "TWILIO_ACCOUNT_SID")
        and await resolve_credential("twilio", "TWILIO_AUTH_TOKEN")
    )


async def _has_vonage() -> bool:
    return bool(
        await resolve_credential("vonage", "VONAGE_API_KEY")
        and await resolve_credential("vonage", "VONAGE_API_SECRET")
    )


async def send_sms(phone_e164: str, body: str, sender: Optional[str] = None) -> SmsResult:
    """Send a generic SMS via the first available provider.

    Providers are tried in preference order; if a provider is configured
    but fails at runtime we still return its failure rather than cascading
    silently — that way admins see the real error in the UI instead of a
    misleading "all providers unavailable".
    """
    if not phone_e164 or not body:
        return SmsResult(ok=False, provider="none", message="Phone and body are required.")

    if await _has_jawally():
        return await _send_via_jawally(phone_e164, body, sender)
    if await _has_twilio():
        return await _send_via_twilio(phone_e164, body, sender)
    if await _has_vonage():
        return await _send_via_vonage(phone_e164, body, sender)
    return SmsResult(ok=False, provider="none", message="No SMS provider configured.")


async def send_otp_sms(phone_e164: str, code: str, *, purpose: str = "login") -> SmsResult:
    """Send a one-time passcode SMS. Body intentionally short for carrier filters."""
    purpose_label = {
        "login": "Login",
        "enroll": "Verification",
        "reset": "Password reset",
    }.get(purpose, "Verification")
    body = f"Doiteasy {purpose_label} code: {code}. Valid for 10 minutes. Do not share."
    result = await send_sms(phone_e164, body)
    if not result.ok:
        logger.warning(
            "[sms] OTP send failed provider=%s phone=%s message=%s",
            result.provider,
            phone_e164,
            result.message,
        )
    return result