from __future__ import annotations

import logging
from dataclasses import dataclass
from typing import Optional

from aiogram import types
from sqlalchemy.ext.asyncio import AsyncSession

from bot.keyboards.inline.user_keyboards import (
    get_payment_url_keyboard,
    payment_methods_back_callback,
)
from bot.middlewares.i18n import JsonI18n
from db.dal import payment_dal
from db.models import Payment

from .common import (
    Translator,
    build_payment_description,
    format_human_units,
    mark_payment_failed_creation,
    parse_positive_int_units,
    sale_mode_base,
    sale_mode_is_hwid_devices,
    sale_mode_tariff_key,
)


@dataclass(frozen=True)
class PaymentCallbackParts:
    months: float
    price: float
    sale_mode: str

    @property
    def human_value(self) -> str:
        return format_human_units(self.months)

    @property
    def sale_base(self) -> str:
        return sale_mode_base(self.sale_mode)


def parse_payment_callback(callback_data: str) -> Optional[PaymentCallbackParts]:
    """Parse the ``<prefix>:<value>:<price>:<sale_mode>`` payload all providers use.

    Returns ``None`` if the payload doesn't have the expected shape — callers
    answer with ``error_try_again`` in that case.
    """
    try:
        _, data_payload = callback_data.split(":", 1)
        parts = data_payload.split(":")
        months = float(parts[0])
        price = float(parts[1])
        sale_mode = parts[2] if len(parts) > 2 else "subscription"
    except (ValueError, IndexError):
        return None
    return PaymentCallbackParts(months=months, price=price, sale_mode=sale_mode)


async def safe_callback_answer(
    callback: types.CallbackQuery,
    text: Optional[str] = None,
    *,
    show_alert: bool = False,
) -> None:
    """``callback.answer`` that never raises (Telegram occasionally 400s)."""
    try:
        if text is None:
            await callback.answer()
        else:
            await callback.answer(text, show_alert=show_alert)
    except Exception:
        pass


async def edit_or_answer(
    callback: types.CallbackQuery,
    text: str,
    *,
    reply_markup=None,
    disable_web_page_preview: bool = False,
    log_prefix: str = "payment_providers",
) -> None:
    """Edit the callback message if possible, else send a fresh reply."""
    if not callback.message:
        return
    try:
        await callback.message.edit_text(
            text,
            reply_markup=reply_markup,
            disable_web_page_preview=disable_web_page_preview,
        )
        return
    except Exception as exc:
        logging.warning("%s: failed to edit message (%s), sending new one.", log_prefix, exc)
    try:
        await callback.message.answer(
            text,
            reply_markup=reply_markup,
            disable_web_page_preview=disable_web_page_preview,
        )
    except Exception:
        pass


def describe_payment(translator: Translator, parts: PaymentCallbackParts) -> str:
    """Shortcut around ``build_payment_description`` for callback usage."""
    return build_payment_description(
        translator,
        months=parts.months,
        sale_mode=parts.sale_mode,
        human_value=parts.human_value,
    )


async def quote_hwid_callback_parts(
    *,
    session: AsyncSession,
    user_id: int,
    parts: PaymentCallbackParts,
    subscription_service,
    currency: str = "rub",
) -> tuple[Optional[PaymentCallbackParts], Optional[dict]]:
    if not sale_mode_is_hwid_devices(parts.sale_mode):
        return parts, None
    device_count = parse_positive_int_units(parts.months)
    if device_count is None:
        return None, None
    quote = await subscription_service.quote_hwid_device_topup(
        session,
        user_id=user_id,
        device_count=device_count,
        tariff_key=sale_mode_tariff_key(parts.sale_mode),
        renewal=sale_mode_base(parts.sale_mode) == "hwid_devices_renewal",
        currency=currency,
    )
    if not quote:
        return None, None
    quoted_parts = PaymentCallbackParts(
        months=device_count,
        price=float(quote.get("price") or 0),
        sale_mode=parts.sale_mode,
    )
    return quoted_parts, quote


def payment_link_message_text(
    translator: Translator,
    parts: PaymentCallbackParts,
    *,
    lead_text: Optional[str] = None,
) -> str:
    """Build the ``payment_link_message`` text (with optional lead block)."""
    traffic_like = sale_mode_base(parts.sale_mode) in {
        "traffic",
        "traffic_package",
        "topup",
        "premium_topup",
    }
    key = "payment_link_message_traffic" if traffic_like else "payment_link_message"
    body = translator(
        key,
        months=int(parts.months),
        traffic_gb=parts.human_value,
    )
    if lead_text:
        return f"{lead_text}\n\n{body}"
    return body


