import asyncio
import hashlib
import hmac
import json
import logging
import time
from datetime import datetime
from typing import Any, Dict, Optional, Tuple
from urllib.parse import parse_qsl

from aiogram import Bot, F, Router, types
from aiohttp import web
from pydantic import Field, field_validator
from pydantic_settings import SettingsConfigDict
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import sessionmaker

from bot.middlewares.i18n import JsonI18n
from bot.services.referral_service import ReferralService
from bot.services.subscription_service import SubscriptionService
from bot.utils.request_security import ip_in_allowlist, request_client_ip
from config.settings import Settings
from config.tariffs_config import (
    default_currency_key_for_settings,
    default_payment_currency_code_for_settings,
)
from db.dal import payment_dal

from .base import (
    PaymentProviderSpec,
    ProviderEnvConfig,
    ProviderManifestField,
    ServiceFactoryContext,
    WebAppPaymentContext,
    normalize_payment_currency_code,
    provider_env_file,
    provider_runtime_enabled,
)
from .shared import (
    HttpClientMixin,
    PaymentSuccessRequest,
    build_payment_record_payload,
    create_webapp_payment_record,
    decimal_amounts_equal,
    describe_payment,
    finalize_successful_payment,
    finalize_webapp_link_payment,
    first_value,
    format_decimal_amount,
    make_translator,
    notify_callback_parse_error,
    notify_payment_record_failure,
    notify_service_unavailable,
    parse_payment_callback,
    payment_failed,
    payment_unavailable,
    payment_units_for_activation,
    post_json_request,
    quote_hwid_callback_parts,
    render_link_or_fail,
)

_LOG = "freekassa"
FREEKASSA_SUPPORTED_CURRENCIES = ("RUB", "USD", "EUR", "UAH", "KZT")


class FreeKassaConfig(ProviderEnvConfig):
    model_config = SettingsConfigDict(
        env_file=provider_env_file(),
        env_file_encoding="utf-8",
        env_prefix="FREEKASSA_",
        extra="ignore",
    )

    ENABLED: bool = Field(default=False)
    MERCHANT_ID: Optional[str] = None
    FIRST_SECRET: Optional[str] = None
    SECOND_SECRET: Optional[str] = None
    PAYMENT_URL: str = Field(default="https://pay.freekassa.ru/")
    API_KEY: Optional[str] = None
    PAYMENT_IP: Optional[str] = None
    PAYMENT_METHOD_ID: Optional[int] = None
    TRUSTED_IPS: str = Field(default="168.119.157.136,168.119.60.227,178.154.197.79,51.250.54.238")

    @field_validator("PAYMENT_METHOD_ID", mode="before")
    @classmethod
    def _empty_to_none_int(cls, v):
        if isinstance(v, str):
            v = v.strip()
            if not v:
                return None
        return v

    @field_validator(
        "MERCHANT_ID",
        "FIRST_SECRET",
        "SECOND_SECRET",
        "API_KEY",
        "PAYMENT_IP",
        mode="before",
    )
    @classmethod
    def _strip_optional(cls, v):
        if isinstance(v, str) and not v.strip():
            return None
        return v

    @property
    def webhook_path(self) -> str:
        return "/webhook/freekassa"

    @property
    def trusted_ips_list(self) -> list:
        return [item.strip() for item in (self.TRUSTED_IPS or "").split(",") if item.strip()]


class FreeKassaPresentation(ProviderEnvConfig):
    model_config = SettingsConfigDict(
        env_file=provider_env_file(),
        env_file_encoding="utf-8",
        env_prefix="PAYMENT_FREEKASSA_",
        extra="ignore",
    )

    WEBAPP_LABEL_RU: Optional[str] = None
    WEBAPP_LABEL_EN: Optional[str] = None
    WEBAPP_ICON: Optional[str] = None
    TELEGRAM_LABEL_RU: Optional[str] = None
    TELEGRAM_LABEL_EN: Optional[str] = None
    TELEGRAM_EMOJI: Optional[str] = None


