from __future__ import annotations

import asyncio
import importlib.util
import logging
import os
import sys
from typing import Any

from app.core.config import settings


class SmsDeliveryError(Exception):
    pass


logger = logging.getLogger("uvicorn.error")


def _otp_dev_mode_enabled() -> bool:
    return bool(getattr(settings, "OTP_DEV_MODE", False))


def _is_automated_test_context() -> bool:
    if str(os.getenv("PYTEST_CURRENT_TEST", "")).strip():
        return True
    if str(os.getenv("UNITTEST_CURRENT_TEST", "")).strip():
        return True
    if str(os.getenv("AUTOTEST_MODE", "")).strip().lower() in {"1", "true", "yes", "on"}:
        return True
    app_env = str(getattr(settings, "APP_ENV", "") or "").strip().lower()
    if app_env in {"test", "ci"}:
        return True
    argv = " ".join(str(part) for part in sys.argv).lower()
    if "pytest" in argv or "unittest" in argv:
        return True
    return False


def _autotest_force_mock_enabled() -> bool:
    return bool(getattr(settings, "OTP_AUTOTEST_FORCE_MOCK_SMS", True)) and _is_automated_test_context()


def _module_available(module_name: str) -> bool:
    return importlib.util.find_spec(module_name) is not None


def _normalize_phone_to_int(phone: str) -> int:
    digits = "".join(ch for ch in str(phone or "") if ch.isdigit())
    if not digits:
        raise SmsDeliveryError("Некорректный номер телефона")
    try:
        return int(digits)
    except ValueError as exc:
        raise SmsDeliveryError("Некорректный номер телефона") from exc


def _build_otp_message(*, code: str, purpose: str, track_number: str | None) -> str:
    template = str(settings.OTP_SMS_TEMPLATE or "").strip() or "Ваш код подтверждения: {code}"
    try:
        rendered = template.format(code=code, purpose=purpose, track_number=track_number or "")
    except Exception:
        rendered = f"Ваш код подтверждения: {code}"
    return rendered


def _mock_sms_send(*, phone: str, code: str, purpose: str, track_number: str | None) -> dict[str, Any]:
    line = f"[OTP MOCK] purpose={purpose} phone={phone} track={track_number or '-'} code={code}"
    logger.warning(line)
    return {
        "provider": "mock_sms",
        "status": "accepted",
        "message": "SMS provider response mocked",
        "sent": False,
        "mocked": True,
    }


async def _send_sms_aero_async(*, phone: int, message: str) -> dict[str, Any]:
    try:
        import smsaero
    except Exception as exc:  # pragma: no cover - runtime dependency branch
        raise SmsDeliveryError("Библиотека smsaero-api-async не установлена") from exc

    email = str(settings.SMSAERO_EMAIL or "").strip()
    api_key = str(settings.SMSAERO_API_KEY or "").strip()
    if not email or not api_key:
        raise SmsDeliveryError("Не заданы SMSAERO_EMAIL и/или SMSAERO_API_KEY")

    api = smsaero.SmsAero(email, api_key)
    try:
        result = await api.send_sms(phone, message)
    except Exception as exc:  # pragma: no cover - network/runtime branch
        raise SmsDeliveryError(f"Ошибка отправки SMS через SMS Aero: {exc}") from exc
    finally:
        await api.close_session()
    return {
        "provider": "smsaero",
        "status": "accepted",
        "message": "SMS отправлено",
        "sent": True,
        "response": result,
    }


def _send_sms_aero(*, phone: str, message: str) -> dict[str, Any]:
    phone_int = _normalize_phone_to_int(phone)
    return asyncio.run(_send_sms_aero_async(phone=phone_int, message=message))


async def _get_sms_aero_balance_async() -> dict[str, Any]:
    try:
        import smsaero
    except Exception as exc:  # pragma: no cover - runtime dependency branch
        raise SmsDeliveryError("Библиотека smsaero-api-async не установлена") from exc

    email = str(settings.SMSAERO_EMAIL or "").strip()
    api_key = str(settings.SMSAERO_API_KEY or "").strip()
    if not email or not api_key:
        raise SmsDeliveryError("Не заданы SMSAERO_EMAIL и/или SMSAERO_API_KEY")

    api = smsaero.SmsAero(email, api_key)
    try:
        result = await api.balance()
    except Exception as exc:  # pragma: no cover - network/runtime branch
        raise SmsDeliveryError(f"Ошибка получения баланса SMS Aero: {exc}") from exc
    finally:
        await api.close_session()
    return dict(result or {})


