"""Rewards Engine — redemption lifecycle on top of the immutable point ledger (Phase 7.3)."""
from __future__ import annotations

from datetime import datetime, timezone
from uuid import UUID, uuid4

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

from app.events.publisher import TransactionalEventPublisher
from app.events.types import LoyaltyEventType
from app.models.rewards import RewardRedemption
from app.models.types import AuditAction, RedemptionStatus
from app.repositories.foundation import (
    LoyaltyProgramRepository,
    MemberRepository,
    PointAccountRepository,
    RewardRepository,
)
from app.repositories.rewards import RewardRedemptionRepository
from app.schemas.ledger import PointEarnRequest, PointRedeemRequest
from app.schemas.rewards import CancelRequest, RedeemRequest
from app.services.audit_service import AuditService
from app.services.point_engine import PointEngineService
from app.validators import (
    ensure_cancellable,
    ensure_fulfillable,
    ensure_max_per_member,
    ensure_member_active,
    ensure_reward_active,
    ensure_stock_available,
    validate_reason,
)
from shared.exceptions import AppError, NotFoundError
from shared.security import CurrentUser


def _now() -> datetime:
    return datetime.now(timezone.utc)


def _next_voucher_code() -> str:
    return f"VCH-{uuid4().hex[:10].upper()}"