class FreeKassaService(HttpClientMixin):
    def __init__(
        self,
        *,
        bot: Bot,
        settings: Settings,
        config: FreeKassaConfig,
        i18n: JsonI18n,
        async_session_factory: sessionmaker,
        subscription_service: SubscriptionService,
        referral_service: ReferralService,
    ):
        self.bot = bot
        self.settings = settings
        self.config = config
        self.i18n = i18n
        self.async_session_factory = async_session_factory
        self.subscription_service = subscription_service
        self.referral_service = referral_service

        self.default_currency: str = default_payment_currency_code_for_settings(settings).upper()

        self.api_base_url: str = "https://api.fk.life/v1"
        self._init_http_client(total_timeout=15)
        self._nonce_lock = asyncio.Lock()
        self._last_nonce = int(time.time() * 1000)

        if not self.configured:
            logging.warning(
                "FreeKassaService initialized but not fully configured. Payments disabled."
            )
        if provider_runtime_enabled(config) and not self.server_ip:
            logging.warning(
                "FreeKassaService: FREEKASSA_PAYMENT_IP is not set. Requests may be rejected by the provider."  # noqa: E501
            )

    @property
    def configured(self) -> bool:
        return bool(provider_runtime_enabled(self.config) and self.shop_id and self.api_key)

    @property
    def shop_id(self):
        return self.config.MERCHANT_ID

    @property
    def api_key(self):
        return self.config.API_KEY

    @property
    def second_secret(self):
        return self.config.SECOND_SECRET

    @property
    def server_ip(self):
        return self.config.PAYMENT_IP

    @property
    def payment_method_id(self):
        return self.config.PAYMENT_METHOD_ID

    async def create_order(
        self,
        *,
        payment_db_id: int,
        user_id: int,
        months: int,
        amount: float,
        currency: Optional[str],
        email: Optional[str] = None,
        ip_address: Optional[str] = None,
        payment_method_id: Optional[int] = None,
        extra_params: Optional[Dict[str, Any]] = None,
    ) -> Tuple[bool, Dict[str, Any]]:
        if not self.configured:
            logging.error("FreeKassaService is not configured. Cannot create order.")
            return False, {"message": "service_not_configured"}

        ip_address = ip_address or self.server_ip
        if not ip_address:
            logging.error("FreeKassaService: payment IP is required but not configured.")
            return False, {"message": "missing_ip"}

        email = email or f"{user_id}@telegram.org"
        currency_code = normalize_payment_currency_code(currency or self.default_currency or "RUB")
        if currency_code not in FREEKASSA_SUPPORTED_CURRENCIES:
            return False, {
                "message": "unsupported_currency",
                "currency": currency_code,
                "supported_currencies": list(FREEKASSA_SUPPORTED_CURRENCIES),
            }

        payload: Dict[str, Any] = {
            "shopId": int(self.shop_id),
            "nonce": await self._generate_nonce(),
            "paymentId": str(payment_db_id),
            "i": int(payment_method_id),
            "amount": f"{format_decimal_amount(amount):.2f}",
            "currency": currency_code,
            "email": email,
            "ip": ip_address,
            "us_user_id": str(user_id),
            "us_months": str(months),
            "us_payment_db_id": str(payment_db_id),
        }

        if extra_params:
            for key, value in extra_params.items():
                if value is None:
                    continue
                payload[key] = value

        payload["signature"] = self._sign_payload(payload)

        session = await self._get_session()
        return await post_json_request(
            session,
            f"{self.api_base_url}/orders/create",
            body=payload,
            log_prefix="FreeKassa create_order",
            # FreeKassa returns ``{"type": "success", ...}`` on success.
            is_success=lambda status, data: status == 200 and (data or {}).get("type") == "success",
        )

    async def _generate_nonce(self) -> int:
        async with self._nonce_lock:
            candidate = int(time.time() * 1000)
            if candidate <= self._last_nonce:
                candidate = self._last_nonce + 1
            self._last_nonce = candidate
            return candidate

    def _sign_payload(self, payload: Dict[str, Any]) -> str:
        if not self.api_key:
            raise RuntimeError("FreeKassa API key is not configured.")
        items = [
            (key, value)
            for key, value in payload.items()
            if key != "signature" and value is not None
        ]
        items.sort(key=lambda pair: pair[0])
        message = "|".join(str(value) for _, value in items)
        return hmac.new(
            self.api_key.encode("utf-8"), message.encode("utf-8"), hashlib.sha256
        ).hexdigest()

    def _validate_signature(self, raw_body: bytes, provided_signature: str) -> bool:
        if not provided_signature or not self.second_secret:
            return False
        expected_signature = hmac.new(
            self.second_secret.encode("utf-8"),
            raw_body,
            hashlib.sha256,
        ).hexdigest()
        return hmac.compare_digest(expected_signature, provided_signature)

    async def webhook_route(self, request: web.Request) -> web.Response:
        if not self.configured:
            return web.Response(status=503, text="freekassa_disabled")

        try:
            client_ip = request_client_ip(request, trusted_proxies=self.settings.trusted_proxies)
            trusted = self.config.trusted_ips_list
            if not ip_in_allowlist(client_ip, trusted):
                logging.warning(
                    "FreeKassa webhook denied from unauthorized IP source "
                    "(client_ip=%s remote=%s x_forwarded_for=%s trusted_ips=%s "
                    "trusted_proxies=%s).",
                    client_ip,
                    request.remote,
                    request.headers.get("X-Forwarded-For"),
                    trusted,
                    self.settings.trusted_proxies,
                )
                return web.Response(status=403)

            raw_body = await request.read()
        except Exception:
            logging.exception("FreeKassa webhook: failed to read request body.")
            return web.Response(status=400, text="bad_request")

        payload_dict: Dict[str, Any] = {}
        if raw_body:
            try:
                if request.content_type.startswith("application/json"):
                    decoded_json = json.loads(raw_body.decode("utf-8"))
                    if isinstance(decoded_json, dict):
                        payload_dict = {str(k): v for k, v in decoded_json.items()}
                else:
                    payload_dict = {
                        str(key): value
                        for key, value in parse_qsl(
                            raw_body.decode("utf-8"), keep_blank_values=True
                        )
                    }
            except Exception:
                payload_dict = {}

        def _get(key: str, default: Optional[str] = None) -> Optional[str]:
            return payload_dict.get(key) or payload_dict.get(key.lower()) or default

        merchant_id = _get("MERCHANT_ID")
        if merchant_id != self.shop_id:
            return web.Response(status=403)

        signature = _get("SIGN") or _get("signature")
        if not signature:
            return web.Response(status=400, text="missing_signature")

        order_id_str = _get("MERCHANT_ORDER_ID") or _get("ORDER_ID") or _get("o")
        amount_str = _get("AMOUNT") or _get("OA") or _get("amount")
        provider_payment_id = _get("intid") or _get("payment_id") or _get("transaction_id")

        if not order_id_str or not amount_str:
            return web.Response(status=400, text="missing_data")

        if not self._validate_signature(raw_body, signature):
            return web.Response(status=403, text="invalid_signature")

        try:
            payment_db_id = int(order_id_str)
        except (TypeError, ValueError):
            logging.error("FreeKassa webhook: invalid order_id value %r", order_id_str)
            return web.Response(status=400, text="invalid_order_id")

        async with self.async_session_factory() as session:
            payment = await payment_dal.get_payment_by_db_id(session, payment_db_id)
            if not payment:
                logging.error("FreeKassa webhook: payment %s not found", payment_db_id)
                return web.Response(status=404, text="payment_not_found")

            if payment.status == "succeeded":
                logging.info("FreeKassa webhook: payment %s already succeeded", payment_db_id)
                return web.Response(text="YES")

            try:
                if not decimal_amounts_equal(amount_str, payment.amount):
                    logging.warning(
                        "FreeKassa webhook: amount mismatch for payment %s (expected %s, got %s)",
                        payment_db_id,
                        format_decimal_amount(payment.amount),
                        format_decimal_amount(amount_str),
                    )
            except Exception as exc:
                logging.warning(
                    "FreeKassa webhook: failed to compare amount for payment %s: %s",
                    payment_db_id,
                    exc,
                )

            resolved_provider_id = str(provider_payment_id or f"freekassa:{order_id_str}")
            try:
                await payment_dal.update_provider_payment_and_status(
                    session=session,
                    payment_db_id=payment.payment_id,
                    provider_payment_id=resolved_provider_id,
                    new_status="succeeded",
                )
                await session.commit()
            except Exception:
                await session.rollback()
                logging.exception(
                    "FreeKassa webhook: failed to mark payment %s as succeeded.", payment_db_id
                )
                return web.Response(status=500, text="processing_error")

            sale_mode = payment.sale_mode or (
                "traffic" if self.settings.traffic_sale_mode else "subscription"
            )
            months = payment_units_for_activation(payment, sale_mode)

            success_prefix: Optional[str] = None
            if provider_payment_id:
                # FreeKassa-specific: prepend "Order N from YYYY-MM-DD" to the success text.
                # ``i18n`` is resolved inside ``finalize_successful_payment`` per user language,
                # so the prefix is built in the admin/default language too — kept here for parity
                # with the original behavior, which used the same key.
                admin_lang = self.settings.DEFAULT_LANGUAGE
                translator = make_translator(self.i18n, admin_lang)
                success_prefix = translator(
                    "free_kassa_order_full",
                    order_id=provider_payment_id,
                    date=datetime.now().strftime("%Y-%m-%d"),
                )

            outcome = await finalize_successful_payment(
                PaymentSuccessRequest(
                    bot=self.bot,
                    settings=self.settings,
                    i18n=self.i18n,
                    session=session,
                    subscription_service=self.subscription_service,
                    referral_service=self.referral_service,
                    payment=payment,
                    user_id=payment.user_id,
                    amount=float(payment.amount),
                    currency=self.default_currency,
                    sale_mode=sale_mode,
                    months=months,
                    traffic_amount=float(months),
                    provider_subscription="freekassa",
                    provider_notification="freekassa",
                    db_user=payment.user,
                    log_prefix="FreeKassa webhook",
                    text_prefix=success_prefix,
                )
            )
            if outcome is None:
                return web.Response(status=500, text="processing_error")

        return web.Response(text="YES")


