import logging
import os
from typing import Iterable

import requests
from django.contrib.auth import get_user_model
from django.db import transaction
from django.utils import timezone

from .models import Notification, NotificationCampaign, UserDeviceToken

logger = logging.getLogger(__name__)
User = get_user_model()


def _build_audience_queryset(campaign: NotificationCampaign):
    qs = User.objects.all()
    if campaign.audience == 'active':
        qs = qs.filter(is_active=True)
    elif campaign.audience == 'role' and campaign.role:
        qs = qs.filter(role__iexact=campaign.role)
    return qs


def _send_fcm_push(tokens: Iterable[str], title: str, body: str, data: dict | None = None) -> tuple[int, int]:
    token_list = [t for t in tokens if t]
    if not token_list:
        return 0, 0

    server_key = os.getenv('FCM_SERVER_KEY', '').strip()
    if not server_key:
        logger.warning('FCM_SERVER_KEY not configured; skipping push delivery.')
        return 0, len(token_list)

    url = 'https://fcm.googleapis.com/fcm/send'
    headers = {
        'Authorization': f'key={server_key}',
        'Content-Type': 'application/json',
    }

    payload = {
        'registration_ids': token_list,
        'notification': {'title': title, 'body': body},
        'data': data or {},
        'priority': 'high',
    }

    try:
        response = requests.post(url, json=payload, headers=headers, timeout=10)
        response.raise_for_status()
        body_json = response.json()
        success = int(body_json.get('success', 0))
        failure = int(body_json.get('failure', 0))
        return success, failure
    except Exception as exc:
        logger.exception('FCM push failed: %s', exc)
        return 0, len(token_list)


def dispatch_campaign(campaign: NotificationCampaign) -> dict:
    recipients = list(_build_audience_queryset(campaign))
    now = timezone.now()
    banner_image_url = campaign.banner_image.url if getattr(campaign, 'banner_image', None) else None

    total_in_app_sent = 0
    total_push_success = 0
    total_push_failure = 0
    total_push_skipped_no_token = 0

    with transaction.atomic():
        for user in recipients:
            recipient = user.mobile or user.email or str(user.id)

            Notification.objects.create(
                user=user,
                channel='in_app',
                recipient=recipient,
                subject=campaign.title,
                message=campaign.message,
                payload={'campaign_id': str(campaign.id), 'banner_image_url': banner_image_url},
                status='sent',
                provider='internal',
                sent_at=now,
                is_read=False,
            )
            total_in_app_sent += 1

        if campaign.delivery_channel in {'push', 'both'}:
            for user in recipients:
                tokens = list(
                    UserDeviceToken.objects.filter(user=user, is_active=True).values_list('device_token', flat=True)
                )
                recipient = user.mobile or user.email or str(user.id)

                if not tokens:
                    Notification.objects.create(
                        user=user,
                        channel='push',
                        recipient=recipient,
                        subject=campaign.title,
                        message=campaign.message,
                        payload={'campaign_id': str(campaign.id), 'reason': 'no_active_device_token'},
                        status='failed',
                        provider='fcm',
                        sent_at=now,
                    )
                    total_push_failure += 1
                    total_push_skipped_no_token += 1
                    continue

                success, failure = _send_fcm_push(
                    tokens=tokens,
                    title=campaign.title,
                    body=campaign.message,
                    data={'campaign_id': str(campaign.id), 'banner_image_url': banner_image_url},
                )
                if success > 0 or failure > 0:
                    Notification.objects.create(
                        user=user,
                        channel='push',
                        recipient=recipient,
                        subject=campaign.title,
                        message=campaign.message,
                        payload={'campaign_id': str(campaign.id), 'attempted_tokens': len(tokens), 'banner_image_url': banner_image_url},
                        status='sent' if failure == 0 else ('sent' if success > 0 else 'failed'),
                        provider='fcm',
                        sent_at=now,
                    )

                total_push_success += success
                total_push_failure += failure

        campaign.total_recipients = len(recipients)
        campaign.total_sent = total_in_app_sent + total_push_success
        campaign.total_failed = total_push_failure
        campaign.status = 'sent' if (total_in_app_sent > 0 or total_push_success > 0) else 'failed'
        campaign.sent_at = now
        campaign.save(update_fields=['total_recipients', 'total_sent', 'total_failed', 'status', 'sent_at', 'updated_at'])

    return {
        'recipients': len(recipients),
        'in_app_sent': total_in_app_sent,
        'push_sent': total_push_success,
        'push_failed': total_push_failure,
        'push_skipped_no_token': total_push_skipped_no_token,
        'campaign_id': str(campaign.id),
    }


def process_due_campaigns() -> dict:
    now = timezone.now()
    due = NotificationCampaign.objects.filter(status='scheduled', scheduled_at__isnull=False, scheduled_at__lte=now)
    processed = 0
    failures = 0

    for campaign in due:
        try:
            dispatch_campaign(campaign)
            processed += 1
        except Exception:
            failures += 1
            campaign.status = 'failed'
            campaign.save(update_fields=['status', 'updated_at'])
            logger.exception('Failed to process campaign %s', campaign.id)

    return {'processed': processed, 'failed': failures}