class RewardsEngineService:
    def __init__(
        self,
        session: AsyncSession,
        publisher: TransactionalEventPublisher | None = None,
    ) -> None:
        self.session = session
        self.rewards = RewardRepository(session)
        self.redemptions = RewardRedemptionRepository(session)
        self.members = MemberRepository(session)
        self.accounts = PointAccountRepository(session)
        self.programs = LoyaltyProgramRepository(session)
        self.audit = AuditService(session)
        self.publisher = publisher or TransactionalEventPublisher(session)
        self.point_engine = PointEngineService(session, publisher=self.publisher)

    async def redeem(
        self,
        tenant_id: UUID,
        reward_id: UUID,
        body: RedeemRequest,
        *,
        actor: CurrentUser | None = None,
    ) -> RewardRedemption:
        if body.idempotency_key:
            existing = await self.redemptions.get_by_idempotency(
                tenant_id, body.idempotency_key
            )
            if existing is not None:
                return existing

        reward = await self.rewards.get(tenant_id, reward_id)
        if reward is None:
            raise NotFoundError("پاداش یافت نشد", error_code="reward_not_found")
        ensure_reward_active(reward)

        member = await self.members.get(tenant_id, body.member_id)
        if member is None:
            raise NotFoundError("عضو یافت نشد", error_code="member_not_found")
        if member.program_id != reward.program_id:
            raise AppError(
                "عضو متعلق به برنامه این پاداش نیست",
                status_code=409,
                error_code="member_program_mismatch",
            )
        ensure_member_active(member.status)

        account = await self.accounts.get_by_member(tenant_id, member.id)
        if account is None:
            raise NotFoundError(
                "حساب امتیاز یافت نشد", error_code="point_account_not_found"
            )

        ensure_stock_available(reward)
        existing_count = await self.redemptions.count_for_member_reward(
            tenant_id, member.id, reward.id
        )
        ensure_max_per_member(existing_count, reward.max_per_member)

        actor_id = actor.user_id if actor else None
        ledger_entry_id = None
        points_spent = 0
        if reward.points_cost > 0:
            entry = await self.point_engine.redeem(
                tenant_id,
                account.id,
                PointRedeemRequest(
                    amount=reward.points_cost,
                    reason=f"reward:{reward.code}",
                    reference_type="reward_redemption",
                    reference_id=str(reward.id),
                ),
                actor=actor,
                auto_commit=False,
            )
            ledger_entry_id = entry.id
            points_spent = reward.points_cost

        redemption = RewardRedemption(
            tenant_id=tenant_id,
            program_id=reward.program_id,
            reward_id=reward.id,
            member_id=member.id,
            point_account_id=account.id,
            status=RedemptionStatus.PENDING,
            points_spent=points_spent,
            ledger_entry_id=ledger_entry_id,
            voucher_code=_next_voucher_code(),
            idempotency_key=body.idempotency_key,
            reason=body.reason,
            metadata_json=body.metadata_json,
            actor_user_id=actor_id,
        )
        try:
            await self.redemptions.add(redemption)
        except IntegrityError:
            await self.session.rollback()
            raise AppError(
                "درخواست بازخرید تکراری است",
                status_code=409,
                error_code="redemption_idempotency_conflict",
            )
        reward.redeemed_count += 1
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="reward_redemption",
            entity_id=redemption.id,
            action=AuditAction.REDEEM_REWARD,
            actor_user_id=actor_id,
            message=f"Reward {reward.code} redeemed by member {member.membership_number}",
            changes={"points_spent": points_spent, "reward_id": str(reward.id)},
        )
        await self.publisher.publish(
            event_type=LoyaltyEventType.REWARD_REDEEMED,
            aggregate_type="reward_redemption",
            aggregate_id=redemption.id,
            tenant_id=tenant_id,
            payload={
                "reward_id": str(reward.id),
                "member_id": str(member.id),
                "points_spent": points_spent,
                "voucher_code": redemption.voucher_code,
            },
        )
        await self.session.commit()
        await self.session.refresh(redemption)
        return redemption

    async def fulfill(
        self,
        tenant_id: UUID,
        redemption_id: UUID,
        *,
        actor: CurrentUser | None = None,
    ) -> RewardRedemption:
        entity = await self.get(tenant_id, redemption_id)
        ensure_fulfillable(entity.status)
        actor_id = actor.user_id if actor else None
        entity.status = RedemptionStatus.FULFILLED
        entity.fulfilled_at = _now()
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="reward_redemption",
            entity_id=entity.id,
            action=AuditAction.FULFILL_REWARD,
            actor_user_id=actor_id,
            message=f"Redemption {entity.voucher_code} fulfilled",
        )
        await self.publisher.publish(
            event_type=LoyaltyEventType.REWARD_FULFILLED,
            aggregate_type="reward_redemption",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"voucher_code": entity.voucher_code},
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def cancel(
        self,
        tenant_id: UUID,
        redemption_id: UUID,
        body: CancelRequest,
        *,
        actor: CurrentUser | None = None,
    ) -> RewardRedemption:
        entity = await self.get(tenant_id, redemption_id)
        ensure_cancellable(entity.status)
        reason = validate_reason(body.reason, required=True, field="reason")
        actor_id = actor.user_id if actor else None

        if entity.points_spent > 0:
            account = await self.accounts.get(tenant_id, entity.point_account_id)
            if account is None:
                raise NotFoundError(
                    "حساب امتیاز یافت نشد", error_code="point_account_not_found"
                )
            await self.point_engine.earn(
                tenant_id,
                account.id,
                PointEarnRequest(
                    amount=entity.points_spent,
                    reason=f"refund:{entity.voucher_code}",
                    reference_type="reward_redemption_cancel",
                    reference_id=str(entity.id),
                ),
                actor=actor,
                auto_commit=False,
            )

        reward = await self.rewards.get(tenant_id, entity.reward_id)
        if reward is not None and reward.redeemed_count > 0:
            reward.redeemed_count -= 1

        entity.status = RedemptionStatus.CANCELLED
        entity.cancelled_at = _now()
        entity.cancel_reason = reason
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="reward_redemption",
            entity_id=entity.id,
            action=AuditAction.CANCEL_REDEMPTION,
            actor_user_id=actor_id,
            message=f"Redemption {entity.voucher_code} cancelled",
            changes={"reason": reason, "points_refunded": entity.points_spent},
        )
        await self.publisher.publish(
            event_type=LoyaltyEventType.REWARD_REDEMPTION_CANCELLED,
            aggregate_type="reward_redemption",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"voucher_code": entity.voucher_code, "reason": reason},
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def get(self, tenant_id: UUID, redemption_id: UUID) -> RewardRedemption:
        entity = await self.redemptions.get(tenant_id, redemption_id)
        if entity is None:
            raise NotFoundError(
                "درخواست بازخرید یافت نشد", error_code="redemption_not_found"
            )
        return entity

    async def list(
        self,
        tenant_id: UUID,
        *,
        reward_id: UUID | None = None,
        member_id: UUID | None = None,
        status: RedemptionStatus | None = None,
        offset: int = 0,
        limit: int = 50,
    ):
        rows = await self.redemptions.list(
            tenant_id,
            reward_id=reward_id,
            member_id=member_id,
            status=status,
            offset=offset,
            limit=limit,
        )
        total = await self.redemptions.count(
            tenant_id, reward_id=reward_id, member_id=member_id, status=status
        )
        return rows, total