async def freekassa_webhook_route(request: web.Request) -> web.Response:
    service: FreeKassaService = request.app["freekassa_service"]
    return await service.webhook_route(request)


router = Router(name="user_subscription_payments_freekassa_router")


@router.callback_query(F.data.startswith("pay_fk:"))
async def pay_fk_callback_handler(
    callback: types.CallbackQuery,
    settings: Settings,
    i18n_data: dict,
    freekassa_service: FreeKassaService,
    session: AsyncSession,
):
    current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
    i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
    translator = make_translator(i18n, current_lang)

    if not i18n or not callback.message:
        await notify_callback_parse_error(callback, translator)
        return

    if not SPEC.is_available_to_user(
        settings,
        user_id=callback.from_user.id,
        require_configured=False,
    ):
        await notify_service_unavailable(callback, translator)
        return

    if not freekassa_service or not freekassa_service.configured:
        logging.error("FreeKassa service is not configured or unavailable.")
        await notify_service_unavailable(callback, translator)
        return

    parts = parse_payment_callback(callback.data or "")
    if not parts:
        logging.error("Invalid pay_fk data in callback: %s", callback.data)
        await notify_callback_parse_error(callback, translator)
        return
    parts, hwid_quote = await quote_hwid_callback_parts(
        session=session,
        user_id=callback.from_user.id,
        parts=parts,
        subscription_service=freekassa_service.subscription_service,
        currency=default_currency_key_for_settings(settings),
    )
    if not parts:
        await notify_callback_parse_error(callback, translator)
        return

    currency_code = (
        getattr(freekassa_service, "default_currency", None)
        or default_payment_currency_code_for_settings(settings)
        or "RUB"
    )
    payment_description = describe_payment(translator, parts)
    record_payload = build_payment_record_payload(
        user_id=callback.from_user.id,
        amount=parts.price,
        currency=currency_code,
        status="pending_freekassa",
        description=payment_description,
        months=parts.months,
        provider="freekassa",
        sale_mode=parts.sale_mode,
        hwid_quote=hwid_quote,
    )

    try:
        payment_record = await payment_dal.create_payment_record(session, record_payload)
        await session.commit()
    except Exception:
        await session.rollback()
        logging.exception(
            "FreeKassa: failed to create payment record for user %s.", callback.from_user.id
        )
        await notify_payment_record_failure(callback, translator)
        return

    success, response_data = await freekassa_service.create_order(
        payment_db_id=payment_record.payment_id,
        user_id=payment_record.user_id,
        months=parts.months,
        amount=parts.price,
        currency=freekassa_service.default_currency,
        payment_method_id=freekassa_service.payment_method_id,
        ip_address=freekassa_service.server_ip,
        extra_params={
            "us_method": freekassa_service.payment_method_id,
        },
    )

    location = first_value(response_data, "location")
    provider_identifier = first_value(response_data, "orderHash", "orderId")
    lead_text: Optional[str] = None
    if success and location:
        order_id_display = (
            first_value(response_data, "orderId")
            or provider_identifier
            or str(payment_record.payment_id)
        )
        lead_text = translator(
            "free_kassa_order_info",
            order_id=order_id_display,
            date=datetime.now().strftime("%Y-%m-%d"),
        )

    await render_link_or_fail(
        callback,
        translator=translator,
        current_lang=current_lang,
        i18n=i18n,
        parts=parts,
        session=session,
        payment=payment_record,
        api_success=success,
        payment_url=location,
        provider_payment_id=provider_identifier,
        lead_text=lead_text,
        log_prefix=_LOG,
    )


