import uuid
from decimal import Decimal
from typing import Any

from sqlalchemy.ext.asyncio import AsyncSession

from app.core.exceptions import ConflictError, NotFoundError, PaymentError
from app.models import Transaction, Wallet
from app.repositories import TransactionRepository, WalletRepository
from app.services.log_service import LogService, LogType
from app.utils.helpers import money


class TransactionType:
    DEPOSIT = "deposit"
    PURCHASE = "purchase"
    REFUND = "refund"
    BONUS = "bonus"
    ADMIN = "admin"

    ALL = (DEPOSIT, PURCHASE, REFUND, BONUS, ADMIN)


class WalletService:
    """The only place where wallet balances change. Always via a transaction row."""

    def __init__(self, session: AsyncSession) -> None:
        self.session = session
        self.wallets = WalletRepository(session)
        self.transactions = TransactionRepository(session)
        self.logs = LogService(session)

    async def get_wallet(self, user_id: uuid.UUID) -> Wallet:
        wallet = await self.wallets.get_by_user(user_id)
        if not wallet:
            wallet = await self.wallets.create(user_id=user_id)
        return wallet

    async def balance(self, user_id: uuid.UUID) -> Decimal:
        wallet = await self.get_wallet(user_id)
        return money(wallet.balance)

    async def _apply(
        self,
        user_id: uuid.UUID,
        amount: Decimal,
        tx_type: str,
        description: str = "",
        order_id: uuid.UUID | None = None,
        reference: str | None = None,
    ) -> Transaction:
        if reference:
            existing = await self.transactions.get_by_reference(reference)
            if existing:
                raise ConflictError("duplicate_transaction")

        wallet = await self.wallets.get_for_update(user_id)
        if not wallet:
            wallet = await self.wallets.create(user_id=user_id)

        new_balance = money(Decimal(wallet.balance) + amount)
        if new_balance < 0:
            raise PaymentError("insufficient_balance")

        wallet.balance = new_balance
        await self.session.flush()

        transaction = await self.transactions.create(
            wallet_id=wallet.id,
            type=tx_type,
            amount=money(amount),
            balance_after=new_balance,
            description=description,
            order_id=order_id,
            reference=reference,
        )
        await self.logs.write(
            LogType.WALLET,
            f"wallet_{tx_type}",
            description or tx_type,
            user_id=user_id,
            order_id=order_id,
            data={"amount": str(money(amount)), "balance": str(new_balance)},
        )
        return transaction

    async def deposit(
        self,
        user_id: uuid.UUID,
        amount: Decimal,
        description: str = "",
        order_id: uuid.UUID | None = None,
        reference: str | None = None,
        tx_type: str = TransactionType.DEPOSIT,
    ) -> Transaction:
        if amount <= 0:
            raise PaymentError("invalid_amount")
        return await self._apply(user_id, money(amount), tx_type, description, order_id, reference)

    async def withdraw(
        self,
        user_id: uuid.UUID,
        amount: Decimal,
        description: str = "",
        order_id: uuid.UUID | None = None,
        reference: str | None = None,
        tx_type: str = TransactionType.PURCHASE,
    ) -> Transaction:
        if amount <= 0:
            raise PaymentError("invalid_amount")
        return await self._apply(user_id, -money(amount), tx_type, description, order_id, reference)

    async def adjust(self, user_id: uuid.UUID, amount: Decimal, description: str = "") -> Transaction:
        """Admin correction; positive credits, negative debits."""
        if amount == 0:
            raise PaymentError("invalid_amount")
        return await self._apply(user_id, money(amount), TransactionType.ADMIN, description)

    async def history(
        self, user_id: uuid.UUID, page: int = 1, size: int = 20, **filters: Any
    ) -> tuple[list[Transaction], int]:
        wallet = await self.get_wallet(user_id)
        return await self.transactions.history(wallet.id, page, size, **filters)

    async def has_balance(self, user_id: uuid.UUID, amount: Decimal) -> bool:
        return await self.balance(user_id) >= money(amount)

    async def wallet_or_404(self, wallet_id: uuid.UUID) -> Wallet:
        wallet = await self.wallets.get(wallet_id)
        if not wallet:
            raise NotFoundError("wallet_not_found")
        return wallet
