"""Service layer for phantom user claiming and merging.

Phantom users are created during guest landing purchases when Bot.get_chat() fails —
the user record has @username but no telegram_id. When the real user later presses /start,
we match by username and either claim or merge the phantom into their active account.
"""

from __future__ import annotations

from datetime import UTC, datetime
from typing import Literal

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

from app.database.crud.rbac import AuditLogCRUD
from app.database.crud.user import get_user_by_telegram_id
from app.database.models import User, UserStatus
from app.services.account_merge_service import execute_merge
from app.services.subscription_service import SubscriptionService
from app.utils.user_utils import generate_unique_referral_code
from app.utils.validators import sanitize_telegram_name


logger = structlog.get_logger(__name__)


async def claim_phantom(
    db: AsyncSession,
    phantom: User,
    *,
    telegram_id: int,
    username: str | None,
    first_name: str | None,
    last_name: str | None,
    language: str,
    referrer_id: int | None,
) -> tuple[bool, User | None]:
    """Claim a phantom user by backfilling Telegram profile data.

    Commits internally on success; rolls back on IntegrityError.
    Returns (success, user). On IntegrityError falls back to existing user lookup.

    Note: Phantom users created when Bot.get_chat() fails at purchase time are matched
    by username only. Since Telegram usernames are changeable and reassignable, this is
    inherently vulnerable to username change attacks. When Bot.get_chat() succeeds at
    purchase time, telegram_id is stored on the user and the phantom path is not used.
    """
    phantom.telegram_id = telegram_id
    phantom.username = username
    phantom.first_name = sanitize_telegram_name(first_name)
    phantom.last_name = sanitize_telegram_name(last_name)
    phantom.language = language
    phantom.status = UserStatus.ACTIVE.value
    if referrer_id and referrer_id != phantom.id:
        phantom.referred_by_id = referrer_id
    if not phantom.referral_code:
        phantom.referral_code = await generate_unique_referral_code(db, telegram_id)
    phantom.updated_at = datetime.now(UTC)
    phantom.last_activity = datetime.now(UTC)

    # Write audit log in a savepoint — if it fails, the claim mutations are not affected
    try:
        async with db.begin_nested():
            await AuditLogCRUD.create(
                db,
                user_id=phantom.id,
                action='phantom_claimed',
                resource_type='user',
                resource_id=str(phantom.id),
                details={
                    'telegram_id': telegram_id,
                    'username': username,
                },
                status='success',
            )
    except Exception:
        logger.warning('Failed to write phantom claim audit log', phantom_id=phantom.id, exc_info=True)

    try:
        await db.commit()
    except IntegrityError:
        await db.rollback()
        logger.warning(
            'IntegrityError claiming phantom user, falling back to existing user lookup',
            phantom_user_id=phantom.id,
            telegram_id=telegram_id,
        )
        existing = await get_user_by_telegram_id(db, telegram_id)
        return False, existing
    await db.refresh(phantom, ['subscriptions'])

    # SECURITY NOTE: Phantom matched by username only (telegram_id was unknown at purchase time).
    # Telegram usernames are changeable/reassignable, so the claimer may not be the intended
    # recipient. This is logged at WARNING for admin audit. A confirmation flow would be needed
    # to fully prevent username spoofing attacks on phantom claims.
    logger.warning(
        'Phantom user claimed by username match (verify intended recipient)',
        phantom_user_id=phantom.id,
        telegram_id=telegram_id,
        username=username,
        has_subscription=bool(phantom.subscriptions),
    )

    # Sync Remnawave panel with updated user data (telegram_id, username, etc.)
    subs = phantom.subscriptions or []
    if subs:
        subscription_service = SubscriptionService()
        for sub in subs:
            try:
                await subscription_service.update_remnawave_user(db, sub)
            except Exception:
                logger.warning(
                    'Failed to update Remnawave panel after phantom claim',
                    phantom_user_id=phantom.id,
                    subscription_id=sub.id,
                    exc_info=True,
                )
                from app.services.remnawave_retry_queue import remnawave_retry_queue

                if hasattr(sub, 'id') and hasattr(sub, 'user_id'):
                    remnawave_retry_queue.enqueue(
                        subscription_id=sub.id,
                        user_id=sub.user_id,
                        action='update',
                    )

    return True, phantom


async def merge_phantom_into_user(
    db: AsyncSession,
    phantom: User,
    active_user: User,
) -> bool:
    """Merge phantom user into active user using the full account merge service.

    Uses execute_merge which handles 30+ tables, subscription transfer, balance,
    unique constraint safety, and soft-deletion. Caller is responsible for commit/rollback.

    Returns True if a subscription was transferred from phantom (caller should sync
    Remnawave panel AFTER commit via ``sync_remnawave_after_phantom_merge``).
    """
    # Determine which subscription to keep: phantom's if active user has none, otherwise active's
    await db.refresh(phantom, ['subscriptions'])
    await db.refresh(active_user, ['subscriptions'])
    phantom_subs = getattr(phantom, 'subscriptions', None) or []
    active_subs = getattr(active_user, 'subscriptions', None) or []
    keep_from: Literal['primary', 'secondary'] = 'secondary' if phantom_subs and not active_subs else 'primary'

    logger.warning(
        'Merging phantom user into active user via execute_merge',
        phantom_id=phantom.id,
        active_user_id=active_user.id,
        keep_subscription_from=keep_from,
        phantom_has_sub=phantom.subscription is not None,
        active_has_sub=active_user.subscription is not None,
    )

    await execute_merge(
        db,
        primary_user_id=active_user.id,
        secondary_user_id=phantom.id,
        keep_subscription_from=keep_from,
        provider='phantom_merge',
    )

    # Durable audit log in a savepoint — if it fails, the merge itself is not affected
    try:
        async with db.begin_nested():
            await AuditLogCRUD.create(
                db,
                user_id=active_user.id,
                action='phantom_merged',
                resource_type='user',
                resource_id=str(phantom.id),
                details={
                    'phantom_id': phantom.id,
                    'active_user_id': active_user.id,
                    'keep_subscription_from': keep_from,
                    'phantom_username': phantom.username,
                },
                status='success',
            )
    except Exception:
        logger.warning(
            'Failed to write phantom merge audit log',
            phantom_id=phantom.id,
            active_user_id=active_user.id,
            exc_info=True,
        )

    return keep_from == 'secondary'


async def sync_remnawave_after_phantom_merge(db: AsyncSession, user: User) -> None:
    """Sync Remnawave panel after a phantom merge that transferred a subscription.

    Must be called AFTER db.commit() to avoid holding FOR UPDATE locks during HTTP calls.
    """
    await db.refresh(user, ['subscriptions'])
    subs = getattr(user, 'subscriptions', None) or []
    if not subs:
        return
    try:
        subscription_service = SubscriptionService()
        for sub in subs:
            await subscription_service.update_remnawave_user(db, sub)
    except Exception:
        logger.warning(
            'Failed to update Remnawave panel after phantom merge',
            user_id=user.id,
            exc_info=True,
        )
        from app.services.remnawave_retry_queue import remnawave_retry_queue

        for sub in subs:
            if hasattr(sub, 'id') and hasattr(sub, 'user_id'):
                remnawave_retry_queue.enqueue(
                    subscription_id=sub.id,
                    user_id=sub.user_id,
                    action='update',
                )