def create_service(ctx: ServiceFactoryContext) -> FreeKassaService:
    bundle = ctx.config_for("freekassa_service")
    config = (
        bundle.config
        if bundle and isinstance(bundle.config, FreeKassaConfig)
        else FreeKassaConfig()
    )
    return FreeKassaService(
        bot=ctx.bot,
        settings=ctx.settings,
        config=config,
        i18n=ctx.i18n,
        async_session_factory=ctx.async_session_factory,
        subscription_service=ctx.subscription_service,
        referral_service=ctx.referral_service,
    )


async def create_webapp_payment(ctx: WebAppPaymentContext) -> web.Response:
    service: FreeKassaService = ctx.request.app["freekassa_service"]
    if not service or not service.configured or not service.payment_method_id:
        return payment_unavailable()
    currency = ctx.currency or service.default_currency

    try:
        payment = await create_webapp_payment_record(
            ctx,
            amount=ctx.price,
            currency=currency,
            status="pending_freekassa",
            provider="freekassa",
        )
        success, response_data = await service.create_order(
            payment_db_id=payment.payment_id,
            user_id=ctx.user_id,
            months=ctx.months,
            amount=ctx.price,
            currency=currency,
            payment_method_id=service.payment_method_id,
            ip_address=service.server_ip,
            extra_params={"us_method": service.payment_method_id},
        )
    except Exception:
        await ctx.session.rollback()
        logging.exception("FreeKassa WebApp payment failed")
        return payment_failed()

    return await finalize_webapp_link_payment(
        session=ctx.session,
        payment=payment,
        api_success=success,
        payment_url=first_value(response_data, "location") if success else None,
        provider_payment_id=first_value(response_data, "orderHash", "orderId"),
        log_prefix="FreeKassa",
    )


