"""
Bidding Rate Distribution and Sequential/Broadcast Dispatch Engine.
"""

from __future__ import annotations

import logging
from decimal import Decimal
from django.conf import settings
from django.contrib.auth import get_user_model
from django.db import models, transaction
from django.utils import timezone

from accounts.models import PlatformSettings, TransporterProfile, UserRole, Vehicle
from core.models import Bid, RateRequest, Shipment, Trip, Zone
from core.load_discovery import vehicle_matches_required_type
from core.countries import (
    normalize_country_code,
    transporter_load_search_radius_km,
)
from core.pricing.service import country_code_for_point
from api.notify import notify_users
from api.traccar import get_latest_device_position, get_latest_positions_map
from api.trip_status import record_trip_status

User = get_user_model()
logger = logging.getLogger(__name__)

ACTIVE_TRIP_STATUSES = (
    Trip.Status.ASSIGNED,
    Trip.Status.EN_ROUTE,
    Trip.Status.ARRIVED_PICKUP,
    Trip.Status.LOADED,
    Trip.Status.IN_TRANSIT,
    Trip.Status.ARRIVED_DELIVERY,
)


def _haversine_km(lat1: float, lon1: float, lat2: float, lon2: float) -> float:
    import math

    radius_km = 6371.0
    dlat = math.radians(lat2 - lat1)
    dlon = math.radians(lon2 - lon1)
    a = (
        math.sin(dlat / 2) ** 2
        + math.cos(math.radians(lat1)) * math.cos(math.radians(lat2)) * math.sin(dlon / 2) ** 2
    )
    c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
    return radius_km * c


def find_eligible_drivers_for_shipment(shipment: Shipment) -> list[dict]:
    """
    Find all available, verified drivers compatible with this shipment,
    ranked by ascending distance from pickup.
    """
    if shipment.pickup_lat is None or shipment.pickup_lon is None:
        return []

    try:
        pickup_lat = float(shipment.pickup_lat)
        pickup_lon = float(shipment.pickup_lon)
    except (TypeError, ValueError):
        return []

    shipment_country = normalize_country_code(
        shipment.pickup_country_code or country_code_for_point(pickup_lat, pickup_lon) or ''
    )

    try:
        positions_map = get_latest_positions_map()
    except Exception:
        positions_map = {}

    busy_driver_ids = set(
        Trip.objects.filter(status__in=ACTIVE_TRIP_STATUSES)
        .values_list('transporter_id', flat=True)
    ) | set(
        Trip.objects.filter(status__in=ACTIVE_TRIP_STATUSES, assigned_driver__isnull=False)
        .values_list('assigned_driver_id', flat=True)
    )

    eligible_candidates = []

    # Query all active, verified transporter profiles
    profiles = (
        TransporterProfile.objects.filter(
            user__is_active=True,
            user__role__role=UserRole.Role.TRANSPORTER,
            documents_verified=True,
        )
        .exclude(user_id__in=busy_driver_ids)
        .select_related('user', 'user__role')
    )

    for profile in profiles:
        user = profile.user
        tc_id = (str(profile.tc_id or '')).strip()
        if not tc_id:
            continue

        pos = positions_map.get(tc_id)
        if not pos:
            try:
                pos = get_latest_device_position(tc_id)
            except Exception:
                pos = None
        if not pos:
            continue

        try:
            driver_lat = float(pos['latitude'])
            driver_lon = float(pos['longitude'])
        except (TypeError, ValueError, KeyError):
            continue

        driver_country = country_code_for_point(driver_lat, driver_lon)
        if not driver_country:
            continue

        # Load type rules (Local vs Country-to-Country)
        if profile.local:
            if not shipment.local or driver_country != shipment_country:
                continue
        elif profile.country_to_country:
            if not shipment.country_to_country and not shipment.local:
                continue
            if driver_country != shipment_country:
                continue

        # Search radius
        radius_km, _ = transporter_load_search_radius_km(driver_country)
        dist_km = _haversine_km(pickup_lat, pickup_lon, driver_lat, driver_lon)
        if dist_km > radius_km:
            continue

        # Vehicle type compatibility
        user_vehicles = Vehicle.objects.filter(
            models.Q(owner=user) | models.Q(assigned_driver=user),
            is_verified=True,
            is_active=True,
        )
        matching_vehicle = None
        for v in user_vehicles:
            if vehicle_matches_required_type(v, shipment.vehicle_type_required):
                matching_vehicle = v
                break

        if not matching_vehicle:
            continue

        eligible_candidates.append({
            'driver': user,
            'profile': profile,
            'vehicle': matching_vehicle,
            'distance_km': round(dist_km, 2),
            'lat': driver_lat,
            'lon': driver_lon,
        })

    eligible_candidates.sort(key=lambda x: x['distance_km'])
    return eligible_candidates


