import uuid
from decimal import Decimal
from typing import Any

from sqlalchemy.ext.asyncio import AsyncSession

from app.core.exceptions import ConflictError, NotFoundError, ValidationError
from app.flows import FlowContext, get_flow
from app.models import Order, OrderStatus, PaymentStatus, Product, User
from app.repositories import OrderRepository, ProductRepository
from app.services.coupon_service import CouponService
from app.services.fragment_service import FragmentService
from app.services.log_service import LogService, LogType
from app.services.settings_service import SettingsService
from app.services.ton_service import TonService
from app.services.wallet_service import TransactionType, WalletService
from app.utils.helpers import crypto, money, order_number, utcnow


class OrderService:
    """Order lifecycle: draft -> waiting_payment -> paid -> processing -> completed."""

    def __init__(self, session: AsyncSession) -> None:
        self.session = session
        self.orders = OrderRepository(session)
        self.products = ProductRepository(session)
        self.settings = SettingsService(session)
        self.coupons = CouponService(session)
        self.wallets = WalletService(session)
        self.logs = LogService(session)

    def _context(self) -> FlowContext:
        return FlowContext(self.settings, FragmentService(), TonService())

    @staticmethod
    def _snapshot(product: Product | None, flow_name: str) -> dict[str, Any]:
        if product is None:
            return {"flow": flow_name, "title": flow_name, "data": {}}
        return {
            "product_id": str(product.id),
            "flow": product.flow,
            "title": product.title,
            "slug": product.slug,
            "description": product.description,
            "price": str(product.effective_price),
            "currency": product.currency,
            "icon": product.icon,
            "image": product.image,
            "category": product.category.title if product.category else None,
            "data": dict(product.data or {}),
        }

    async def create_draft(
        self,
        user: User,
        product_id: uuid.UUID | None = None,
        flow_name: str | None = None,
        input_data: dict[str, Any] | None = None,
    ) -> Order:
        product: Product | None = None
        if product_id:
            product = await self.products.get(product_id)
            if not product or not product.enabled:
                raise ValidationError("product_unavailable")
            flow_name = product.flow

        flow = get_flow(flow_name or "")
        if product is None and not flow.productless:
            raise ValidationError("product_required")

        cleaned = flow.validate_all(product, input_data or {})
        price = await flow.calculate_price(self._context(), product, cleaned)
        quantity = flow.quantity(product, cleaned)

        order = await self.orders.create(
            order_number=order_number(),
            user_id=user.id,
            product_id=product.id if product else None,
            status=OrderStatus.DRAFT,
            payment_status=PaymentStatus.PENDING,
            quantity=quantity,
            price=price,
            discount=Decimal("0"),
            final_price=price,
            currency=product.currency if product else await self.settings.get_str("currency", "IRT"),
            input_data=cleaned,
            product_snapshot=self._snapshot(product, flow.name),
        )
        await self.logs.write(
            LogType.ORDER,
            "order_created",
            f"order {order.order_number} created",
            user_id=user.id,
            order_id=order.id,
            data={"price": str(price), "flow": flow.name},
        )
        return order

    async def max_affordable(
        self, user: User, product_id: uuid.UUID | None, flow_name: str | None
    ) -> str:
        """Largest quantity/amount the user's wallet balance can cover.

        Backs the "calculate with my balance" button in the bot.
        """
        balance = await self.wallets.balance(user.id)
        product = await self.products.get(product_id) if product_id else None
        flow = get_flow(product.flow if product else (flow_name or ""))

        if flow.name in ("ton", "ton_balance"):
            rate = await self.settings.get_decimal("ton_rate_irt")
            if rate <= 0:
                raise ValidationError("ton_rate_not_configured")
            return str(crypto(balance / rate, 2))

        if product is not None and bool((product.data or {}).get("per_unit")):
            unit_price = Decimal(product.effective_price)
            if unit_price <= 0:
                raise ValidationError("invalid_product_price")
            maximum = int((product.data or {}).get("max_quantity", 100000))
            return str(min(int(balance / unit_price), maximum))

        return str(money(balance))

    async def get(self, order_id: uuid.UUID) -> Order:
        order = await self.orders.get(order_id)
        if not order:
            raise NotFoundError("order_not_found")
        return order

    async def get_for_user(self, order_id: uuid.UUID, user_id: uuid.UUID) -> Order:
        order = await self.get(order_id)
        if order.user_id != user_id:
            raise NotFoundError("order_not_found")
        return order

    async def get_by_number(self, order_number_value: str) -> Order:
        order = await self.orders.get_by_number(order_number_value)
        if not order:
            raise NotFoundError("order_not_found")
        return order

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

    async def recent(self, user_id: uuid.UUID, limit: int = 10) -> list[Order]:
        return await self.orders.recent_for_user(user_id, limit)

    async def update_inputs(self, order: Order, values: dict[str, Any]) -> Order:
        """Change gift options (hide/comment) or any collected input, then reprice."""
        if order.status not in (OrderStatus.DRAFT, OrderStatus.WAITING_PAYMENT):
            raise ConflictError("order_not_editable")

        product = await self.products.get(order.product_id) if order.product_id else None
        flow = get_flow(order.flow)
        merged = {**(order.input_data or {}), **values}
        cleaned = flow.validate_all(product, merged)

        price = await flow.calculate_price(self._context(), product, cleaned)
        order.input_data = cleaned
        order.quantity = flow.quantity(product, cleaned)
        order.price = price
        order.final_price = money(max(price - Decimal(order.discount), Decimal("0")))
        await self.session.flush()
        return order

    async def apply_coupon(self, order: Order, code: str) -> Order:
        if order.status not in (OrderStatus.DRAFT, OrderStatus.WAITING_PAYMENT):
            raise ConflictError("order_not_editable")

        coupon, discount = await self.coupons.apply(code, order.user_id, Decimal(order.price))
        order.coupon_id = coupon.id
        order.discount = discount
        order.final_price = money(Decimal(order.price) - discount)
        await self.session.flush()
        await self.logs.write(
            LogType.ORDER,
            "coupon_applied",
            f"coupon {coupon.code} applied",
            user_id=order.user_id,
            order_id=order.id,
            data={"discount": str(discount)},
        )
        return order

    async def remove_coupon(self, order: Order) -> Order:
        order.coupon_id = None
        order.discount = Decimal("0")
        order.final_price = money(order.price)
        await self.session.flush()
        return order

    async def confirm(self, order: Order) -> Order:
        if order.status == OrderStatus.DRAFT:
            order.status = OrderStatus.WAITING_PAYMENT
            await self.session.flush()
            await self.logs.write(
                LogType.ORDER,
                "order_confirmed",
                f"order {order.order_number} confirmed",
                user_id=order.user_id,
                order_id=order.id,
            )
        return order

    async def cancel(self, order: Order, reason: str = "") -> Order:
        if order.status in (OrderStatus.COMPLETED, OrderStatus.REFUNDED):
            raise ConflictError("order_not_cancellable")
        order.status = OrderStatus.CANCELLED
        order.error_message = reason or None
        await self.session.flush()
        await self.logs.write(
            LogType.ORDER,
            "order_cancelled",
            reason or f"order {order.order_number} cancelled",
            user_id=order.user_id,
            order_id=order.id,
        )
        return order

    async def mark_paid(self, order: Order, method: str) -> Order:
        order.status = OrderStatus.PAID
        order.payment_status = PaymentStatus.PAID
        order.payment_method = method
        await self.session.flush()
        await self.coupons.mark_used(order.coupon_id)
        await self.logs.write(
            LogType.ORDER,
            "order_paid",
            f"order {order.order_number} paid by {method}",
            user_id=order.user_id,
            order_id=order.id,
            data={"amount": str(order.final_price)},
        )
        return order

    async def process(self, order: Order) -> Order:
        """Deliver the product. Safe to call twice: completed orders are skipped."""
        if order.status in (OrderStatus.COMPLETED, OrderStatus.REFUNDED):
            return order
        if order.status not in (OrderStatus.PAID, OrderStatus.PROCESSING, OrderStatus.FAILED):
            raise ConflictError("order_not_payable")

        order.status = OrderStatus.PROCESSING
        await self.session.flush()

        flow = get_flow(order.flow)
        try:
            result = await flow.process(self._context(), order)
        except Exception as exc:  # noqa: BLE001 - recorded and surfaced to the admin
            order.status = OrderStatus.FAILED
            order.error_message = str(exc)[:500]
            await self.session.flush()
            await self.logs.write(
                LogType.ORDER,
                "order_failed",
                str(exc)[:500],
                user_id=order.user_id,
                order_id=order.id,
            )
            return order

        order.result_data = result or {}
        if result.get("manual"):
            await self.logs.write(
                LogType.ORDER,
                "order_manual_processing",
                "waiting for manual fulfilment",
                user_id=order.user_id,
                order_id=order.id,
                data=result,
            )
            return order

        return await self.complete(order)

    async def complete(self, order: Order, note: str = "") -> Order:
        if order.status == OrderStatus.COMPLETED:
            return order
        order.status = OrderStatus.COMPLETED
        order.completed_at = utcnow()
        if note:
            order.result_data = {**(order.result_data or {}), "note": note}
        await self.session.flush()
        await self.logs.write(
            LogType.ORDER,
            "order_completed",
            f"order {order.order_number} completed",
            user_id=order.user_id,
            order_id=order.id,
        )
        await self._reward_referrer(order)
        return order

    async def fail(self, order: Order, reason: str) -> Order:
        order.status = OrderStatus.FAILED
        order.error_message = reason[:500]
        await self.session.flush()
        await self.logs.write(
            LogType.ORDER, "order_failed", reason[:500], user_id=order.user_id, order_id=order.id
        )
        return order

    async def refund(self, order: Order, reason: str = "") -> Order:
        if order.payment_status != PaymentStatus.PAID:
            raise ConflictError("order_not_refundable")
        await self.wallets.deposit(
            order.user_id,
            Decimal(order.final_price),
            description=reason or f"refund {order.order_number}",
            order_id=order.id,
            reference=f"refund:{order.id}",
            tx_type=TransactionType.REFUND,
        )
        order.status = OrderStatus.REFUNDED
        await self.session.flush()
        await self.logs.write(
            LogType.ORDER,
            "order_refunded",
            reason or f"order {order.order_number} refunded",
            user_id=order.user_id,
            order_id=order.id,
        )
        return order

    async def _reward_referrer(self, order: Order) -> None:
        from app.services.referral_service import ReferralService

        await ReferralService(self.session).reward_for_order(order)