_PRESENTATION_MANIFEST = tuple(
    ProviderManifestField(
        key=key,
        type=type_,
        label=label,
        description=description,
        placeholder=placeholder,
        subsection="FreeKassa",
        target="presentation",
        attr=attr,
    )
    for key, type_, label, description, placeholder, attr in (
        (
            "PAYMENT_FREEKASSA_WEBAPP_LABEL_RU",
            "string",
            "WebApp button text (RU)",
            "Custom Russian text shown in the Web App payment method button.",
            "",
            "WEBAPP_LABEL_RU",
        ),
        (
            "PAYMENT_FREEKASSA_WEBAPP_LABEL_EN",
            "string",
            "WebApp button text (EN)",
            "Custom English text shown in the Web App payment method button.",
            "",
            "WEBAPP_LABEL_EN",
        ),
        (
            "PAYMENT_FREEKASSA_WEBAPP_ICON",
            "icon",
            "WebApp button icon",
            "Lucide icon name rendered inside the Web App payment method button.",
            "Smartphone",
            "WEBAPP_ICON",
        ),
        (
            "PAYMENT_FREEKASSA_TELEGRAM_LABEL_RU",
            "string",
            "Telegram button text (RU)",
            "Custom Russian text shown in Telegram bot payment buttons.",
            "",
            "TELEGRAM_LABEL_RU",
        ),
        (
            "PAYMENT_FREEKASSA_TELEGRAM_LABEL_EN",
            "string",
            "Telegram button text (EN)",
            "Custom English text shown in Telegram bot payment buttons.",
            "",
            "TELEGRAM_LABEL_EN",
        ),
        (
            "PAYMENT_FREEKASSA_TELEGRAM_EMOJI",
            "string",
            "Telegram button emoji",
            "Emoji prepended to the Telegram bot payment button when customized.",
            "📱",
            "TELEGRAM_EMOJI",
        ),
    )
)