def dispatch_rate_requests_for_shipment(
    shipment: Shipment,
    *,
    batch_number: int | None = None,
) -> list[RateRequest]:
    """
    Dispatch rate requests to the next batch of eligible drivers based on
    PlatformSettings.bidding_rate_distribution_count.
    """
    if shipment.status != Shipment.Status.PUBLISHED:
        logger.info('Cannot dispatch rate requests: shipment #%s status is %s', shipment.pk, shipment.status)
        return []

    ps = PlatformSettings.load()
    batch_size = max(1, int(getattr(ps, 'bidding_rate_distribution_count', 1) or 1))

    existing_requests = RateRequest.objects.filter(shipment=shipment)
    contacted_driver_ids = set(existing_requests.values_list('driver_id', flat=True))

    if batch_number is None:
        max_batch = existing_requests.aggregate(models.Max('batch_number'))['batch_number__max']
        batch_number = (max_batch or 0) + 1

    eligible = find_eligible_drivers_for_shipment(shipment)
    uncontacted = [c for c in eligible if c['driver'].id not in contacted_driver_ids]

    if not uncontacted:
        logger.info('No remaining uncontacted drivers for shipment #%s', shipment.pk)
        return []

    to_dispatch = uncontacted[:batch_size]
    created_requests: list[RateRequest] = []

    with transaction.atomic():
        for item in to_dispatch:
            rr, created = RateRequest.objects.get_or_create(
                shipment=shipment,
                driver=item['driver'],
                defaults={
                    'batch_number': batch_number,
                    'status': RateRequest.Status.PENDING,
                    'distance_km': Decimal(str(item['distance_km'])),
                    'offered_price': shipment.suggested_price,
                    'currency': shipment.currency or 'USD',
                },
            )
            if created:
                created_requests.append(rr)

    for rr in created_requests:
        try:
            notify_users(
                [rr.driver_id],
                'RATE_REQUEST',
                {
                    'rate_request_id': str(rr.id),
                    'shipment_id': str(shipment.id),
                    'unique_id': shipment.unique_id or str(shipment.id),
                    'offered_price': str(rr.offered_price or ''),
                    'currency': rr.currency,
                    'batch_number': str(rr.batch_number),
                    'pickup_address': shipment.pickup_address,
                    'delivery_address': shipment.delivery_address,
                    'cargo_type': shipment.cargo_type,
                },
                title='New Load Rate Request',
                body=f'A new load ({shipment.cargo_type}) is requested for you with offer {rr.offered_price or ""} {rr.currency}.',
            )
        except Exception:
            logger.exception('Failed to notify driver %s for rate request %s', rr.driver_id, rr.id)

    logger.info(
        'Dispatched batch #%s of %s rate requests for shipment #%s',
        batch_number,
        len(created_requests),
        shipment.pk,
    )
    return created_requests


def advance_rate_requests_batch_if_needed(shipment: Shipment) -> list[RateRequest]:
    """
    Check if the current batch is fully exhausted (all rejected/cancelled/expired).
    If so, automatically dispatch the next batch.
    """
    if shipment.status != Shipment.Status.PUBLISHED:
        return []

    if RateRequest.objects.filter(shipment=shipment, status=RateRequest.Status.ACCEPTED).exists():
        return []

    latest_batch = RateRequest.objects.filter(shipment=shipment).aggregate(models.Max('batch_number'))['batch_number__max']
    if latest_batch is None:
        return dispatch_rate_requests_for_shipment(shipment)

    # Check if any requests in latest batch are still pending
    pending_in_batch = RateRequest.objects.filter(
        shipment=shipment,
        batch_number=latest_batch,
        status=RateRequest.Status.PENDING,
    ).exists()

    if not pending_in_batch:
        logger.info(
            'Batch #%s for shipment #%s fully resolved with no acceptance; advancing to next batch.',
            latest_batch,
            shipment.pk,
        )
        return dispatch_rate_requests_for_shipment(shipment, batch_number=latest_batch + 1)

    return []