async def render_payment_link(
    callback: types.CallbackQuery,
    *,
    translator: Translator,
    current_lang: str,
    i18n: Optional[JsonI18n],
    parts: PaymentCallbackParts,
    payment_url: str,
    lead_text: Optional[str] = None,
    back_text_key: str = "back_to_payment_methods_button",
    log_prefix: str = "payment_providers",
) -> None:
    """Show the payment link with the standard back button and shared fallbacks."""
    text = payment_link_message_text(translator, parts, lead_text=lead_text)
    keyboard = get_payment_url_keyboard(
        payment_url,
        current_lang,
        i18n,
        back_callback=payment_methods_back_callback(
            parts.human_value, parts.sale_mode, parts.price
        ),
        back_text_key=back_text_key,
    )
    await edit_or_answer(
        callback,
        text,
        reply_markup=keyboard,
        log_prefix=log_prefix,
    )
    await safe_callback_answer(callback)


async def notify_service_unavailable(
    callback: types.CallbackQuery,
    translator: Translator,
) -> None:
    """Render the standard ``payment_service_unavailable`` UX."""
    await safe_callback_answer(
        callback,
        translator("payment_service_unavailable_alert"),
        show_alert=True,
    )
    if callback.message:
        try:
            await callback.message.edit_text(translator("payment_service_unavailable"))
        except Exception:
            pass


async def notify_callback_parse_error(
    callback: types.CallbackQuery,
    translator: Translator,
) -> None:
    """The 4-line "callback payload looked wrong" guard every provider repeats."""
    await safe_callback_answer(callback, translator("error_try_again"), show_alert=True)


async def notify_payment_record_failure(
    callback: types.CallbackQuery,
    translator: Translator,
) -> None:
    """Both error_creating_payment_record + error_try_again shown after DB failure."""
    if callback.message:
        try:
            await callback.message.edit_text(translator("error_creating_payment_record"))
        except Exception:
            pass
    await safe_callback_answer(callback, translator("error_try_again"), show_alert=True)


async def notify_payment_gateway_failure(
    callback: types.CallbackQuery,
    translator: Translator,
) -> None:
    """``error_payment_gateway`` shown both inline and as alert."""
    if callback.message:
        try:
            await callback.message.edit_text(translator("error_payment_gateway"))
        except Exception:
            pass
    await safe_callback_answer(
        callback,
        translator("error_payment_gateway"),
        show_alert=True,
    )


async def safe_store_provider_payment_id(
    session: AsyncSession,
    payment: Payment,
    *,
    provider_payment_id: str,
    new_status: Optional[str] = None,
    log_prefix: str,
) -> bool:
    """Persist ``(provider_payment_id, status)`` on the payment with rollback-on-fail.

    Returns True on success; logs and rolls back on failure. ``new_status``
    defaults to the payment's existing status (used after a successful API call
    that doesn't change the pending state).
    """
    try:
        await payment_dal.update_provider_payment_and_status(
            session,
            payment.payment_id,
            str(provider_payment_id),
            new_status or payment.status,
        )
        await session.commit()
        return True
    except Exception:
        await session.rollback()
        logging.exception(
            "%s: failed to store provider payment id for payment %s.",
            log_prefix,
            payment.payment_id,
        )
        return False


async def safe_mark_failed_creation(
    session: AsyncSession,
    payment: Payment,
    *,
    log_prefix: str,
) -> None:
    """Mark the payment as ``failed_creation``; swallow + log on failure."""
    try:
        await mark_payment_failed_creation(session, payment.payment_id)
    except Exception:
        await session.rollback()
        logging.exception(
            "%s: failed to mark payment %s as failed_creation.",
            log_prefix,
            payment.payment_id,
        )


async def render_link_or_fail(
    callback: types.CallbackQuery,
    *,
    translator: Translator,
    current_lang: str,
    i18n: Optional[JsonI18n],
    parts: "PaymentCallbackParts",
    session: AsyncSession,
    payment: Payment,
    api_success: bool,
    payment_url: Optional[str],
    provider_payment_id: Optional[str] = None,
    new_status: Optional[str] = None,
    lead_text: Optional[str] = None,
    log_prefix: str,
) -> None:
    """Finalize the link-based callback flow after the provider API responded.

    Persists the provider payment id (when one was returned), shows the
    payment link, or falls through to ``error_payment_gateway`` and marks the
    payment as ``failed_creation``. Every link-style provider used to inline
    this same sequence.
    """
    if api_success and provider_payment_id:
        await safe_store_provider_payment_id(
            session,
            payment,
            provider_payment_id=provider_payment_id,
            new_status=new_status,
            log_prefix=log_prefix,
        )

    if api_success and payment_url:
        await render_payment_link(
            callback,
            translator=translator,
            current_lang=current_lang,
            i18n=i18n,
            parts=parts,
            payment_url=payment_url,
            lead_text=lead_text,
            log_prefix=log_prefix,
        )
        return

    await safe_mark_failed_creation(session, payment, log_prefix=log_prefix)
    await notify_payment_gateway_failure(callback, translator)
