import uuid
from datetime import timezone
from decimal import Decimal
from typing import Any

from sqlalchemy.ext.asyncio import AsyncSession

from app.core.exceptions import NotFoundError, ValidationError
from app.models import Coupon
from app.repositories import CouponRepository
from app.utils.helpers import money, utcnow


class CouponService:
    def __init__(self, session: AsyncSession) -> None:
        self.session = session
        self.coupons = CouponRepository(session)

    async def validate(self, code: str, user_id: uuid.UUID, amount: Decimal) -> Coupon:
        coupon = await self.coupons.get_by_code(code)
        if not coupon or not coupon.enabled:
            raise ValidationError("coupon_invalid")

        if coupon.expires_at is not None:
            expires_at = coupon.expires_at
            if expires_at.tzinfo is None:
                expires_at = expires_at.replace(tzinfo=timezone.utc)
            if expires_at < utcnow():
                raise ValidationError("coupon_expired")

        if coupon.usage_limit is not None and coupon.used_count >= coupon.usage_limit:
            raise ValidationError("coupon_exhausted")

        if coupon.minimum_order and amount < coupon.minimum_order:
            raise ValidationError("coupon_minimum_order")

        if coupon.per_user_limit is not None:
            used = await self.coupons.count_user_usage(coupon.id, user_id)
            if used >= coupon.per_user_limit:
                raise ValidationError("coupon_user_limit")

        return coupon

    def calculate_discount(self, coupon: Coupon, amount: Decimal) -> Decimal:
        if coupon.type == "fixed":
            discount = Decimal(coupon.value)
        else:
            discount = amount * Decimal(coupon.value) / Decimal("100")

        if coupon.max_discount:
            discount = min(discount, Decimal(coupon.max_discount))
        return money(min(discount, amount))

    async def apply(self, code: str, user_id: uuid.UUID, amount: Decimal) -> tuple[Coupon, Decimal]:
        coupon = await self.validate(code, user_id, amount)
        return coupon, self.calculate_discount(coupon, amount)

    async def mark_used(self, coupon_id: uuid.UUID | None) -> None:
        if not coupon_id:
            return
        coupon = await self.coupons.get(coupon_id)
        if coupon:
            coupon.used_count += 1
            await self.session.flush()

    # -- admin ------------------------------------------------------
    async def search(self, page: int, size: int, search: str | None = None) -> tuple[list[Coupon], int]:
        return await self.coupons.search(page, size, search)

    async def get(self, coupon_id: uuid.UUID) -> Coupon:
        coupon = await self.coupons.get(coupon_id)
        if not coupon:
            raise NotFoundError("coupon_not_found")
        return coupon

    async def create(self, **values: Any) -> Coupon:
        values["code"] = str(values["code"]).strip().upper()
        if await self.coupons.get_by_code(values["code"]):
            raise ValidationError("coupon_exists")
        return await self.coupons.create(**values)

    async def update(self, coupon_id: uuid.UUID, **values: Any) -> Coupon:
        coupon = await self.get(coupon_id)
        payload = {key: value for key, value in values.items() if value is not None}
        if "code" in payload:
            payload["code"] = str(payload["code"]).strip().upper()
            other = await self.coupons.get_by_code(payload["code"])
            if other and other.id != coupon.id:
                raise ValidationError("coupon_exists")
        return await self.coupons.update(coupon, **payload)

    async def delete(self, coupon_id: uuid.UUID) -> None:
        await self.coupons.delete(await self.get(coupon_id))