_CONFIG_MANIFEST = (
    ProviderManifestField(
        "FREEKASSA_ENABLED", "bool", "Включена", subsection="FreeKassa", attr="ENABLED"
    ),
    ProviderManifestField(
        "FREEKASSA_MERCHANT_ID", "string", "Merchant ID", subsection="FreeKassa", attr="MERCHANT_ID"
    ),
    ProviderManifestField(
        "FREEKASSA_FIRST_SECRET",
        "string",
        "First secret",
        subsection="FreeKassa",
        secret=True,
        attr="FIRST_SECRET",
    ),
    ProviderManifestField(
        "FREEKASSA_SECOND_SECRET",
        "string",
        "Second secret",
        subsection="FreeKassa",
        secret=True,
        attr="SECOND_SECRET",
    ),
    ProviderManifestField(
        "FREEKASSA_API_KEY",
        "string",
        "API key",
        subsection="FreeKassa",
        secret=True,
        attr="API_KEY",
    ),
    ProviderManifestField(
        "FREEKASSA_PAYMENT_URL",
        "url",
        "Payment URL",
        placeholder="https://pay.freekassa.ru/",
        subsection="FreeKassa",
        attr="PAYMENT_URL",
    ),
    ProviderManifestField(
        "FREEKASSA_PAYMENT_METHOD_ID",
        "int",
        "Payment method ID",
        description="See https://merchant.freekassa.net/settings/currencies",
        subsection="FreeKassa",
        attr="PAYMENT_METHOD_ID",
    ),
    ProviderManifestField(
        "FREEKASSA_PAYMENT_IP",
        "string",
        "Server IP",
        description="Public IP address reported to FreeKassa.",
        subsection="FreeKassa",
        attr="PAYMENT_IP",
    ),
    ProviderManifestField(
        "FREEKASSA_TRUSTED_IPS",
        "string",
        "Trusted IPs",
        description="Comma-separated IP addresses accepted for FreeKassa webhooks.",
        subsection="FreeKassa",
        attr="TRUSTED_IPS",
    ),
)


SPEC = PaymentProviderSpec(
    id="freekassa",
    provider_key="freekassa",
    label="FreeKassa",
    webapp_label="FreeKassa / СБП",
    webapp_labels={"ru": "FreeKassa / СБП", "en": "FreeKassa / SBP"},
    webapp_icon="Smartphone",
    telegram_labels={"ru": "СБП", "en": "SBP"},
    telegram_emoji="📱",
    pending_status="pending_freekassa",
    enabled=lambda config: bool(getattr(config, "ENABLED", False)),
    service_key="freekassa_service",
    callback_prefix="pay_fk",
    router=router,
    create_service=create_service,
    webhook_path=lambda source: "/webhook/freekassa",
    webhook_route=freekassa_webhook_route,
    create_webapp_payment=create_webapp_payment,
    config_class=FreeKassaConfig,
    presentation_class=FreeKassaPresentation,
    manifest_fields=_CONFIG_MANIFEST + _PRESENTATION_MANIFEST,
    supported_currencies=FREEKASSA_SUPPORTED_CURRENCIES,
    currency_support_note=(
        "FreeKassa SCI documents the payment currency parameter as RUB, USD, EUR, UAH or KZT."
    ),
    currency_support_url="https://docs.freekassa.net/",
)
