"""Phase 7.1 — Membership lifecycle engine."""
from __future__ import annotations

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

from sqlalchemy.ext.asyncio import AsyncSession

from app.events.publisher import TransactionalEventPublisher
from app.events.types import LoyaltyEventType
from app.models.foundation import Member, MembershipLifecycleEvent
from app.models.types import AuditAction, MemberStatus, MembershipLifecycleAction
from app.repositories.foundation import (
    LoyaltyProgramRepository,
    MemberRepository,
    MembershipLifecycleEventRepository,
    MembershipTierRepository,
)
from app.schemas.foundation import (
    MemberActivateRequest,
    MemberCancelRequest,
    MemberExpireRequest,
    MemberFreezeRequest,
    MemberRenewRequest,
    MemberResumeRequest,
    MemberTransferRequest,
)
from app.services.audit_service import AuditService
from app.validators.membership import (
    DEFAULT_TERM_DAYS,
    compute_expiry,
    ensure_lifecycle_transition,
    target_status_for,
    validate_reason,
)
from shared.exceptions import AppError, NotFoundError
from shared.security import CurrentUser


def _aware(dt: datetime | None) -> datetime | None:
    if dt is None:
        return None
    if dt.tzinfo is None:
        return dt.replace(tzinfo=timezone.utc)
    return dt


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