def _get_sms_aero_balance() -> tuple[float | None, dict[str, Any] | None, str | None]:
    try:
        raw = _get_sms_aero_balance_async()
        data = asyncio.run(raw)
        amount = data.get("balance")
        number = float(amount)
        return number, data, None
    except Exception as exc:
        return None, None, str(exc)


def sms_provider_health() -> dict[str, Any]:
    provider = str(settings.SMS_PROVIDER or "dummy").strip().lower()
    if _otp_dev_mode_enabled():
        return {
            "provider": provider or "dummy",
            "effective_provider": "mock_sms",
            "status": "ok",
            "mode": "mock",
            "dev_mode": True,
            "can_send": True,
            "balance_available": False,
            "balance_amount": None,
            "balance_currency": "RUB",
            "checks": {"otp_dev_mode": True},
            "issues": ["OTP_DEV_MODE включен: реальная SMS-рассылка отключена"],
        }
    if provider in {"", "dummy", "mock", "console"}:
        return {
            "provider": "dummy",
            "status": "ok",
            "mode": "mock",
            "dev_mode": False,
            "can_send": True,
            "balance_available": False,
            "balance_amount": None,
            "balance_currency": "RUB",
            "checks": {"mock_mode": True},
            "issues": [],
        }

    if provider in {"smsaero", "sms_aero"}:
        email = str(settings.SMSAERO_EMAIL or "").strip()
        api_key = str(settings.SMSAERO_API_KEY or "").strip()
        installed = _module_available("smsaero")
        checks = {
            "smsaero_installed": bool(installed),
            "email_configured": bool(email),
            "api_key_configured": bool(api_key),
        }
        issues: list[str] = []
        if not checks["smsaero_installed"]:
            issues.append("Не установлена библиотека smsaero-api-async")
        if not checks["email_configured"]:
            issues.append("Не задан SMSAERO_EMAIL")
        if not checks["api_key_configured"]:
            issues.append("Не задан SMSAERO_API_KEY")
        can_send = all(checks.values())
        balance_available = False
        balance_amount: float | None = None
        balance_raw: dict[str, Any] | None = None
        if can_send:
            amount, raw_balance, balance_error = _get_sms_aero_balance()
            if amount is None:
                issues.append(str(balance_error or "Не удалось получить баланс SMS Aero"))
            else:
                balance_available = True
                balance_amount = amount
                balance_raw = raw_balance
        return {
            "provider": "smsaero",
            "status": "ok" if can_send and balance_available else "degraded",
            "mode": "real",
            "dev_mode": False,
            "can_send": can_send,
            "balance_available": balance_available,
            "balance_amount": balance_amount,
            "balance_currency": "RUB",
            "balance_raw": balance_raw,
            "checks": checks,
            "issues": issues,
        }

    return {
        "provider": provider,
        "status": "error",
        "mode": "unknown",
        "dev_mode": False,
        "can_send": False,
        "balance_available": False,
        "balance_amount": None,
        "balance_currency": "RUB",
        "checks": {"provider_supported": False},
        "issues": [f"Неизвестный SMS_PROVIDER: {provider}"],
    }


def send_otp_message(*, phone: str, code: str, purpose: str, track_number: str | None = None) -> dict[str, Any]:
    provider = str(settings.SMS_PROVIDER or "dummy").strip().lower()

    if _otp_dev_mode_enabled():
        payload = _mock_sms_send(phone=phone, code=code, purpose=purpose, track_number=track_number)
        payload["dev_mode"] = True
        payload["debug_code"] = str(code)
        return payload

    # Safety rail: automated tests must never call paid SMS providers.
    if _autotest_force_mock_enabled() and provider in {"smsaero", "sms_aero"}:
        payload = _mock_sms_send(phone=phone, code=code, purpose=purpose, track_number=track_number)
        payload["autotest_forced_mock"] = True
        payload["debug_code"] = str(code)
        return payload

    if provider in {"", "dummy", "mock", "console"}:
        return _mock_sms_send(phone=phone, code=code, purpose=purpose, track_number=track_number)
    if provider in {"smsaero", "sms_aero"}:
        message = _build_otp_message(code=code, purpose=purpose, track_number=track_number)
        return _send_sms_aero(phone=phone, message=message)
    raise SmsDeliveryError(f"Неизвестный SMS_PROVIDER: {provider}")
