"""Core referral business logic orchestrating store and rewards."""

from __future__ import annotations

import secrets
import string
from datetime import datetime, timedelta, timezone
from uuid import uuid4

from referral.config import get_referral
from referral.models import (
    Referral,
    ReferralCode,
    ReferralStatus,
    ReferralStats,
)
from referral.store import ReferralStore
from errors import NotFoundError, ValidationError


_CODE_ALPHABET = string.ascii_uppercase + string.digits
_CODE_LENGTH = 8


def _generate_code(length: int = _CODE_LENGTH) -> str:
    """Generate a cryptographically random alphanumeric code."""
    return "".join(secrets.choice(_CODE_ALPHABET) for _ in range(length))


class ReferralService:
    """Orchestrates referral code creation, application, and lifecycle.

    Args:
        store: The persistence backend (in-memory or database).
    """

    def __init__(self, store: ReferralStore) -> None:
        self.store = store

    # -- code management --

    async def create_code(
        self,
        user_id: str,
        *,
        max_uses: int | None = None,
    ) -> ReferralCode:
        """Generate a unique referral code for the given user.

        Args:
            user_id: The user who owns this referral code.
            max_uses: Optional cap on how many times this code can be used.

        Returns:
            The newly created ReferralCode.

        Raises:
            ValueError: If the user already has too many codes.
        """
        config = get_referral()
        existing = await self.store.get_user_codes(user_id)
        active_count = sum(1 for c in existing if c.active)
        if active_count >= config.max_referrals_per_user:
            raise ValidationError(
                f"User {user_id} has reached the maximum of "
                f"{config.max_referrals_per_user} active referral codes."
            )

        now = datetime.now(timezone.utc)
        expires_at = now + timedelta(days=config.expiry_days) if config.expiry_days > 0 else None

        # Ensure uniqueness by retrying on collision
        for _ in range(10):
            code_str = _generate_code()
            if await self.store.get_code(code_str) is None:
                break
        else:
            raise RuntimeError("Failed to generate a unique referral code after 10 attempts.")

        code = ReferralCode(
            id=uuid4(),
            code=code_str,
            user_id=user_id,
            uses=0,
            max_uses=max_uses or config.default_code_max_uses,
            created_at=now,
            expires_at=expires_at,
            active=True,
        )
        return await self.store.save_code(code)

    async def get_code(self, code: str) -> ReferralCode | None:
        """Look up a referral code by its string value."""
        return await self.store.get_code(code)

    async def get_user_codes(self, user_id: str) -> list[ReferralCode]:
        """Return all referral codes belonging to a user."""
        return await self.store.get_user_codes(user_id)

    async def deactivate_code(self, code: str) -> ReferralCode | None:
        """Deactivate a referral code so it can no longer be used."""
        existing = await self.store.get_code(code)
        if existing is None:
            return None
        existing.active = False
        return await self.store.update_code(existing)

    # -- referral application --

    async def apply_referral(
        self,
        code: str,
        referred_user_id: str,
    ) -> Referral:
        """Apply a referral code for a new user.

        Validates the code is active, not expired, not at max uses, and
        the referred user hasn't already been referred.

        Args:
            code: The referral code string.
            referred_user_id: The ID of the user being referred.

        Returns:
            The created Referral record.

        Raises:
            ValueError: If the code is invalid, expired, exhausted, or the
                user is self-referring / already referred.
        """
        referral_code = await self.store.get_code(code)
        if referral_code is None:
            raise NotFoundError(f"Referral code '{code}' not found.")

        if not referral_code.active:
            raise ValidationError(f"Referral code '{code}' is no longer active.")

        now = datetime.now(timezone.utc)
        if referral_code.expires_at is not None and now > referral_code.expires_at:
            raise ValidationError(f"Referral code '{code}' has expired.")

        if referral_code.max_uses is not None and referral_code.uses >= referral_code.max_uses:
            raise ValidationError(f"Referral code '{code}' has reached its maximum uses.")

        if referral_code.user_id == referred_user_id:
            raise ValidationError("Users cannot refer themselves.")

        # Check if user was already referred
        existing = await self.store.list_referrals(referred_id=referred_user_id, limit=1)
        if existing:
            raise ValidationError(f"User {referred_user_id} has already been referred.")

        referral = Referral(
            id=uuid4(),
            referrer_id=referral_code.user_id,
            referred_id=referred_user_id,
            code=code,
            status=ReferralStatus.PENDING,
            created_at=now,
        )
        saved = await self.store.save_referral(referral)

        # Increment usage counter
        referral_code.uses += 1
        await self.store.update_code(referral_code)

        return saved

    # -- referral completion --

    async def complete_referral(self, referral_id: str | object) -> Referral:
        """Mark a referral as completed.

        Args:
            referral_id: UUID of the referral (string or UUID).

        Returns:
            The updated Referral.

        Raises:
            ValueError: If the referral is not found or not in PENDING state.
        """
        from uuid import UUID as _UUID

        rid = referral_id if isinstance(referral_id, _UUID) else _UUID(str(referral_id))
        referral = await self.store.get_referral(rid)
        if referral is None:
            raise NotFoundError(f"Referral {referral_id} not found.")

        if referral.status != ReferralStatus.PENDING:
            raise ValidationError(
                f"Referral {referral_id} cannot be completed "
                f"(current status: {referral.status.value})."
            )

        referral.status = ReferralStatus.COMPLETED
        referral.completed_at = datetime.now(timezone.utc)
        return await self.store.update_referral(referral)

    # -- stats & maintenance --

    async def get_stats(self, user_id: str) -> ReferralStats:
        """Return aggregate referral statistics for a user."""
        return await self.store.get_stats(user_id)

    async def expire_stale(self) -> int:
        """Expire all pending referrals older than the configured expiry window.

        Returns:
            Number of referrals expired.
        """
        config = get_referral()
        cutoff = datetime.now(timezone.utc) - timedelta(days=config.expiry_days)
        return await self.store.expire_stale(cutoff)

    async def get_leaderboard(self, limit: int = 10) -> list[ReferralStats]:
        """Return the top referrers sorted by completed referrals."""
        return await self.store.get_leaderboard(limit)