def handle_rate_request_accept(rate_request: RateRequest) -> tuple[Trip, RateRequest]:
    """
    Accept a rate request:
    1. Mark this rate request as ACCEPTED.
    2. Cancel all other pending rate requests for this shipment.
    3. Create an accepted Bid.
    4. Mark shipment as ASSIGNED and create the Trip.
    """
    shipment = rate_request.shipment
    if shipment.status != Shipment.Status.PUBLISHED:
        raise ValueError('Shipment is no longer available.')

    if rate_request.status != RateRequest.Status.PENDING:
        raise ValueError(f'Rate request is already {rate_request.status.lower()}.')

    with transaction.atomic():
        # Lock shipment row
        shipment = Shipment.objects.select_for_update().get(pk=shipment.pk)
        if shipment.status != Shipment.Status.PUBLISHED:
            raise ValueError('Shipment is no longer available.')

        rate_request.status = RateRequest.Status.ACCEPTED
        rate_request.responded_at = timezone.now()
        rate_request.save(update_fields=['status', 'responded_at', 'updated_at'])

        # Cancel all other pending requests for this shipment
        RateRequest.objects.filter(
            shipment=shipment,
            status=RateRequest.Status.PENDING,
        ).exclude(pk=rate_request.pk).update(
            status=RateRequest.Status.CANCELLED,
            responded_at=timezone.now(),
            updated_at=timezone.now(),
        )

        amount = rate_request.offered_price or shipment.suggested_price or Decimal('0.00')

        # Create or update accepted bid
        bid = Bid.objects.create(
            shipment=shipment,
            transporter=rate_request.driver,
            amount=amount,
            status=Bid.Status.ACCEPTED,
        )

        shipment.status = Shipment.Status.ASSIGNED
        shipment.save(update_fields=['status', 'updated_at'])

        # Resolve assigned driver if driver is linked fleet driver
        assigned_driver = None
        profile = getattr(rate_request.driver, 'transporter_profile', None)
        if profile and profile.account_type in (
            TransporterProfile.AccountType.DRIVER,
            TransporterProfile.AccountType.TRANSPORTER_DRIVER,
        ):
            assigned_driver = rate_request.driver

        trip = Trip.objects.create(
            shipment=shipment,
            accepted_bid=bid,
            transporter=rate_request.driver,
            assigned_driver=assigned_driver,
            agreed_price=amount,
            status=Trip.Status.ASSIGNED,
        )
        record_trip_status(trip, Trip.Status.ASSIGNED)

    # Notifications
    try:
        notify_users(
            [shipment.shipper_id],
            'bid_accepted',
            {
                'trip_id': str(trip.id),
                'shipment_id': str(shipment.id),
                'driver_id': str(rate_request.driver_id),
                'bid_id': str(bid.id),
            },
            title='Driver Accepted Load',
            body=f'Driver {rate_request.driver.get_full_name() or rate_request.driver.email} accepted load #{shipment.unique_id or shipment.id}.',
        )
    except Exception:
        logger.exception('Failed to notify shipper of rate request acceptance.')

    return trip, rate_request


def handle_rate_request_reject(rate_request: RateRequest, reason: str = '') -> RateRequest:
    """
    Reject a rate request and advance to the next batch if all requests in the batch are resolved.
    """
    if rate_request.status != RateRequest.Status.PENDING:
        raise ValueError(f'Rate request is already {rate_request.status.lower()}.')

    with transaction.atomic():
        rate_request.status = RateRequest.Status.REJECTED
        rate_request.rejection_reason = (reason or '').strip()
        rate_request.responded_at = timezone.now()
        rate_request.save(update_fields=['status', 'rejection_reason', 'responded_at', 'updated_at'])

    advance_rate_requests_batch_if_needed(rate_request.shipment)
    return rate_request
