import secrets
import uuid
from datetime import timedelta
from decimal import Decimal
from typing import Any

from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession

from app.core.exceptions import ConflictError, NotFoundError, PaymentError, ValidationError
from app.models import Order, OrderStatus, Payment, PaymentMethod, PaymentStatus, User
from app.repositories import PaymentRepository
from app.services.log_service import LogService, LogType
from app.services.order_service import OrderService
from app.services.settings_service import SettingsService
from app.services.wallet_service import TransactionType, WalletService
from app.utils.helpers import crypto, money, utcnow

MAX_UNIQUE_AMOUNT_ATTEMPTS = 40


class PaymentService:
    """One checkout for every product: wallet, TRON invoice or bank receipt."""

    def __init__(self, session: AsyncSession) -> None:
        self.session = session
        self.payments = PaymentRepository(session)
        self.orders = OrderService(session)
        self.wallets = WalletService(session)
        self.settings = SettingsService(session)
        self.logs = LogService(session)

    # -- helpers ----------------------------------------------------
    async def _timeout(self) -> timedelta:
        return timedelta(minutes=max(await self.settings.get_int("payment_timeout_minutes", 30), 1))

    async def _expire_active(self, order: Order) -> None:
        active = await self.payments.active_for_order(order.id)
        if active:
            active.status = PaymentStatus.EXPIRED
            await self.session.flush()

    def _ensure_payable(self, order: Order) -> None:
        if order.payment_status == PaymentStatus.PAID:
            raise ConflictError("order_already_paid")
        if order.status in (OrderStatus.CANCELLED, OrderStatus.COMPLETED, OrderStatus.REFUNDED):
            raise ConflictError("order_not_payable")
        if Decimal(order.final_price) <= 0:
            raise ValidationError("invalid_order_amount")

    # -- wallet -----------------------------------------------------
    async def pay_with_wallet(self, order: Order) -> Order:
        self._ensure_payable(order)
        if order.flow == "topup":
            raise PaymentError("wallet_not_allowed_for_topup")
        amount = money(order.final_price)

        if not await self.wallets.has_balance(order.user_id, amount):
            raise PaymentError("insufficient_balance")

        await self.orders.confirm(order)
        await self.wallets.withdraw(
            order.user_id,
            amount,
            description=f"purchase {order.order_number}",
            order_id=order.id,
            reference=f"order:{order.id}",
            tx_type=TransactionType.PURCHASE,
        )
        payment = await self.payments.create(
            order_id=order.id,
            method=PaymentMethod.WALLET,
            status=PaymentStatus.PAID,
            amount=amount,
            currency=order.currency,
            paid_at=utcnow(),
        )
        await self.logs.write(
            LogType.PAYMENT,
            "wallet_payment",
            f"order {order.order_number} paid from wallet",
            user_id=order.user_id,
            order_id=order.id,
            payment_id=payment.id,
            data={"amount": str(amount)},
        )
        await self.orders.mark_paid(order, PaymentMethod.WALLET)
        return await self._fulfil(order)

    # -- tron -------------------------------------------------------
    async def _unique_payable_amount(self, base_amount: Decimal) -> Decimal:
        """Add a tiny random offset until the amount is free among live invoices."""
        for attempt in range(MAX_UNIQUE_AMOUNT_ATTEMPTS):
            offset = Decimal(secrets.randbelow(9000) + 100) / Decimal("1000000")
            spread = Decimal(attempt // 10) / Decimal("1000")
            candidate = crypto(base_amount + offset + spread)
            if not await self.payments.payable_amount_taken(candidate):
                return candidate
        raise PaymentError("unique_amount_unavailable")

    async def create_tron_invoice(self, order: Order) -> Payment:
        self._ensure_payable(order)
        await self.orders.confirm(order)

        wallet_address = await self.settings.get_str("tron_wallet_address")
        if not wallet_address:
            raise PaymentError("tron_wallet_not_configured")

        rate = await self.settings.get_decimal("trx_rate_irt")
        if rate <= 0:
            raise PaymentError("trx_rate_not_configured")

        await self._expire_active(order)

        amount_irt = money(order.final_price)
        original_amount = crypto(amount_irt / rate)

        expires_at = utcnow() + await self._timeout()
        payment: Payment | None = None
        for _ in range(3):
            payable = await self._unique_payable_amount(original_amount)
            try:
                # savepoint: a lost race on the unique index must not kill the transaction
                async with self.session.begin_nested():
                    payment = await self.payments.create(
                        order_id=order.id,
                        method=PaymentMethod.TRON,
                        status=PaymentStatus.PENDING,
                        amount=amount_irt,
                        currency=order.currency,
                        original_amount=original_amount,
                        payable_amount=payable,
                        pay_currency="TRX",
                        wallet_address=wallet_address,
                        expires_at=expires_at,
                    )
                break
            except IntegrityError:
                payment = None
                continue

        if payment is None:
            raise PaymentError("unique_amount_unavailable")

        await self.logs.write(
            LogType.PAYMENT,
            "tron_invoice_created",
            f"invoice for order {order.order_number}",
            user_id=order.user_id,
            order_id=order.id,
            payment_id=payment.id,
            data={
                "original_amount": str(original_amount),
                "payable_amount": str(payable),
                "wallet": wallet_address,
            },
        )
        return payment

    async def confirm_tron_payment(self, payment: Payment, tx_hash: str, amount: Decimal) -> Order:
        """Called by the monitor once a matching blockchain transfer is found."""
        if payment.status == PaymentStatus.PAID:
            return await self.orders.get(payment.order_id)

        if await self.payments.get_by_tx_hash(tx_hash):
            raise ConflictError("duplicate_transaction")

        payment.status = PaymentStatus.PAID
        payment.tx_hash = tx_hash
        payment.paid_at = utcnow()
        await self.session.flush()

        order = await self.orders.get(payment.order_id)
        await self.logs.write(
            LogType.PAYMENT,
            "tron_payment_verified",
            f"tx {tx_hash} matched order {order.order_number}",
            user_id=order.user_id,
            order_id=order.id,
            payment_id=payment.id,
            data={"amount": str(amount), "payable_amount": str(payment.payable_amount)},
        )
        await self.orders.mark_paid(order, PaymentMethod.TRON)
        return await self._fulfil(order)

    # -- bank -------------------------------------------------------
    async def create_bank_payment(self, order: Order, receipt_image: str | None = None) -> Payment:
        self._ensure_payable(order)
        await self.orders.confirm(order)
        await self._expire_active(order)

        payment = await self.payments.create(
            order_id=order.id,
            method=PaymentMethod.BANK,
            status=PaymentStatus.WAITING_APPROVAL if receipt_image else PaymentStatus.PENDING,
            amount=money(order.final_price),
            currency=order.currency,
            receipt_image=receipt_image,
            expires_at=None if receipt_image else utcnow() + await self._timeout(),
        )
        if receipt_image:
            order.payment_status = PaymentStatus.WAITING_APPROVAL
            await self.session.flush()
        await self.logs.write(
            LogType.PAYMENT,
            "bank_payment_created",
            f"bank payment for order {order.order_number}",
            user_id=order.user_id,
            order_id=order.id,
            payment_id=payment.id,
        )
        return payment

    async def attach_receipt(self, payment: Payment, receipt_image: str) -> Payment:
        payment.receipt_image = receipt_image
        payment.status = PaymentStatus.WAITING_APPROVAL
        payment.expires_at = None
        order = await self.orders.get(payment.order_id)
        order.payment_status = PaymentStatus.WAITING_APPROVAL
        await self.session.flush()
        await self.logs.write(
            LogType.PAYMENT,
            "bank_receipt_uploaded",
            f"receipt uploaded for order {order.order_number}",
            user_id=order.user_id,
            order_id=order.id,
            payment_id=payment.id,
        )
        return payment

    async def approve_manual_payment(self, payment_id: uuid.UUID, note: str = "") -> Order:
        payment = await self.get(payment_id)
        if payment.status == PaymentStatus.PAID:
            return await self.orders.get(payment.order_id)

        payment.status = PaymentStatus.PAID
        payment.paid_at = utcnow()
        payment.note = note or payment.note
        await self.session.flush()

        order = await self.orders.get(payment.order_id)
        await self.logs.write(
            LogType.PAYMENT,
            "manual_payment_approved",
            f"payment approved for order {order.order_number}",
            user_id=order.user_id,
            order_id=order.id,
            payment_id=payment.id,
        )
        await self.orders.mark_paid(order, payment.method)
        return await self._fulfil(order)

    async def reject_manual_payment(self, payment_id: uuid.UUID, note: str = "") -> Payment:
        payment = await self.get(payment_id)
        payment.status = PaymentStatus.REJECTED
        payment.note = note or payment.note
        order = await self.orders.get(payment.order_id)
        order.payment_status = PaymentStatus.PENDING
        await self.session.flush()
        await self.logs.write(
            LogType.PAYMENT,
            "manual_payment_rejected",
            note or f"payment rejected for order {order.order_number}",
            user_id=order.user_id,
            order_id=order.id,
            payment_id=payment.id,
        )
        return payment

    # -- shared completion ------------------------------------------
    async def _fulfil(self, order: Order) -> Order:
        """Single completion path for wallet, TRON and manual payments."""
        if order.flow == "topup":
            amount = Decimal(str(order.input_data.get("amount", order.final_price)))
            await self.wallets.deposit(
                order.user_id,
                money(amount),
                description=f"topup {order.order_number}",
                order_id=order.id,
                reference=f"topup:{order.id}",
                tx_type=TransactionType.DEPOSIT,
            )
            return await self.orders.complete(order)

        if not await self.settings.get_bool("auto_process_orders", True):
            order.status = OrderStatus.PROCESSING
            await self.session.flush()
            return order

        return await self.orders.process(order)

    # -- expiry / lookup --------------------------------------------
    async def expire_pending(self, limit: int = 100) -> int:
        expired = await self.payments.list_expired(limit)
        for payment in expired:
            payment.status = PaymentStatus.EXPIRED
            order = await self.orders.get(payment.order_id)
            await self.logs.write(
                LogType.PAYMENT,
                "payment_timeout",
                f"invoice expired for order {order.order_number}",
                user_id=order.user_id,
                order_id=order.id,
                payment_id=payment.id,
                data={"payable_amount": str(payment.payable_amount or "")},
            )
        await self.session.flush()
        return len(expired)

    async def get(self, payment_id: uuid.UUID) -> Payment:
        payment = await self.payments.get(payment_id)
        if not payment:
            raise NotFoundError("payment_not_found")
        return payment

    async def search(self, page: int, size: int, **filters: Any) -> tuple[list[Payment], int]:
        return await self.payments.search(page, size, **filters)

    async def methods_for(self, order: Order, user: User) -> dict[str, Any]:
        balance = await self.wallets.balance(user.id)
        return {
            "balance": str(balance),
            "amount": str(money(order.final_price)),
            "wallet_available": balance >= money(order.final_price) and order.flow != "topup",
            "tron_available": bool(await self.settings.get_str("tron_wallet_address")),
            "bank_available": bool(await self.settings.get_str("bank_card_number")),
        }