class MembershipEngineService:
    def __init__(
        self,
        session: AsyncSession,
        publisher: TransactionalEventPublisher | None = None,
    ) -> None:
        self.session = session
        self.members = MemberRepository(session)
        self.programs = LoyaltyProgramRepository(session)
        self.tiers = MembershipTierRepository(session)
        self.lifecycle = MembershipLifecycleEventRepository(session)
        self.audit = AuditService(session)
        self.publisher = publisher or TransactionalEventPublisher(session)

    async def _get_member(self, tenant_id: UUID, member_id: UUID) -> Member:
        entity = await self.members.get(tenant_id, member_id)
        if entity is None:
            raise NotFoundError("عضو یافت نشد", error_code="member_not_found")
        return entity

    async def _record_lifecycle(
        self,
        *,
        tenant_id: UUID,
        member: Member,
        action: MembershipLifecycleAction,
        from_status: MemberStatus,
        to_status: MemberStatus,
        reason: str | None,
        actor_id: str | None,
        metadata: dict[str, Any] | None = None,
    ) -> MembershipLifecycleEvent:
        row = MembershipLifecycleEvent(
            tenant_id=tenant_id,
            member_id=member.id,
            action=action,
            from_status=from_status.value,
            to_status=to_status.value,
            reason=reason,
            metadata_json=metadata,
            actor_user_id=actor_id,
            created_at=_now(),
        )
        await self.lifecycle.add(row)
        return row

    async def _publish(
        self,
        *,
        event_type: LoyaltyEventType,
        member: Member,
        tenant_id: UUID,
        payload: dict[str, Any],
    ) -> None:
        await self.publisher.publish(
            event_type=event_type,
            aggregate_type="member",
            aggregate_id=member.id,
            tenant_id=tenant_id,
            payload=payload,
        )

    async def activate(
        self,
        tenant_id: UUID,
        member_id: UUID,
        body: MemberActivateRequest,
        *,
        actor: CurrentUser | None = None,
    ) -> Member:
        entity = await self._get_member(tenant_id, member_id)
        action = MembershipLifecycleAction.ACTIVATE
        ensure_lifecycle_transition(action=action, current=entity.status)
        if body.tier_id is not None:
            tier = await self.tiers.get(tenant_id, body.tier_id)
            if tier is None or tier.program_id != entity.program_id:
                raise NotFoundError("سطح عضویت یافت نشد", error_code="tier_not_found")
            entity.tier_id = body.tier_id
        from_status = entity.status
        to_status = target_status_for(action)
        now = _now()
        entity.status = to_status
        entity.enrolled_at = entity.enrolled_at or now
        entity.activated_at = entity.activated_at or now
        entity.membership_started_at = entity.membership_started_at or now
        entity.membership_expires_at = compute_expiry(
            from_when=now,
            term_days=body.term_days or DEFAULT_TERM_DAYS,
            explicit=body.expires_at,
        )
        entity.expired_at = None
        entity.frozen_at = None
        entity.freeze_reason = None
        entity.version += 1
        entity.updated_by = actor.user_id if actor else None
        await self._record_lifecycle(
            tenant_id=tenant_id,
            member=entity,
            action=action,
            from_status=from_status,
            to_status=to_status,
            reason=validate_reason(body.reason),
            actor_id=entity.updated_by,
            metadata={"term_days": body.term_days or DEFAULT_TERM_DAYS},
        )
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="member",
            entity_id=entity.id,
            action=AuditAction.ACTIVATE,
            actor_user_id=entity.updated_by,
            message=f"Member {entity.membership_number} activated",
        )
        await self._publish(
            event_type=LoyaltyEventType.MEMBER_ACTIVATED,
            member=entity,
            tenant_id=tenant_id,
            payload={
                "membership_number": entity.membership_number,
                "status": entity.status.value,
                "membership_expires_at": entity.membership_expires_at.isoformat()
                if entity.membership_expires_at
                else None,
            },
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def renew(
        self,
        tenant_id: UUID,
        member_id: UUID,
        body: MemberRenewRequest,
        *,
        actor: CurrentUser | None = None,
    ) -> Member:
        entity = await self._get_member(tenant_id, member_id)
        action = MembershipLifecycleAction.RENEW
        ensure_lifecycle_transition(action=action, current=entity.status)
        from_status = entity.status
        to_status = target_status_for(action)
        now = _now()
        current_expiry = _aware(entity.membership_expires_at)
        base = current_expiry if (current_expiry and current_expiry > now) else now
        entity.status = to_status
        entity.activated_at = _aware(entity.activated_at) or now
        entity.membership_started_at = _aware(entity.membership_started_at) or now
        entity.membership_expires_at = compute_expiry(
            from_when=base,
            term_days=body.term_days or DEFAULT_TERM_DAYS,
            explicit=body.expires_at,
        )
        entity.expired_at = None
        entity.frozen_at = None
        entity.freeze_reason = None
        entity.version += 1
        entity.updated_by = actor.user_id if actor else None
        await self._record_lifecycle(
            tenant_id=tenant_id,
            member=entity,
            action=action,
            from_status=from_status,
            to_status=to_status,
            reason=validate_reason(body.reason),
            actor_id=entity.updated_by,
            metadata={"term_days": body.term_days or DEFAULT_TERM_DAYS},
        )
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="member",
            entity_id=entity.id,
            action=AuditAction.RENEW,
            actor_user_id=entity.updated_by,
            message=f"Member {entity.membership_number} renewed",
        )
        await self._publish(
            event_type=LoyaltyEventType.MEMBER_RENEWED,
            member=entity,
            tenant_id=tenant_id,
            payload={
                "membership_number": entity.membership_number,
                "membership_expires_at": entity.membership_expires_at.isoformat()
                if entity.membership_expires_at
                else None,
            },
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def freeze(
        self,
        tenant_id: UUID,
        member_id: UUID,
        body: MemberFreezeRequest,
        *,
        actor: CurrentUser | None = None,
    ) -> Member:
        entity = await self._get_member(tenant_id, member_id)
        action = MembershipLifecycleAction.FREEZE
        ensure_lifecycle_transition(action=action, current=entity.status)
        reason = validate_reason(body.reason, required=True)
        from_status = entity.status
        to_status = target_status_for(action)
        now = _now()
        entity.status = to_status
        entity.frozen_at = now
        entity.freeze_reason = reason
        entity.version += 1
        entity.updated_by = actor.user_id if actor else None
        await self._record_lifecycle(
            tenant_id=tenant_id,
            member=entity,
            action=action,
            from_status=from_status,
            to_status=to_status,
            reason=reason,
            actor_id=entity.updated_by,
        )
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="member",
            entity_id=entity.id,
            action=AuditAction.FREEZE,
            actor_user_id=entity.updated_by,
            message=f"Member {entity.membership_number} frozen",
        )
        await self._publish(
            event_type=LoyaltyEventType.MEMBER_FROZEN,
            member=entity,
            tenant_id=tenant_id,
            payload={
                "membership_number": entity.membership_number,
                "reason": reason,
            },
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def resume(
        self,
        tenant_id: UUID,
        member_id: UUID,
        body: MemberResumeRequest,
        *,
        actor: CurrentUser | None = None,
    ) -> Member:
        entity = await self._get_member(tenant_id, member_id)
        action = MembershipLifecycleAction.RESUME
        ensure_lifecycle_transition(action=action, current=entity.status)
        from_status = entity.status
        to_status = target_status_for(action)
        entity.status = to_status
        entity.frozen_at = None
        entity.freeze_reason = None
        entity.version += 1
        entity.updated_by = actor.user_id if actor else None
        await self._record_lifecycle(
            tenant_id=tenant_id,
            member=entity,
            action=action,
            from_status=from_status,
            to_status=to_status,
            reason=validate_reason(body.reason),
            actor_id=entity.updated_by,
        )
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="member",
            entity_id=entity.id,
            action=AuditAction.RESUME,
            actor_user_id=entity.updated_by,
            message=f"Member {entity.membership_number} resumed",
        )
        await self._publish(
            event_type=LoyaltyEventType.MEMBER_RESUMED,
            member=entity,
            tenant_id=tenant_id,
            payload={"membership_number": entity.membership_number},
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def cancel(
        self,
        tenant_id: UUID,
        member_id: UUID,
        body: MemberCancelRequest,
        *,
        actor: CurrentUser | None = None,
    ) -> Member:
        entity = await self._get_member(tenant_id, member_id)
        action = MembershipLifecycleAction.CANCEL
        ensure_lifecycle_transition(action=action, current=entity.status)
        reason = validate_reason(body.reason, required=True)
        from_status = entity.status
        to_status = target_status_for(action)
        now = _now()
        entity.status = to_status
        entity.cancelled_at = now
        entity.cancel_reason = reason
        entity.frozen_at = None
        entity.freeze_reason = None
        entity.version += 1
        entity.updated_by = actor.user_id if actor else None
        await self._record_lifecycle(
            tenant_id=tenant_id,
            member=entity,
            action=action,
            from_status=from_status,
            to_status=to_status,
            reason=reason,
            actor_id=entity.updated_by,
        )
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="member",
            entity_id=entity.id,
            action=AuditAction.CANCEL,
            actor_user_id=entity.updated_by,
            message=f"Member {entity.membership_number} cancelled",
        )
        await self._publish(
            event_type=LoyaltyEventType.MEMBER_CANCELLED,
            member=entity,
            tenant_id=tenant_id,
            payload={
                "membership_number": entity.membership_number,
                "reason": reason,
            },
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def expire(
        self,
        tenant_id: UUID,
        member_id: UUID,
        body: MemberExpireRequest,
        *,
        actor: CurrentUser | None = None,
    ) -> Member:
        entity = await self._get_member(tenant_id, member_id)
        action = MembershipLifecycleAction.EXPIRE
        ensure_lifecycle_transition(action=action, current=entity.status)
        from_status = entity.status
        to_status = target_status_for(action)
        now = _now()
        entity.status = to_status
        entity.expired_at = now
        entity.frozen_at = None
        entity.freeze_reason = None
        entity.version += 1
        entity.updated_by = actor.user_id if actor else None
        await self._record_lifecycle(
            tenant_id=tenant_id,
            member=entity,
            action=action,
            from_status=from_status,
            to_status=to_status,
            reason=validate_reason(body.reason),
            actor_id=entity.updated_by,
        )
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="member",
            entity_id=entity.id,
            action=AuditAction.EXPIRE,
            actor_user_id=entity.updated_by,
            message=f"Member {entity.membership_number} expired",
        )
        await self._publish(
            event_type=LoyaltyEventType.MEMBER_EXPIRED,
            member=entity,
            tenant_id=tenant_id,
            payload={"membership_number": entity.membership_number},
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def transfer(
        self,
        tenant_id: UUID,
        member_id: UUID,
        body: MemberTransferRequest,
        *,
        actor: CurrentUser | None = None,
    ) -> Member:
        """Transfer membership to another program: create target member, mark source transferred."""
        source = await self._get_member(tenant_id, member_id)
        action = MembershipLifecycleAction.TRANSFER
        ensure_lifecycle_transition(action=action, current=source.status)
        target_program = await self.programs.get(tenant_id, body.target_program_id)
        if target_program is None:
            raise NotFoundError("برنامه مقصد یافت نشد", error_code="program_not_found")
        if target_program.id == source.program_id:
            raise AppError(
                "برنامه مقصد باید متفاوت باشد",
                status_code=422,
                error_code="same_program_transfer",
            )
        if body.target_tier_id is not None:
            tier = await self.tiers.get(tenant_id, body.target_tier_id)
            if tier is None or tier.program_id != body.target_program_id:
                raise NotFoundError("سطح عضویت مقصد یافت نشد", error_code="tier_not_found")
        reason = validate_reason(body.reason)
        actor_id = actor.user_id if actor else None
        now = _now()
        target = Member(
            tenant_id=tenant_id,
            program_id=body.target_program_id,
            membership_number=f"MBR-{uuid4().hex[:10].upper()}",
            external_customer_ref=source.external_customer_ref,
            display_name=source.display_name,
            email=source.email,
            mobile=source.mobile,
            status=MemberStatus.ACTIVE,
            tier_id=body.target_tier_id,
            enrolled_at=now,
            activated_at=now,
            membership_started_at=now,
            membership_expires_at=source.membership_expires_at
            or compute_expiry(from_when=now),
            profile=source.profile,
            created_by=actor_id,
            updated_by=actor_id,
        )
        await self.members.add(target)
        from_status = source.status
        source.status = MemberStatus.TRANSFERRED
        source.transferred_to_member_id = target.id
        source.version += 1
        source.updated_by = actor_id
        await self._record_lifecycle(
            tenant_id=tenant_id,
            member=source,
            action=action,
            from_status=from_status,
            to_status=MemberStatus.TRANSFERRED,
            reason=reason,
            actor_id=actor_id,
            metadata={
                "target_program_id": str(body.target_program_id),
                "target_member_id": str(target.id),
            },
        )
        await self._record_lifecycle(
            tenant_id=tenant_id,
            member=target,
            action=MembershipLifecycleAction.ACTIVATE,
            from_status=MemberStatus.PENDING,
            to_status=MemberStatus.ACTIVE,
            reason=reason or "transfer_in",
            actor_id=actor_id,
            metadata={"source_member_id": str(source.id)},
        )
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="member",
            entity_id=source.id,
            action=AuditAction.TRANSFER,
            actor_user_id=actor_id,
            message=f"Member {source.membership_number} transferred to {target.membership_number}",
            changes={
                "target_member_id": str(target.id),
                "target_program_id": str(body.target_program_id),
            },
        )
        await self._publish(
            event_type=LoyaltyEventType.MEMBER_TRANSFERRED,
            member=source,
            tenant_id=tenant_id,
            payload={
                "membership_number": source.membership_number,
                "target_member_id": str(target.id),
                "target_program_id": str(body.target_program_id),
            },
        )
        await self._publish(
            event_type=LoyaltyEventType.MEMBER_CREATED,
            member=target,
            tenant_id=tenant_id,
            payload={
                "membership_number": target.membership_number,
                "status": target.status.value,
                "transfer_from": str(source.id),
            },
        )
        await self.session.commit()
        await self.session.refresh(source)
        return source

    async def list_lifecycle(
        self, tenant_id: UUID, member_id: UUID, *, limit: int = 100
    ):
        await self._get_member(tenant_id, member_id)
        return await self.lifecycle.list_for_member(
            tenant_id, member_id, limit=limit
        )
