"""Point Engine — immutable ledger writes (Phase 7.2)."""
from __future__ import annotations

from datetime import datetime, timezone
from uuid import UUID

from sqlalchemy.ext.asyncio import AsyncSession

from app.events.publisher import TransactionalEventPublisher
from app.events.types import LoyaltyEventType
from app.models.ledger import PointLedgerEntry
from app.models.types import AuditAction, LedgerEntryType, PointAccountStatus
from app.repositories.foundation import PointAccountRepository
from app.repositories.ledger import PointLedgerRepository
from app.schemas.ledger import (
    PointAdjustRequest,
    PointEarnRequest,
    PointExpireRequest,
    PointRedeemRequest,
)
from app.services.audit_service import AuditService
from app.validators.ledger import (
    ensure_account_open,
    signed_amount,
    validate_adjust_amount,
    validate_positive_points,
)
from shared.exceptions import AppError, NotFoundError
from shared.security import CurrentUser


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


class PointEngineService:
    def __init__(
        self,
        session: AsyncSession,
        publisher: TransactionalEventPublisher | None = None,
    ) -> None:
        self.session = session
        self.accounts = PointAccountRepository(session)
        self.ledger = PointLedgerRepository(session)
        self.audit = AuditService(session)
        self.publisher = publisher or TransactionalEventPublisher(session)

    async def _get_open_account(self, tenant_id: UUID, account_id: UUID):
        entity = await self.accounts.get(tenant_id, account_id)
        if entity is None:
            raise NotFoundError(
                "حساب امتیاز یافت نشد", error_code="point_account_not_found"
            )
        ensure_account_open(entity.status)
        return entity

    async def get_balance(self, tenant_id: UUID, account_id: UUID) -> dict:
        entity = await self.accounts.get(tenant_id, account_id)
        if entity is None:
            raise NotFoundError(
                "حساب امتیاز یافت نشد", error_code="point_account_not_found"
            )
        balance = await self.ledger.balance_for_account(tenant_id, account_id)
        return {
            "point_account_id": account_id,
            "member_id": entity.member_id,
            "program_id": entity.program_id,
            "status": entity.status,
            "balance": balance,
            "currency_name": entity.currency_name,
        }

    async def list_entries(
        self,
        tenant_id: UUID,
        account_id: UUID,
        *,
        entry_type: LedgerEntryType | None = None,
        offset: int = 0,
        limit: int = 50,
        q: str | None = None,
    ):
        entity = await self.accounts.get(tenant_id, account_id)
        if entity is None:
            raise NotFoundError(
                "حساب امتیاز یافت نشد", error_code="point_account_not_found"
            )
        rows = await self.ledger.list_for_account(
            tenant_id,
            account_id,
            entry_type=entry_type,
            offset=offset,
            limit=limit,
            q=q,
        )
        total = await self.ledger.count_for_account(
            tenant_id, account_id, entry_type=entry_type, q=q
        )
        return rows, total

    async def _append(
        self,
        *,
        tenant_id: UUID,
        account,
        entry_type: LedgerEntryType,
        signed: int,
        body_idempotency: str | None,
        reason: str | None,
        reference_type: str | None,
        reference_id: str | None,
        expires_at: datetime | None,
        metadata: dict | None,
        actor: CurrentUser | None,
        event_type: LoyaltyEventType,
        audit_action: AuditAction,
        auto_commit: bool = True,
    ) -> PointLedgerEntry:
        if body_idempotency:
            existing = await self.ledger.get_by_idempotency(tenant_id, body_idempotency)
            if existing is not None:
                return existing
        balance = await self.ledger.balance_for_account(tenant_id, account.id)
        new_balance = balance + signed
        if new_balance < 0:
            raise AppError(
                "موجودی امتیاز کافی نیست",
                status_code=409,
                error_code="insufficient_points",
                details={"balance": balance, "requested": signed},
            )
        actor_id = actor.user_id if actor else None
        entry = PointLedgerEntry(
            tenant_id=tenant_id,
            point_account_id=account.id,
            program_id=account.program_id,
            member_id=account.member_id,
            entry_type=entry_type,
            amount=signed,
            balance_after=new_balance,
            idempotency_key=body_idempotency,
            reference_type=reference_type,
            reference_id=reference_id,
            reason=reason,
            metadata_json=metadata,
            expires_at=expires_at,
            actor_user_id=actor_id,
            created_at=_now(),
        )
        await self.ledger.add(entry)
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="point_ledger_entry",
            entity_id=entry.id,
            action=audit_action,
            actor_user_id=actor_id,
            message=f"{entry_type.value} {signed} on {account.account_number}",
            changes={
                "amount": signed,
                "balance_after": new_balance,
                "point_account_id": str(account.id),
            },
        )
        await self.publisher.publish(
            event_type=event_type,
            aggregate_type="point_ledger_entry",
            aggregate_id=entry.id,
            tenant_id=tenant_id,
            payload={
                "point_account_id": str(account.id),
                "entry_type": entry_type.value,
                "amount": signed,
                "balance_after": new_balance,
            },
        )
        if auto_commit:
            await self.session.commit()
        await self.session.refresh(entry)
        return entry

    async def earn(
        self,
        tenant_id: UUID,
        account_id: UUID,
        body: PointEarnRequest,
        *,
        actor: CurrentUser | None = None,
        auto_commit: bool = True,
    ) -> PointLedgerEntry:
        account = await self._get_open_account(tenant_id, account_id)
        amount = validate_positive_points(body.amount)
        return await self._append(
            tenant_id=tenant_id,
            account=account,
            entry_type=LedgerEntryType.EARN,
            signed=signed_amount(LedgerEntryType.EARN, amount),
            body_idempotency=body.idempotency_key,
            reason=body.reason,
            reference_type=body.reference_type,
            reference_id=body.reference_id,
            expires_at=body.expires_at,
            metadata=body.metadata_json,
            actor=actor,
            event_type=LoyaltyEventType.POINTS_EARNED,
            audit_action=AuditAction.EARN,
            auto_commit=auto_commit,
        )

    async def redeem(
        self,
        tenant_id: UUID,
        account_id: UUID,
        body: PointRedeemRequest,
        *,
        actor: CurrentUser | None = None,
        auto_commit: bool = True,
    ) -> PointLedgerEntry:
        account = await self._get_open_account(tenant_id, account_id)
        amount = validate_positive_points(body.amount)
        return await self._append(
            tenant_id=tenant_id,
            account=account,
            entry_type=LedgerEntryType.REDEEM,
            signed=signed_amount(LedgerEntryType.REDEEM, amount),
            body_idempotency=body.idempotency_key,
            reason=body.reason,
            reference_type=body.reference_type,
            reference_id=body.reference_id,
            expires_at=None,
            metadata=body.metadata_json,
            actor=actor,
            event_type=LoyaltyEventType.POINTS_REDEEMED,
            audit_action=AuditAction.REDEEM,
            auto_commit=auto_commit,
        )

    async def adjust(
        self,
        tenant_id: UUID,
        account_id: UUID,
        body: PointAdjustRequest,
        *,
        actor: CurrentUser | None = None,
        auto_commit: bool = True,
    ) -> PointLedgerEntry:
        account = await self._get_open_account(tenant_id, account_id)
        amount = validate_adjust_amount(body.amount)
        return await self._append(
            tenant_id=tenant_id,
            account=account,
            entry_type=LedgerEntryType.ADJUST,
            signed=signed_amount(LedgerEntryType.ADJUST, amount),
            body_idempotency=body.idempotency_key,
            reason=body.reason,
            reference_type=body.reference_type,
            reference_id=body.reference_id,
            expires_at=None,
            metadata=body.metadata_json,
            actor=actor,
            event_type=LoyaltyEventType.POINTS_ADJUSTED,
            audit_action=AuditAction.ADJUST,
            auto_commit=auto_commit,
        )

    async def expire(
        self,
        tenant_id: UUID,
        account_id: UUID,
        body: PointExpireRequest,
        *,
        actor: CurrentUser | None = None,
        auto_commit: bool = True,
    ) -> PointLedgerEntry:
        account = await self._get_open_account(tenant_id, account_id)
        amount = validate_positive_points(body.amount)
        return await self._append(
            tenant_id=tenant_id,
            account=account,
            entry_type=LedgerEntryType.EXPIRE,
            signed=signed_amount(LedgerEntryType.EXPIRE, amount),
            body_idempotency=body.idempotency_key,
            reason=body.reason or "points_expired",
            reference_type=body.reference_type,
            reference_id=body.reference_id,
            expires_at=None,
            metadata=body.metadata_json,
            actor=actor,
            event_type=LoyaltyEventType.POINTS_EXPIRED,
            audit_action=AuditAction.POINTS_EXPIRE,
            auto_commit=auto_commit,
        )
