from datetime import datetime
import json
import logging
import math
import os
import re
import secrets

from django.conf import settings as django_settings
from django.contrib.auth import authenticate, get_user_model
from django.db import IntegrityError, models
from django.db.models import Prefetch, Q
from django.utils import timezone
from rest_framework import generics, serializers
from rest_framework.decorators import api_view, permission_classes
from rest_framework.views import APIView
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.authtoken.models import Token

from accounts.models import (
    UserRole,
    ShipperProfile,
    TransporterProfile,
    TransporterDriverLink,
    KYCDocument,
    Vehicle,
    VehicleDocument,
    PlatformSettings,
    PasswordResetOTP,
)
from core.models import (
    Shipment,
    Bid,
    Trip,
    TripLocation,
    Conversation,
    Message,
    ProofOfDelivery,
    PODPhoto,
    FavoriteTransporter,
    RateRequest,
)
from core.shipment_publish import (
    enqueue_shipment_publish_side_effects,
    prepare_shipment_for_publish,
)
from core.countries import (
    extract_flat_shipment_validation_error,
    normalize_country_code,
    transporter_load_search_radius_km,
    zone_search_radius_km,
)
from core.load_type import (
    LOAD_TYPE_COUNTRY_TO_COUNTRY,
    parse_load_type_query,
    shipment_matches_load_type_filter,
    transporter_can_access_shipment,
)
from core.currencies import currency_for_shipment
from core.pricing.service import country_code_for_point
from core.load_discovery import (
    find_matching_vehicle,
    matching_vehicle_type,
    parse_required_country_code,
    shipment_matches_transporter_vehicle_types,
    transporter_verified_active_vehicles,
    vehicle_capacity_tons,
    vehicle_matches_required_type,
    vehicle_type_list,
)
from billing.models import Payment
from billing.services import ensure_invoice_for_completed_trip, settlement_amount_for_bid

from .permissions import (
    IsFleetDriverTransporter,
    IsFleetOwnerTransporter,
    IsIndividualDriverTransporter,
    IsPlatformAdmin,
    IsShipper,
    IsShipperVerified,
    IsTransporter,
    IsTransporterVerified,
)
from .response import api_response
from .storage_utils import ensure_upload_dir
from .traccar import get_latest_device_position, get_latest_positions_map
from .notify import notify_users
from .trip_notifications import trip_progress_notify_user_ids
from .trip_status import record_trip_status
from .serializers import (
    ShipperRegisterSerializer,
    ShipperProfileSerializer,
    TransporterRegisterSerializer,
    TransporterProfileSerializer,
    TransporterDocumentUploadSerializer,
    transporter_doc_types_for_account,
    KYCDocumentSerializer,
    KYCDocumentUploadSerializer,
    ShipmentSerializer,
    TransporterAvailableShipmentSerializer,
    ShipmentCreateUpdateSerializer,
    ShipmentPriceEstimateSerializer,
    ShipperDriverLocationsSerializer,
    BidListSerializer,
    BidDetailSerializer,
    BidCounterSerializer,
    TransporterBidSubmitSerializer,
    TripListSerializer,
    TripDetailSerializer,
    ShipperTripListSerializer,
    ShipperTripDetailSerializer,
    TransporterTripDetailSerializer,
    TransporterCompletedTripSerializer,
    TripLocationSerializer,
    MessageSerializer,
    MessageCreateSerializer,
    PODSerializer,
    VehicleSerializer,
    VehicleDriverAssignSerializer,
    VehicleDocumentSerializer,
    VehicleDocumentUploadSerializer,
    VehicleDocumentReviewSerializer,
    VehicleVerificationSerializer,
    PlatformSettingsSerializer,
    DriverTripStatusSerializer,
    TripAssignDriverSerializer,
    FleetDriverCreateSerializer,
    FleetDriverUpdateSerializer,
    FleetDriverSerializer,
    RateRequestSerializer,
    RateRequestRejectSerializer,
    save_avatar_and_get_url,
    upsert_driver_kyc_documents,
    TRANSPORTER_DRIVER_DOC_TYPES,
    get_shipper_favorite_transporter_ids,
    resolve_trips_assigned_vehicles,
)

User = get_user_model()
logger = logging.getLogger('api')


def _haversine_km(lat1, lon1, lat2, lon2):
    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 _user_has_verified_active_vehicle(user, *, owner_only=False):
    if owner_only:
        return user.fleet_vehicles.filter(is_verified=True, is_active=True).exists()
    return Vehicle.objects.filter(
        is_verified=True,
        is_active=True,
    ).filter(models.Q(owner=user) | models.Q(assigned_driver=user)).exists()


def _user_has_matching_verified_vehicle(user, vehicle_type_required, *, owner_only=False):
    from core.load_discovery import vehicle_matches_required_type

    qs = Vehicle.objects.filter(is_verified=True, is_active=True)
    if owner_only:
        qs = qs.filter(owner=user)
    else:
        qs = qs.filter(models.Q(owner=user) | models.Q(assigned_driver=user))
    return any(vehicle_matches_required_type(v, vehicle_type_required) for v in qs)


def _drivers_within_radius(
    ref_lat,
    ref_lon,
    radius_km,
    profiles,
    positions_map,
    *,
    owner_only=False,
    vehicle_type_required=None,
):
    nearby = []
    ref_lat_f = float(ref_lat)
    ref_lon_f = float(ref_lon)

    for profile in profiles:
        user = profile.user
        if not user.is_active:
            continue
        if vehicle_type_required:
            if not _user_has_matching_verified_vehicle(
                user,
                vehicle_type_required,
                owner_only=owner_only,
            ):
                continue
        elif not _user_has_verified_active_vehicle(user, owner_only=owner_only):
            continue
        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:
            lat = float(pos['latitude'])
            lon = float(pos['longitude'])
        except (TypeError, ValueError):
            continue
        dist = _haversine_km(ref_lat_f, ref_lon_f, lat, lon)
        if dist > radius_km:
            continue
        phone = ''
        try:
            phone = user.role.phone or ''
        except Exception:
            phone = ''
        nearby.append(
            {
                'transporter_id': user.id,
                'email': user.email,
                'first_name': user.first_name,
                'last_name': user.last_name,
                'phone': phone,
                'company_name': profile.company_name or '',
                'tc_id': profile.tc_id,
                'distance_km': round(dist, 3),
                'position': {'lat': lat, 'lon': lon},
            }
        )

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


def _shipper_driver_profiles_queryset():
    return (
        TransporterProfile.objects.filter(
            account_type__in=(
                TransporterProfile.AccountType.DRIVER,
                TransporterProfile.AccountType.TRANSPORTER_DRIVER,
            ),
            documents_verified=True,
        )
        .exclude(tc_id__isnull=True)
        .exclude(tc_id='')
        .select_related('user', 'user__role')
    )


def _extract_weight_value(weight_text):
    """
    Return normalized shipment weight in *tons* for compatibility checks.

    - If weight_text includes units:
      - kg -> tons
      - ton(s) -> as-is
      - lb(s) -> lbs->kg->tons
    - If weight_text is a bare number (no unit letters), we treat it as
      WEIGHT_NUMERIC_DEFAULT_UNIT (default: kg).
    """
    if weight_text is None or weight_text == '':
        return None

    weight_str = str(weight_text).strip()
    match = re.search(r'(\d+(?:\.\d+)?)', weight_str)
    if not match:
        return None

    try:
        value = float(match.group(1))
    except (TypeError, ValueError):
        return None

    unit_text = weight_str.lower()
    has_letters = bool(re.search(r'[a-zA-Z]', unit_text))
    default_unit = str(getattr(django_settings, 'WEIGHT_NUMERIC_DEFAULT_UNIT', 'kg')).strip().lower()

    # Known unit conversions
    if 'kg' in unit_text:
        return value / 1000.0
    if 'ton' in unit_text:
        return value
    if 'lb' in unit_text:
        return (value * 0.453592) / 1000.0  # lbs -> kg -> tons

    # Unknown / no-unit cases
    if not has_letters:
        if default_unit == 'kg':
            return value / 1000.0
        return value  # assume tons when default_unit is not kg

    # If letters exist but we don't recognize the unit, fall back to default.
    if default_unit == 'kg':
        return value / 1000.0
    return value


def _is_bid_locked(shipment):
    if shipment.status != Shipment.Status.PUBLISHED:
        return True
    if Bid.objects.filter(shipment=shipment, status=Bid.Status.ACCEPTED).exists():
        return True
    if Trip.objects.filter(shipment=shipment).exists():
        return True
    return False


ACTIVE_BID_STATUSES = (
    Bid.Status.PENDING,
    Bid.Status.COUNTERED,
    Bid.Status.AGREED,
)


def _get_active_bid(shipment, transporter_id):
    return (
        Bid.objects.filter(
            shipment=shipment,
            transporter_id=transporter_id,
            status__in=ACTIVE_BID_STATUSES,
        )
        .order_by('-updated_at', '-id')
        .first()
    )


def _bid_accept_response_data(trip):
    return {
        'bid_id': trip.accepted_bid_id,
        'trip_id': trip.id,
        'unique_id': trip.shipment.unique_id,
        'agreed_price': str(trip.agreed_price),
        'currency': currency_for_shipment(trip.shipment),
    }


def _truthy_query_param(value):
    if value is None:
        return False
    return str(value).strip().lower() in ('1', 'true', 'yes', 'on')


def _extract_dimension_triple(text):
    if not text:
        return None
    nums = re.findall(r'(\d+(?:\.\d+)?)', str(text))
    if len(nums) < 3:
        return None
    try:
        vals = sorted(float(x) for x in nums[:3])
        return vals[0], vals[1], vals[2]
    except (TypeError, ValueError):
        return None


def _vehicle_max_dims_sorted(vehicle):
    if vehicle.max_length_m is None or vehicle.max_width_m is None or vehicle.max_height_m is None:
        return None
    try:
        vals = sorted([
            float(vehicle.max_length_m),
            float(vehicle.max_width_m),
            float(vehicle.max_height_m),
        ])
        return vals[0], vals[1], vals[2]
    except (TypeError, ValueError):
        return None


def _dims_compatible(shipment, vehicle):
    s_dims = _extract_dimension_triple(shipment.dimensions)
    v_dims = _vehicle_max_dims_sorted(vehicle)
    if s_dims is None or v_dims is None:
        return True
    for sv, vv in zip(s_dims, v_dims):
        if sv > vv + 1e-9:
            return False
    return True


TRIP_SEQUENTIAL_NEXT = {
    Trip.Status.ASSIGNED: {Trip.Status.EN_ROUTE},
    Trip.Status.EN_ROUTE: {Trip.Status.ARRIVED_PICKUP},
    Trip.Status.ARRIVED_PICKUP: {Trip.Status.LOADED},
    Trip.Status.LOADED: {Trip.Status.IN_TRANSIT},
    Trip.Status.IN_TRANSIT: {Trip.Status.ARRIVED_DELIVERY},
    Trip.Status.ARRIVED_DELIVERY: set(),
    Trip.Status.DELIVERED: {Trip.Status.COMPLETED},
}


def _sync_shipment_status_from_trip(trip, new_trip_status):
    shipment = trip.shipment
    mapping = {
        Trip.Status.EN_ROUTE: Shipment.Status.EN_ROUTE,
        Trip.Status.LOADED: Shipment.Status.LOADED,
        Trip.Status.IN_TRANSIT: Shipment.Status.IN_TRANSIT,
        Trip.Status.DELIVERED: Shipment.Status.DELIVERED,
        Trip.Status.COMPLETED: Shipment.Status.COMPLETED,
    }
    target = mapping.get(new_trip_status)
    if target is None:
        return
    shipment.status = target
    shipment.save(update_fields=['status', 'updated_at'])


def _notify_trip_progress(trip, actor, event_type, payload, *, title, body):
    notify_users(
        trip_progress_notify_user_ids(trip, actor),
        event_type,
        payload,
        title=title,
        body=body,
    )


def _get_or_create_negotiation_conversation(shipment, transporter, trip=None):
    conv, _ = Conversation.objects.get_or_create(
        shipment=shipment,
        shipper=shipment.shipper,
        transporter=transporter,
        defaults={'trip': trip},
    )
    if trip and conv.trip_id != trip.id:
        conv.trip = trip
        conv.save(update_fields=['trip'])
    return conv


def _conversation_assigned_driver_id(conversation):
    trip = getattr(conversation, 'trip', None)
    if trip is not None:
        return trip.assigned_driver_id
    if conversation.trip_id:
        return (
            Trip.objects.filter(pk=conversation.trip_id)
            .values_list('assigned_driver_id', flat=True)
            .first()
        )
    return None


def _user_can_access_conversation(user, conversation):
    if user.id in (conversation.shipper_id, conversation.transporter_id):
        return True
    assigned_driver_id = _conversation_assigned_driver_id(conversation)
    return bool(assigned_driver_id and user.id == assigned_driver_id)


def _conversation_message_recipient_ids(conversation, sender_id):
    """Notify shipper, fleet owner/transporter, and assigned fleet driver (excluding sender)."""
    recipient_ids = set()
    if conversation.shipper_id and conversation.shipper_id != sender_id:
        recipient_ids.add(conversation.shipper_id)
    if conversation.transporter_id and conversation.transporter_id != sender_id:
        recipient_ids.add(conversation.transporter_id)
    assigned_driver_id = _conversation_assigned_driver_id(conversation)
    if assigned_driver_id and assigned_driver_id != sender_id:
        recipient_ids.add(assigned_driver_id)
    return list(recipient_ids)


def _chat_counterparty_payload(conversation, viewer):
    if viewer.id == conversation.shipper_id:
        other = conversation.transporter
        role = 'TRANSPORTER'
        profile = getattr(other, 'transporter_profile', None)
        profile_data = {
            'company_name': profile.company_name if profile else '',
            'account_type': profile.account_type if profile else None,
            'documents_verified': bool(profile.documents_verified) if profile else False,
        }
    else:
        # Fleet owner / individual driver / assigned fleet driver → show shipper.
        other = conversation.shipper
        role = 'SHIPPER'
        profile = getattr(other, 'shipper_profile', None)
        profile_data = {
            'company_name': profile.company_name if profile else '',
            'account_type': profile.account_type if profile else None,
            'kyc_verified': bool(profile.kyc_verified) if profile else False,
            'credit_approved': bool(profile.credit_approved) if profile else False,
        }
    phone = ''
    try:
        phone = other.role.phone or ''
    except Exception:
        phone = ''
    return {
        'id': other.id,
        'email': other.email,
        'first_name': other.first_name,
        'last_name': other.last_name,
        'phone': phone,
        'role': role,
        'profile': profile_data,
    }


def _shipment_label(shipment):
    return shipment.unique_id or shipment.id


def _notify_transporter_shipper_bid_action(bid, event_type, title, body, extra=None):
    """Push + in-app notification to the transporter/individual driver on this bid."""
    shipment = bid.shipment
    payload = {
        'shipment_id': str(bid.shipment_id),
        'bid_id': str(bid.id),
        'transporter_id': str(bid.transporter_id),
        'unique_id': str(shipment.unique_id or ''),
        'currency': currency_for_shipment(shipment),
        'amount': str(bid.amount),
    }
    if extra:
        payload.update(extra)
    notify_users(
        [bid.transporter_id],
        event_type,
        payload,
        title=title,
        body=body,
    )


def _trip_assigned_driver_for_transporter(transporter):
    """Individual / fleet-linked drivers execute the trip themselves."""
    profile = getattr(transporter, 'transporter_profile', None)
    if not profile:
        return None
    if profile.account_type == TransporterProfile.AccountType.FLEET_OWNER:
        return None
    if profile.account_type in (
        TransporterProfile.AccountType.DRIVER,
        TransporterProfile.AccountType.TRANSPORTER_DRIVER,
    ):
        return transporter
    return None


def _notify_shipper_transporter_assigned_trip(trip, bid):
    shipment = trip.shipment
    transporter = bid.transporter
    label = transporter.get_full_name() or transporter.email
    notify_users(
        [shipment.shipper_id],
        'bid_accepted',
        {
            'trip_id': str(trip.id),
            'shipment_id': str(shipment.id),
            'bid_id': str(bid.id),
            'transporter_id': str(transporter.id),
            'agreed_price': str(trip.agreed_price),
        },
        title='Transporter assigned to load',
        body=f'{label} accepted your load #{shipment.unique_id or shipment.id}.',
    )


def _transporter_bid_response_with_trip(bid, trip):
    data = BidDetailSerializer(bid).data
    data['trip_id'] = trip.id
    data['unique_id'] = trip.shipment.unique_id
    data['agreed_price'] = str(trip.agreed_price)
    return data


def _create_trip_from_transporter_bid_accept(bid):
    """Transporter/individual driver accepted terms — assign trip and notify shipper."""
    trip = _accept_bid_and_create_trip(bid)
    _notify_shipper_transporter_assigned_trip(trip, bid)
    return trip


def _accept_bid_and_create_trip(bid):
    if bid.status == Bid.Status.COUNTERED:
        raise ValueError('Transporter must respond to the counter-offer before acceptance.')
    if bid.status not in (Bid.Status.PENDING, Bid.Status.AGREED):
        raise ValueError('Bid cannot be accepted.')
    shipment = bid.shipment
    if shipment.status != Shipment.Status.PUBLISHED:
        raise ValueError('Shipment is not in published state.')
    eligible_vehicles = Vehicle.objects.filter(
        Q(owner=bid.transporter) | Q(assigned_driver=bid.transporter),
        is_verified=True,
        is_active=True,
    )
    if not eligible_vehicles.exists():
        raise ValueError('Transporter has no eligible verified active vehicle for this load.')

    agreed_price = settlement_amount_for_bid(bid)
    assigned_driver = _trip_assigned_driver_for_transporter(bid.transporter)
    bid.status = Bid.Status.ACCEPTED
    bid.save(update_fields=['status', 'updated_at'])
    shipment.status = Shipment.Status.ASSIGNED
    shipment.save(update_fields=['status', 'updated_at'])
    trip = Trip.objects.create(
        shipment=shipment,
        accepted_bid=bid,
        transporter=bid.transporter,
        assigned_driver=assigned_driver,
        agreed_price=agreed_price,
        status=Trip.Status.ASSIGNED,
    )
    record_trip_status(trip, Trip.Status.ASSIGNED)
    _get_or_create_negotiation_conversation(shipment, bid.transporter, trip=trip)
    return trip


def _validate_trip_status_gps_time(request_data):
    require_gps = django_settings.TRIP_STATUS_REQUIRE_GPS
    lat = request_data.get('lat')
    lon = request_data.get('lon')
    if require_gps:
        if lat is None or lon is None:
            return 'lat and lon are required when TRIP_STATUS_REQUIRE_GPS is enabled.'
    recorded_at = request_data.get('recorded_at')
    if recorded_at is not None:
        ra = recorded_at
        if isinstance(ra, str):
            try:
                ra = datetime.fromisoformat(ra.replace('Z', '+00:00'))
                ra = timezone.make_aware(ra) if timezone.is_naive(ra) else ra
            except ValueError:
                return 'recorded_at must be a valid ISO-8601 datetime.'
        if not timezone.is_aware(ra):
            ra = timezone.make_aware(ra)
        age = (timezone.now() - ra).total_seconds()
        if age > django_settings.TRIP_STATUS_MAX_LOCATION_AGE_SECONDS:
            return 'recorded_at is too old for this transition.'
        if age < -120:
            return 'recorded_at cannot be far in the future.'
    return None


def _login_persona_type(user):
    """
    Client-facing persona for routing (transporter sub-roles).
    Values: SHIPPER, ADMIN, TRANSPORTER_FLEET_OWNER, TRANSPORTER_FLEET_DRIVER,
    TRANSPORTER_INDIVIDUAL_DRIVER, TRANSPORTER (fallback).
    """
    try:
        role = user.role.role
    except UserRole.DoesNotExist:
        return None
    if role == UserRole.Role.SHIPPER:
        return 'SHIPPER'
    if role == UserRole.Role.ADMIN:
        return 'ADMIN'
    if role != UserRole.Role.TRANSPORTER:
        return role
    try:
        profile = user.transporter_profile
    except TransporterProfile.DoesNotExist:
        return 'TRANSPORTER'
    if profile.account_type == TransporterProfile.AccountType.FLEET_OWNER:
        return 'TRANSPORTER_FLEET_OWNER'
    if profile.account_type == TransporterProfile.AccountType.TRANSPORTER_DRIVER:
        return 'TRANSPORTER_FLEET_DRIVER'
    if profile.account_type == TransporterProfile.AccountType.DRIVER:
        if TransporterDriverLink.objects.filter(driver=user, is_active=True).exists():
            return 'TRANSPORTER_FLEET_DRIVER'
        return 'TRANSPORTER_INDIVIDUAL_DRIVER'
    return 'TRANSPORTER'


@api_view(['GET'])
@permission_classes([AllowAny])
def firebase_client_config(request):
    """Public Firebase web/mobile client config (not the Admin SDK key)."""
    from Loadboard.firebase_config import get_firebase_client_config

    config = get_firebase_client_config()
    if not config.get('projectId'):
        return api_response(503, 'Firebase client config is not configured.', error='Not configured.')
    return api_response(200, 'Firebase client config.', data={'firebase': config})


# ---- Auth: Login & Logout ----
@api_view(['POST'])
@permission_classes([AllowAny])
def auth_login(request):
    """Login with email and password. Returns token and user info."""
    email = (request.data.get('email') or '').strip().lower()
    password = request.data.get('password')
    if not email:
        return api_response(400, 'Email is required.', error='Missing email.')
    if not password:
        return api_response(400, 'Password is required.', error='Missing password.')
    user = authenticate(request, username=email, password=password)
    if not user:
        return api_response(401, 'Invalid email or password.', error='Invalid credentials.')
    if not user.is_active:
        return api_response(401, 'Account is disabled.', error='Account disabled.')
    token, _ = Token.objects.get_or_create(user=user)
    role = None
    transporter_account_type = None
    tc_id = None
    tc_u_id = None
    try:
        role = user.role.role
    except UserRole.DoesNotExist:
        pass
    if role == UserRole.Role.TRANSPORTER:
        try:
            profile = user.transporter_profile
            transporter_account_type = profile.account_type
            if profile.account_type in (
                TransporterProfile.AccountType.DRIVER,
                TransporterProfile.AccountType.TRANSPORTER_DRIVER,
            ):
                tc_id = profile.tc_id
                tc_u_id = profile.tc_u_id
        except TransporterProfile.DoesNotExist:
            pass
    return api_response(200, 'Login successful.', data={
        'token': token.key,
        'user_id': user.id,
        'email': user.email,
        'first_name': user.first_name,
        'last_name': user.last_name,
        'role': role,
        'persona_type': _login_persona_type(user),
        'account_type': transporter_account_type,
        'tc_id': tc_id,
        'tc_u_id': tc_u_id,
    })


@api_view(['POST'])
@permission_classes([IsAuthenticated])
def auth_logout(request):
    """Logout: delete the current token."""
    try:
        request.user.auth_token.delete()
    except Exception:
        pass
    return api_response(200, 'Logged out successfully.')


# ---- Auth: Password Reset / Forgot Password ----
def _send_password_reset_otp_email(user, otp_code, expires_minutes=15):
    """Send password reset OTP email using configured Django email backend."""
    subject = 'Your Truck Me Password Reset Code'
    name = f"{user.first_name} {user.last_name}".strip() or user.email
    message = (
        f"Hello {name},\n\n"
        f"We received a request to reset your Truck Me password.\n\n"
        f"Your verification code is: {otp_code}\n\n"
        f"This code will expire in {expires_minutes} minutes. Please do not share this code with anyone.\n\n"
        f"If you did not request a password reset, please ignore this email.\n\n"
        f"Best regards,\n"
        f"The Truck Me Team"
    )
    from_email = getattr(django_settings, 'DEFAULT_FROM_EMAIL', 'no-reply@truckme.com')
    try:
        from django.core.mail import send_mail
        send_mail(subject, message, from_email, [user.email], fail_silently=False)
        return True
    except Exception as exc:
        logger.warning('Failed to send password reset email to %s: %s', user.email, exc)
        return False


@api_view(['POST'])
@permission_classes([AllowAny])
def auth_forgot_password(request):
    """
    Request password reset OTP.
    Covers shippers, fleet owners, individual drivers, and transporter drivers.
    Body: {"email": "user@example.com"}
    """
    email = (request.data.get('email') or '').strip().lower()
    if not email:
        return api_response(400, 'Email is required.', error='Missing email.')

    user = User.objects.filter(Q(email__iexact=email) | Q(username__iexact=email)).first()
    if not user:
        return api_response(404, 'No account found with this email address.', error='User not found.')

    if not user.is_active:
        return api_response(400, 'This account is disabled.', error='Account disabled.')

    otp_code = f"{secrets.randbelow(900000) + 100000}"
    reset_token = secrets.token_urlsafe(32)

    # Invalidate previous un-used OTPs for this user
    PasswordResetOTP.objects.filter(user=user, is_used=False).update(is_used=True)

    expires_at = timezone.now() + timezone.timedelta(minutes=15)
    PasswordResetOTP.objects.create(
        user=user,
        email=user.email,
        otp=otp_code,
        token=reset_token,
        expires_at=expires_at,
    )

    _send_password_reset_otp_email(user, otp_code, expires_minutes=15)

    return api_response(200, 'Password reset code has been sent to your email.', data={
        'email': user.email,
    })


@api_view(['POST'])
@permission_classes([AllowAny])
def auth_verify_reset_otp(request):
    """
    Verify 6-digit password reset OTP.
    Body: {"email": "user@example.com", "otp": "123456"}
    """
    email = (request.data.get('email') or '').strip().lower()
    otp_code = (request.data.get('otp') or request.data.get('code') or '').strip()

    if not email:
        return api_response(400, 'Email is required.', error='Missing email.')
    if not otp_code:
        return api_response(400, 'Verification code is required.', error='Missing OTP.')

    user = User.objects.filter(Q(email__iexact=email) | Q(username__iexact=email)).first()
    if not user:
        return api_response(404, 'No account found with this email address.', error='User not found.')

    otp_record = PasswordResetOTP.objects.filter(
        user=user,
        otp=otp_code,
        is_used=False,
    ).order_by('-created_at').first()

    if not otp_record or otp_record.expires_at <= timezone.now():
        return api_response(400, 'Invalid or expired verification code.', error='Invalid OTP.')

    return api_response(200, 'Verification code verified successfully.', data={
        'email': user.email,
        'reset_token': otp_record.token,
        'verified': True,
    })


@api_view(['POST'])
@permission_classes([AllowAny])
def auth_reset_password(request):
    """
    Reset password with verified OTP or reset_token.
    Body: {"email": "user@example.com", "otp": "123456", "new_password": "NewSecurePassword123"}
       or {"email": "user@example.com", "reset_token": "...", "new_password": "NewSecurePassword123"}
    """
    email = (request.data.get('email') or '').strip().lower()
    otp_code = (request.data.get('otp') or request.data.get('code') or '').strip()
    reset_token = (request.data.get('reset_token') or '').strip()
    new_password = request.data.get('new_password') or request.data.get('password')

    if not email:
        return api_response(400, 'Email is required.', error='Missing email.')
    if not otp_code and not reset_token:
        return api_response(400, 'Verification code or reset_token is required.', error='Missing verification.')
    if not new_password:
        return api_response(400, 'New password is required.', error='Missing password.')
    if len(str(new_password)) < 8:
        return api_response(400, 'Password must be at least 8 characters long.', error='Password too short.')

    user = User.objects.filter(Q(email__iexact=email) | Q(username__iexact=email)).first()
    if not user:
        return api_response(404, 'No account found with this email address.', error='User not found.')

    query = Q(user=user, is_used=False)
    if otp_code:
        query &= Q(otp=otp_code)
    if reset_token:
        query &= Q(token=reset_token)

    otp_record = PasswordResetOTP.objects.filter(query).order_by('-created_at').first()

    if not otp_record or otp_record.expires_at <= timezone.now():
        return api_response(400, 'Invalid or expired verification code.', error='Invalid verification.')

    # Set new password
    user.set_password(new_password)
    user.save(update_fields=['password'])

    # Mark OTP as used
    otp_record.is_used = True
    otp_record.save(update_fields=['is_used'])

    # Re-issue auth token
    Token.objects.filter(user=user).delete()
    token, _ = Token.objects.get_or_create(user=user)

    role = None
    transporter_account_type = None
    try:
        role = user.role.role
    except UserRole.DoesNotExist:
        pass
    if role == UserRole.Role.TRANSPORTER:
        try:
            profile = user.transporter_profile
            transporter_account_type = profile.account_type
        except TransporterProfile.DoesNotExist:
            pass

    return api_response(200, 'Password has been reset successfully.', data={
        'token': token.key,
        'user_id': user.id,
        'email': user.email,
        'role': role,
        'persona_type': _login_persona_type(user),
        'account_type': transporter_account_type,
    })



# ---- Shipper 1: Register (public) ----
@api_view(['POST'])
@permission_classes([AllowAny])
def shipper_register(request):
    ser = ShipperRegisterSerializer(data=request.data)
    if not ser.is_valid():
        return api_response(400, 'Validation failed.', error=ser.errors)
    user = ser.save()
    token, _ = Token.objects.get_or_create(user=user)
    return api_response(201, 'Shipper registered successfully.', data={
        'token': token.key,
        'user_id': user.id,
        'email': user.email,
    })


# ---- Shipper 1: Profile ----
class ShipperProfileView(APIView):
    permission_classes = [IsAuthenticated, IsShipper]

    def get(self, request):
        try:
            profile = request.user.shipper_profile
        except ShipperProfile.DoesNotExist:
            return api_response(404, 'Shipper profile not found.', error='Not found.')
        ser = ShipperProfileSerializer(profile)
        return api_response(200, 'Profile retrieved successfully.', data=ser.data)

    def patch(self, request):
        try:
            profile = request.user.shipper_profile
        except ShipperProfile.DoesNotExist:
            return api_response(404, 'Shipper profile not found.', error='Not found.')
        ser = ShipperProfileSerializer(profile, data=request.data, partial=True)
        if not ser.is_valid():
            return api_response(400, 'Validation failed.', error=ser.errors)
        ser.save()
        return api_response(200, 'Profile updated successfully.', data=ser.data)


# ---- Shipper 1: KYC ----
class KYCDocumentListCreate(generics.ListCreateAPIView):
    permission_classes = [IsAuthenticated, IsShipper]

    def get_queryset(self):
        return KYCDocument.objects.filter(user=self.request.user).order_by('-submitted_at')

    def list(self, request, *args, **kwargs):
        qs = self.get_queryset()
        ser = KYCDocumentSerializer(qs, many=True, context={'request': request})
        return api_response(200, 'KYC documents retrieved successfully.', data=ser.data)

    def create(self, request, *args, **kwargs):
        ser = KYCDocumentUploadSerializer(
            data=request.data,
            context={'request': request, 'user': request.user},
        )
        if not ser.is_valid():
            return api_response(400, 'Validation failed.', error=ser.errors)
        # Ensure KYC upload directory exists with permissions before saving
        kyc_rel = f"kyc/{timezone.now().strftime('%Y/%m/%d')}"
        ensure_upload_dir(kyc_rel)
        doc = ser.save(user=request.user)
        try:
            profile = request.user.shipper_profile
            if not profile.kyc_submitted_at:
                profile.kyc_submitted_at = timezone.now()
                profile.save(update_fields=['kyc_submitted_at'])
        except Exception:
            pass
        return api_response(
            201,
            'KYC document uploaded successfully.',
            data=KYCDocumentSerializer(doc, context={'request': request}).data
        )


# ---- Shipper 2: Shipments ----
def _shipment_validation_error_response(serializer):
    flat_error = extract_flat_shipment_validation_error(serializer.errors)
    if flat_error:
        return api_response(400, 'Validation failed.', error=flat_error)
    return api_response(400, 'Validation failed.', error=serializer.errors)


def _shipment_save_validation_error_response(exc):
    flat_error = extract_flat_shipment_validation_error(exc.detail)
    if flat_error:
        return api_response(400, 'Validation failed.', error=flat_error)
    raise exc


def _publish_shipment_if_requested(request, shipment):
    """Publish when request includes status=PUBLISHED; side effects run after commit."""
    new_status = request.data.get('status')
    if new_status != Shipment.Status.PUBLISHED or shipment.status != Shipment.Status.DRAFT:
        return None
    ok, publish_errors = prepare_shipment_for_publish(shipment)
    if not ok:
        if isinstance(publish_errors, str):
            return api_response(400, 'Validation failed.', error=publish_errors)
        return api_response(400, 'Cannot publish shipment.', error=publish_errors)
    shipment.status = Shipment.Status.PUBLISHED
    shipment.save(update_fields=['status'])
    enqueue_shipment_publish_side_effects(shipment.pk)
    return None


@api_view(['POST'])
@permission_classes([IsAuthenticated, IsShipper])
def shipper_shipment_estimate_price(request):
    """
    Preview suggested price before creating a shipment.
    Same-country uses zone km rates; cross-border uses directional freight-route min_freight.
    Does not create a shipment row.
    """
    ser = ShipmentPriceEstimateSerializer(data=request.data)
    if not ser.is_valid():
        return api_response(400, 'Validation failed.', error=ser.errors)
    try:
        data = ser.estimate()
    except ValueError as exc:
        return api_response(400, 'Could not calculate price.', error=str(exc))
    return api_response(200, 'Suggested price calculated successfully.', data=data)


class ShipmentListCreate(generics.ListCreateAPIView):
    permission_classes = [IsAuthenticated, IsShipper]

    def get_queryset(self):
        return (
            Shipment.objects.filter(
                shipper=self.request.user,
                status=Shipment.Status.PUBLISHED,
            )
            .select_related('trip', 'trip__pod')
            .prefetch_related('trip__pod__photos')
        )

    def list(self, request, *args, **kwargs):
        qs = self.get_queryset()
        ser = ShipmentSerializer(qs, many=True, context={'request': request})
        return api_response(200, 'Shipments retrieved successfully.', data=ser.data)

    def create(self, request, *args, **kwargs):
        ser = ShipmentCreateUpdateSerializer(data=request.data, context={'request': request})
        if not ser.is_valid():
            return _shipment_validation_error_response(ser)
        try:
            shipment = ser.save(shipper=request.user)
        except serializers.ValidationError as exc:
            return _shipment_save_validation_error_response(exc)
        publish_error = _publish_shipment_if_requested(request, shipment)
        if publish_error:
            return publish_error
        shipment.refresh_from_db()
        return api_response(
            201,
            'Shipment created successfully.',
            data=ShipmentSerializer(shipment, context={'request': request}).data
        )


class ShipmentDetail(generics.RetrieveUpdateDestroyAPIView):
    permission_classes = [IsAuthenticated, IsShipper]

    def get_queryset(self):
        return (
            Shipment.objects.filter(shipper=self.request.user)
            .select_related('trip', 'trip__pod')
            .prefetch_related('trip__pod__photos')
        )

    def retrieve(self, request, *args, **kwargs):
        instance = self.get_object()
        return api_response(
            200,
            'Shipment retrieved successfully.',
            data=ShipmentSerializer(instance, context={'request': request}).data,
        )


    def update(self, request, *args, **kwargs):
        partial = kwargs.pop('partial', False)
        instance = self.get_object()
        ser = ShipmentCreateUpdateSerializer(
            instance,
            data=request.data,
            partial=partial,
            context={'request': request},
        )
        if not ser.is_valid():
            return _shipment_validation_error_response(ser)
        try:
            updated = ser.save()
        except serializers.ValidationError as exc:
            return _shipment_save_validation_error_response(exc)
        publish_error = _publish_shipment_if_requested(request, updated)
        if publish_error:
            return publish_error
        updated.refresh_from_db()
        return api_response(
            200,
            'Shipment updated successfully.',
            data=ShipmentSerializer(updated, context={'request': request}).data,
        )

    def destroy(self, request, *args, **kwargs):
        instance = self.get_object()
        if instance.status != Shipment.Status.DRAFT:
            return api_response(400, 'Can only delete draft shipments.', error='Invalid operation.')
        instance.delete()
        return api_response(200, 'Shipment deleted successfully.')


# ---- Shipper 3: Bids ----
class ShipmentBidList(generics.ListAPIView):
    permission_classes = [IsAuthenticated, IsShipper]

    def get_queryset(self):
        return Bid.objects.filter(
            shipment_id=self.kwargs['shipment_pk'],
            shipment__shipper=self.request.user,
        ).select_related('shipment', 'transporter')

    def list(self, request, *args, **kwargs):
        qs = self.get_queryset()
        ser = BidListSerializer(qs, many=True)
        return api_response(200, 'Bids retrieved successfully.', data=ser.data)


class BidDetail(generics.RetrieveAPIView):
    permission_classes = [IsAuthenticated, IsShipper]

    def get_queryset(self):
        return Bid.objects.filter(shipment__shipper=self.request.user).select_related('shipment', 'transporter')

    def retrieve(self, request, *args, **kwargs):
        instance = self.get_object()
        return api_response(200, 'Bid retrieved successfully.', data=BidDetailSerializer(instance).data)


@api_view(['POST'])
@permission_classes([IsAuthenticated, IsShipper])
def bid_accept(request, pk):
    try:
        bid = Bid.objects.select_related('shipment').get(pk=pk, shipment__shipper=request.user)
    except Bid.DoesNotExist:
        return api_response(404, 'Bid not found.', error='Not found.')
    try:
        trip = _accept_bid_and_create_trip(bid)
    except ValueError as exc:
        return api_response(400, str(exc), error='Bid acceptance failed.')
    label = _shipment_label(bid.shipment)
    _notify_transporter_shipper_bid_action(
        bid,
        'bid_accepted',
        'Bid accepted',
        f'The shipper accepted your bid on load #{label}.',
        {
            'trip_id': str(trip.id),
            'agreed_price': str(trip.agreed_price),
        },
    )
    return api_response(
        200,
        'Bid accepted and trip created.',
        data=_bid_accept_response_data(trip),
    )


@api_view(['POST'])
@permission_classes([IsAuthenticated, IsShipper])
def bid_counter(request, pk):
    try:
        bid = Bid.objects.select_related('shipment').get(pk=pk, shipment__shipper=request.user)
    except Bid.DoesNotExist:
        return api_response(404, 'Bid not found.', error='Not found.')
    if _is_bid_locked(bid.shipment):
        return api_response(400, 'Bid is locked after load acceptance.', error='Bid locked.')
    if bid.status != Bid.Status.PENDING:
        return api_response(
            400,
            'Only pending bids can be countered.',
            error='Invalid bid status.',
        )
    ser = BidCounterSerializer(data=request.data)
    if not ser.is_valid():
        return api_response(400, 'Validation failed.', error=ser.errors)
    bid.counter_amount = ser.validated_data['counter_amount']
    bid.message = ser.validated_data.get('message', '') or bid.message
    bid.status = Bid.Status.COUNTERED
    bid.save(update_fields=['counter_amount', 'message', 'status'])
    _get_or_create_negotiation_conversation(bid.shipment, bid.transporter)
    label = _shipment_label(bid.shipment)
    _notify_transporter_shipper_bid_action(
        bid,
        'bid_countered',
        'Counter-offer received',
        f'A shipper countered your bid on load #{label}.',
        {'counter_amount': str(bid.counter_amount)},
    )
    return api_response(200, 'Counter-offer sent.', data=BidDetailSerializer(bid).data)


@api_view(['POST'])
@permission_classes([IsAuthenticated, IsShipper])
def bid_reject(request, pk):
    try:
        bid = Bid.objects.select_related('shipment').get(pk=pk, shipment__shipper=request.user)
    except Bid.DoesNotExist:
        return api_response(404, 'Bid not found.', error='Not found.')
    if bid.status == Bid.Status.ACCEPTED or _is_bid_locked(bid.shipment):
        return api_response(400, 'Cannot reject an accepted bid or locked load.', error='Bid locked.')
    if bid.status == Bid.Status.REJECTED:
        return api_response(400, 'Bid is already rejected.', error='Already rejected.')

    reason = (request.data.get('reason') or request.data.get('message') or '').strip()
    if reason:
        bid.message = reason
        bid.status = Bid.Status.REJECTED
        bid.save(update_fields=['status', 'message', 'updated_at'])
    else:
        bid.status = Bid.Status.REJECTED
        bid.save(update_fields=['status', 'updated_at'])

    label = _shipment_label(bid.shipment)
    reject_extra = {'reason': reason} if reason else None
    _notify_transporter_shipper_bid_action(
        bid,
        'bid_rejected',
        'Bid rejected',
        f'Your bid on load #{label} was rejected by the shipper.',
        reject_extra,
    )
    return api_response(200, 'Bid rejected successfully.', data=BidDetailSerializer(bid).data)



@api_view(['GET'])
@permission_classes([IsAuthenticated, IsTransporter, IsTransporterVerified])
def transporter_load_bid_history(request, shipment_pk):
    try:
        shipment = Shipment.objects.get(pk=shipment_pk)
    except Shipment.DoesNotExist:
        return api_response(404, 'Shipment not found.', error='Not found.')
    bids = (
        Bid.objects.filter(shipment=shipment, transporter=request.user)
        .select_related('transporter', 'shipment')
        .order_by('-created_at')
    )
    trip = (
        Trip.objects.filter(shipment=shipment, transporter=request.user)
        .select_related(
            'shipment',
            'shipment__shipper',
            'shipment__shipper__role',
            'shipment__shipper__shipper_profile',
            'accepted_bid',
            'accepted_bid__transporter',
            'transporter',
            'transporter__transporter_profile',
            'assigned_driver',
            'assigned_driver__role',
            'assigned_driver__transporter_profile',
        )
        .prefetch_related('status_records')
        .first()
    )
    return api_response(
        200,
        'Bid history retrieved successfully.',
        data={
            'shipment': ShipmentSerializer(shipment).data,
            'bids': BidListSerializer(bids, many=True).data,
            'trip': TransporterTripDetailSerializer(trip).data if trip else None,
        },
    )


@api_view(['POST'])
@permission_classes([IsAuthenticated, IsTransporter, IsTransporterVerified])
def transporter_submit_bid(request, shipment_pk):
    try:
        shipment = Shipment.objects.get(pk=shipment_pk)
    except Shipment.DoesNotExist:
        return api_response(404, 'Shipment not found.', error='Not found.')

    if _is_bid_locked(shipment):
        return api_response(400, 'Bidding is locked after load acceptance.', error='Bid locked.')

    try:
        profile = request.user.transporter_profile
    except TransporterProfile.DoesNotExist:
        return api_response(404, 'Transporter profile not found.', error='Not found.')

    if not transporter_can_access_shipment(profile, shipment):
        return api_response(
            403,
            'This load type is not available for your transporter profile.',
            error='Load type mismatch.',
        )

    if profile.account_type == TransporterProfile.AccountType.FLEET_OWNER:
        has_own_driver = TransporterDriverLink.objects.filter(
            transporter=request.user,
            is_active=True,
            driver__is_active=True,
        ).exists()
        if not has_own_driver:
            return api_response(
                400,
                'Add at least one fleet driver before bidding on loads.',
                error='Missing fleet driver.',
            )

    fleet_vehicles = transporter_verified_active_vehicles(request.user, profile.account_type)
    if not fleet_vehicles:
        return api_response(
            400,
            'A verified active vehicle is required to bid on this load.',
            error='Missing verified active vehicle.',
        )
    if not shipment_matches_transporter_vehicle_types(shipment, fleet_vehicles):
        return api_response(
            400,
            'No verified vehicle matches this load type.',
            error='Vehicle type mismatch.',
        )

    ser = TransporterBidSubmitSerializer(data=request.data)
    if not ser.is_valid():
        return api_response(400, 'Validation failed.', error=ser.errors)

    action = ser.validated_data['action']
    amount = ser.validated_data.get('amount')
    message = (ser.validated_data.get('message') or '').strip()
    active_bid = _get_active_bid(shipment, request.user.id)

    if action == TransporterBidSubmitSerializer.ACTION_ACCEPT:
        if active_bid and active_bid.status == Bid.Status.COUNTERED:
            if active_bid.counter_amount is None:
                return api_response(
                    400,
                    'Counter-offer amount is missing on this bid.',
                    error='Invalid counter.',
                )
            active_bid.amount = active_bid.counter_amount
            active_bid.counter_amount = None
            active_bid.status = Bid.Status.AGREED
            active_bid.message = message or 'Accepted shipper counter-offer.'
            active_bid.save(update_fields=['amount', 'counter_amount', 'status', 'message', 'updated_at'])
            _get_or_create_negotiation_conversation(shipment, request.user)
            try:
                trip = _create_trip_from_transporter_bid_accept(active_bid)
            except ValueError as exc:
                return api_response(400, str(exc), error='Bid acceptance failed.')
            return api_response(
                200,
                'Counter-offer accepted and trip assigned.',
                data=_transporter_bid_response_with_trip(active_bid, trip),
            )

        if active_bid and active_bid.status == Bid.Status.AGREED:
            return api_response(
                400,
                'Bid is already agreed and trip is assigned.',
                error='Trip already assigned.',
            )

        if shipment.suggested_price is None:
            return api_response(
                400,
                'Shipper proposed rate is unavailable for this load.',
                error='Missing suggested_price.',
            )
        bid_amount = shipment.suggested_price
        bid_message = message or 'Accepted shipper proposed rate.'
        if active_bid:
            active_bid.amount = bid_amount
            active_bid.counter_amount = None
            active_bid.status = Bid.Status.PENDING
            active_bid.message = bid_message
            active_bid.save(update_fields=['amount', 'counter_amount', 'status', 'message', 'updated_at'])
            bid = active_bid
        else:
            bid = Bid.objects.create(
                shipment=shipment,
                transporter=request.user,
                amount=bid_amount,
                status=Bid.Status.PENDING,
                message=bid_message,
            )
        _get_or_create_negotiation_conversation(shipment, request.user)
        try:
            trip = _create_trip_from_transporter_bid_accept(bid)
        except ValueError as exc:
            return api_response(400, str(exc), error='Bid acceptance failed.')
        return api_response(
            200,
            'Proposed rate accepted and trip assigned.',
            data=_transporter_bid_response_with_trip(bid, trip),
        )
    else:
        if active_bid and active_bid.status == Bid.Status.AGREED:
            return api_response(
                400,
                'Bid is already agreed and trip is assigned.',
                error='Trip already assigned.',
            )
        bid_amount = amount
        bid_message = message or 'Submitted counter-offer.'
        if active_bid:
            active_bid.amount = bid_amount
            active_bid.counter_amount = None
            active_bid.status = Bid.Status.PENDING
            active_bid.message = bid_message
            active_bid.save(update_fields=['amount', 'counter_amount', 'status', 'message', 'updated_at'])
            bid = active_bid
            status_code = 200
            response_message = 'Bid updated successfully.'
        else:
            bid = Bid.objects.create(
                shipment=shipment,
                transporter=request.user,
                amount=bid_amount,
                status=Bid.Status.PENDING,
                message=bid_message,
            )
            status_code = 201
            response_message = 'Bid submitted successfully.'

    _get_or_create_negotiation_conversation(shipment, request.user)
    notify_users(
        [shipment.shipper_id],
        'new_bid',
        {'shipment_id': str(shipment.id), 'bid_id': str(bid.id)},
        title='New bid',
        body='A transporter submitted a bid on your load.',
    )
    return api_response(
        status_code,
        response_message,
        data=BidDetailSerializer(bid).data,
    )


# ---- Shipper 4: Trips ----
class TripList(generics.ListAPIView):
    permission_classes = [IsAuthenticated, IsShipper]

    def get_queryset(self):
        return (
            Trip.objects.filter(shipment__shipper=self.request.user)
            .select_related(
                'shipment',
                'transporter',
                'transporter__role',
                'transporter__transporter_profile',
                'assigned_driver',
                'assigned_driver__role',
                'assigned_driver__transporter_profile',
                'review',
                'pod',
            )
            .prefetch_related('status_records', 'pod__photos')
        )

    def list(self, request, *args, **kwargs):
        # Warm favorites once; batch-resolve vehicles to avoid N+1 queries.
        get_shipper_favorite_transporter_ids(request)
        trips = list(self.get_queryset())
        ser = ShipperTripListSerializer(
            trips,
            many=True,
            context={
                'request': request,
                'assigned_vehicles_by_trip_id': resolve_trips_assigned_vehicles(trips),
            },
        )
        return api_response(200, 'Trips retrieved successfully.', data=ser.data)


class TripDetail(generics.RetrieveAPIView):
    permission_classes = [IsAuthenticated, IsShipper]

    def get_queryset(self):
        return (
            Trip.objects.filter(shipment__shipper=self.request.user)
            .select_related(
                'shipment',
                'transporter',
                'transporter__role',
                'transporter__transporter_profile',
                'assigned_driver',
                'assigned_driver__role',
                'assigned_driver__transporter_profile',
                'review',
                'accepted_bid',
                'accepted_bid__transporter',
                'pod',
            )
            .prefetch_related('status_records', 'pod__photos')
        )

    def retrieve(self, request, *args, **kwargs):
        get_shipper_favorite_transporter_ids(request)
        instance = self.get_object()
        ser = ShipperTripDetailSerializer(
            instance,
            context={
                'request': request,
                'assigned_vehicles_by_trip_id': resolve_trips_assigned_vehicles([instance]),
            },
        )
        return api_response(200, 'Trip retrieved successfully.', data=ser.data)



# ---- Transporter: Trips buckets ----
@api_view(['GET'])
@permission_classes([IsAuthenticated, IsFleetOwnerTransporter, IsTransporterVerified])
def transporter_trips_my(request):
    qs = Trip.objects.filter(transporter=request.user).select_related(
        'shipment', 'accepted_bid', 'transporter', 'assigned_driver',
    ).prefetch_related('status_records')
    return api_response(200, 'Trips retrieved successfully.', data=TripListSerializer(qs, many=True).data)


@api_view(['GET'])
@permission_classes([IsAuthenticated, IsFleetOwnerTransporter, IsTransporterVerified])
def transporter_trips_assigned(request):
    qs = Trip.objects.filter(transporter=request.user, assigned_driver__isnull=False).select_related(
        'shipment', 'accepted_bid', 'transporter', 'assigned_driver',
    ).prefetch_related('status_records')
    return api_response(200, 'Trips retrieved successfully.', data=TripListSerializer(qs, many=True).data)


@api_view(['GET'])
@permission_classes([IsAuthenticated, IsFleetOwnerTransporter, IsTransporterVerified])
def transporter_trips_active(request):
    active = {
        Trip.Status.EN_ROUTE,
        Trip.Status.ARRIVED_PICKUP,
        Trip.Status.LOADED,
        Trip.Status.IN_TRANSIT,
        Trip.Status.ARRIVED_DELIVERY,
        Trip.Status.DELIVERED,
    }
    qs = Trip.objects.filter(transporter=request.user, status__in=active).select_related(
        'shipment', 'accepted_bid', 'transporter', 'assigned_driver',
    ).prefetch_related('status_records')
    return api_response(200, 'Trips retrieved successfully.', data=TripListSerializer(qs, many=True).data)


def _trip_detail_queryset():
    return Trip.objects.select_related(
        'shipment',
        'shipment__shipper',
        'shipment__shipper__role',
        'shipment__shipper__shipper_profile',
        'accepted_bid',
        'accepted_bid__transporter',
        'accepted_bid__shipment',
        'transporter',
        'transporter__transporter_profile',
        'assigned_driver',
        'assigned_driver__role',
        'assigned_driver__transporter_profile',
    ).prefetch_related('status_records')


def _resolve_trip_driver_traccar(trip):
    """Assigned fleet driver if set, otherwise the trip transporter (individual driver)."""
    driver = trip.assigned_driver or trip.transporter
    if not driver:
        return None, '', None
    try:
        profile = driver.transporter_profile
    except TransporterProfile.DoesNotExist:
        return driver, '', None
    tc_id = str(profile.tc_id or '').strip()
    return driver, tc_id, profile.tc_u_id or None


def _trip_driver_summary(driver):
    if not driver:
        return None
    role = getattr(driver, 'role', None)
    profile = getattr(driver, 'transporter_profile', None)
    return {
        'id': driver.id,
        'email': driver.email,
        'first_name': driver.first_name,
        'last_name': driver.last_name,
        'phone': getattr(role, 'phone', '') or '',
        'account_type': getattr(profile, 'account_type', None),
    }


def _user_can_access_trip_driver_location(user, trip):
    if getattr(trip.shipment, 'shipper_id', None) == user.id:
        return True
    return user.id in (trip.transporter_id, trip.assigned_driver_id)


def _completed_trips_with_payment_response(request, *, message):
    """Completed trips for trip.transporter with payment settlement summary (fleet owner or individual driver)."""
    qs = (
        _trip_detail_queryset()
        .filter(transporter=request.user, status=Trip.Status.COMPLETED)
        .prefetch_related(
            Prefetch(
                'payments',
                queryset=Payment.objects.exclude(status=Payment.Status.FAILED).order_by('-id'),
            )
        )
        .order_by('-updated_at')
    )
    data = TransporterCompletedTripSerializer(qs, many=True, context={'request': request}).data
    return api_response(200, message, data={'count': len(data), 'trips': data})


@api_view(['GET'])
@permission_classes([IsAuthenticated, IsFleetOwnerTransporter, IsTransporterVerified])
def transporter_trips_completed(request):
    return _completed_trips_with_payment_response(
        request,
        message='Completed trips retrieved successfully.',
    )


def _fleet_driver_trip_queryset(user):
    return _trip_detail_queryset().filter(assigned_driver=user)


def _individual_driver_in_progress_statuses():
    """Started through delivered — not assigned/completed/closed."""
    return {
        Trip.Status.EN_ROUTE,
        Trip.Status.ARRIVED_PICKUP,
        Trip.Status.LOADED,
        Trip.Status.IN_TRANSIT,
        Trip.Status.ARRIVED_DELIVERY,
        Trip.Status.DELIVERED,
    }


def _individual_driver_trip_list_response(request, *, statuses, message):
    if isinstance(statuses, str):
        qs = _trip_detail_queryset().filter(transporter=request.user, status=statuses)
    else:
        qs = _trip_detail_queryset().filter(transporter=request.user, status__in=statuses)
    qs = qs.order_by('-updated_at')
    data = TransporterTripDetailSerializer(qs, many=True).data
    return api_response(200, message, data={'count': len(data), 'trips': data})


@api_view(['GET'])
@permission_classes([IsAuthenticated, IsIndividualDriverTransporter])
def individual_driver_trips_assigned(request):
    """Individual driver: won trips not started yet (ASSIGNED only)."""
    return _individual_driver_trip_list_response(
        request,
        statuses=Trip.Status.ASSIGNED,
        message='Assigned trips retrieved successfully.',
    )


@api_view(['GET'])
@permission_classes([IsAuthenticated, IsIndividualDriverTransporter])
def individual_driver_trips_active(request):
    """Individual driver: trips they won (Trip.transporter) that are still in progress."""
    return _individual_driver_trip_list_response(
        request,
        statuses=_individual_driver_in_progress_statuses(),
        message='Active trips retrieved successfully.',
    )


@api_view(['GET'])
@permission_classes([IsAuthenticated, IsIndividualDriverTransporter])
def individual_driver_trips_completed(request):
    """Individual driver: won trips that are completed (includes payment settlement summary)."""
    return _completed_trips_with_payment_response(
        request,
        message='Completed trips retrieved successfully.',
    )


@api_view(['GET'])
@permission_classes([IsAuthenticated, IsFleetDriverTransporter])
def driver_trips_assigned(request):
    qs = _fleet_driver_trip_queryset(request.user).filter(status=Trip.Status.ASSIGNED)
    return api_response(
        200,
        'Trips retrieved successfully.',
        data=TransporterTripDetailSerializer(qs, many=True).data,
    )


@api_view(['GET'])
@permission_classes([IsAuthenticated, IsFleetDriverTransporter])
def driver_trips_active(request):
    active = {
        Trip.Status.EN_ROUTE,
        Trip.Status.ARRIVED_PICKUP,
        Trip.Status.LOADED,
        Trip.Status.IN_TRANSIT,
        Trip.Status.ARRIVED_DELIVERY,
        Trip.Status.DELIVERED,
    }
    qs = _fleet_driver_trip_queryset(request.user).filter(status__in=active)
    return api_response(
        200,
        'Trips retrieved successfully.',
        data=TransporterTripDetailSerializer(qs, many=True).data,
    )


@api_view(['GET'])
@permission_classes([IsAuthenticated, IsFleetDriverTransporter])
def driver_trips_completed(request):
    qs = _fleet_driver_trip_queryset(request.user).filter(status=Trip.Status.COMPLETED)
    return api_response(
        200,
        'Trips retrieved successfully.',
        data=TransporterTripDetailSerializer(qs, many=True).data,
    )


# ---- Shipper 5: Tracking ----
class TripLocationList(generics.ListAPIView):
    permission_classes = [IsAuthenticated, IsShipper]

    def get_queryset(self):
        return TripLocation.objects.filter(
            trip_id=self.kwargs['trip_pk'],
            trip__shipment__shipper=self.request.user
        ).order_by('-recorded_at')[:100]

    def list(self, request, *args, **kwargs):
        qs = self.get_queryset()
        ser = TripLocationSerializer(qs, many=True)
        return api_response(200, 'Locations retrieved successfully.', data=ser.data)


# ---- Unified chats (bid negotiation + trip lifecycle) ----
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def chat_list(request):
    qs = Conversation.objects.filter(
        Q(shipper=request.user)
        | Q(transporter=request.user)
        | Q(trip__assigned_driver=request.user)
    ).select_related(
        'shipment',
        'trip',
        'trip__assigned_driver',
        'shipper',
        'shipper__role',
        'shipper__shipper_profile',
        'transporter',
        'transporter__role',
        'transporter__transporter_profile',
    ).distinct().order_by('-created_at')
    data = [
        {
            'id': c.id,
            'shipment_id': c.shipment_id,
            'trip_id': c.trip_id,
            'shipper_id': c.shipper_id,
            'transporter_id': c.transporter_id,
            'assigned_driver_id': c.trip.assigned_driver_id if c.trip_id else None,
            'counterparty': _chat_counterparty_payload(c, request.user),
            'last_message_at': c.messages.order_by('-created_at').values_list('created_at', flat=True).first(),
            'created_at': c.created_at,
        }
        for c in qs
    ]
    return api_response(200, 'Chats retrieved successfully.', data=data)


@api_view(['POST'])
@permission_classes([IsAuthenticated])
def chat_open_or_create(request):
    shipment_id = request.data.get('shipment_id')
    transporter_id = request.data.get('transporter_id')
    if not shipment_id or not transporter_id:
        return api_response(400, 'shipment_id and transporter_id are required.', error='Missing fields.')
    try:
        shipment = Shipment.objects.get(pk=shipment_id)
    except Shipment.DoesNotExist:
        return api_response(404, 'Shipment not found.', error='Not found.')
    try:
        transporter_id_int = int(transporter_id)
    except (TypeError, ValueError):
        return api_response(400, 'transporter_id must be an integer.', error='Invalid transporter_id.')

    is_assigned_driver = Trip.objects.filter(
        shipment=shipment,
        transporter_id=transporter_id_int,
        assigned_driver=request.user,
    ).exists()
    if request.user.id not in (shipment.shipper_id, transporter_id_int) and not is_assigned_driver:
        return api_response(403, 'You cannot access this chat.', error='Forbidden.')
    if not (
        Bid.objects.filter(shipment=shipment, transporter_id=transporter_id_int).exists()
        or Trip.objects.filter(shipment=shipment, transporter_id=transporter_id_int).exists()
    ):
        return api_response(400, 'No bid or trip found for this shipment/transporter pair.', error='Invalid chat context.')
    try:
        transporter = User.objects.get(pk=transporter_id_int)
    except User.DoesNotExist:
        return api_response(404, 'Transporter not found.', error='Not found.')
    trip = (
        Trip.objects.filter(shipment=shipment, transporter_id=transporter_id_int)
        .order_by('-id')
        .first()
    )
    conv = _get_or_create_negotiation_conversation(shipment, transporter, trip=trip)
    return api_response(
        200,
        'Chat ready.',
        data={
            'conversation_id': conv.id,
            'shipment_id': conv.shipment_id,
            'trip_id': conv.trip_id,
            'shipper_id': conv.shipper_id,
            'transporter_id': conv.transporter_id,
            'assigned_driver_id': trip.assigned_driver_id if trip else None,
            'counterparty': _chat_counterparty_payload(conv, request.user),
        },
    )


@api_view(['GET', 'POST'])
@permission_classes([IsAuthenticated])
def chat_messages(request, conversation_id):
    try:
        conv = Conversation.objects.select_related('shipment', 'trip').get(pk=conversation_id)
    except Conversation.DoesNotExist:
        return api_response(404, 'Conversation not found.', error='Not found.')
    if not _user_can_access_conversation(request.user, conv):
        return api_response(403, 'You cannot access this conversation.', error='Forbidden.')
    if request.method == 'GET':
        messages = (
            conv.messages.select_related(
                'sender',
                'sender__shipper_profile',
                'sender__transporter_profile',
                'conversation',
            )
        )
        ser = MessageSerializer(messages, many=True, context={'request': request})
        return api_response(200, 'Messages retrieved successfully.', data=ser.data)

    ser = MessageCreateSerializer(data=request.data)
    if not ser.is_valid():
        return api_response(400, 'Validation failed.', error=ser.errors)
    vtype = ser.validated_data.get('message_type') or Message.MessageType.TEXT
    if vtype == Message.MessageType.VOICE:
        date_path = timezone.now().strftime('%Y/%m/%d')
        ensure_upload_dir(f'messages/voice/{date_path}')
    msg = ser.save(conversation=conv, sender=request.user)
    msg = (
        Message.objects.select_related(
            'sender',
            'sender__shipper_profile',
            'sender__transporter_profile',
            'conversation',
        )
        .get(pk=msg.pk)
    )
    recipient_ids = _conversation_message_recipient_ids(conv, request.user.id)
    if recipient_ids:
        tag_payload = {
            'conversation_id': str(conv.id),
            'shipment_id': str(conv.shipment_id) if conv.shipment_id else '',
            'trip_id': str(conv.trip_id) if conv.trip_id else '',
            'message_id': str(msg.id),
            'sender_id': str(request.user.id),
            'chat_message_type': str(msg.message_type),
            'event_type': 'new_message',
        }
        if msg.message_type == Message.MessageType.VOICE:
            push_body = 'You received a voice message.'
        else:
            text = (msg.text or '').strip()
            push_body = (text[:120] if text else 'You received a new message.')
        notify_users(
            recipient_ids,
            'new_message',
            {
                **tag_payload,
                # Mobile clients route on these FCM data keys (PHP-compatible shape).
                'type': 'New Message',
                'tag': json.dumps(tag_payload),
            },
            title='New Message',
            body=push_body,
        )
    return api_response(
        201,
        'Message sent successfully.',
        data=MessageSerializer(msg, context={'request': request}).data,
    )


@api_view(['POST'])
@permission_classes([IsAuthenticated, IsShipper])
def chat_accept_bid(request, conversation_id):
    try:
        conv = Conversation.objects.select_related('shipment').get(pk=conversation_id, shipper=request.user)
    except Conversation.DoesNotExist:
        return api_response(404, 'Conversation not found.', error='Not found.')
    bid = _get_active_bid(conv.shipment, conv.transporter_id)
    if not bid:
        return api_response(400, 'No active bid found for this chat.', error='No active bid.')
    try:
        bid = Bid.objects.select_related('shipment').get(pk=bid.pk)
    except Bid.DoesNotExist:
        return api_response(404, 'Bid not found.', error='Not found.')
    try:
        trip = _accept_bid_and_create_trip(bid)
    except ValueError as exc:
        return api_response(400, str(exc), error='Bid acceptance failed.')
    label = _shipment_label(conv.shipment)
    _notify_transporter_shipper_bid_action(
        bid,
        'bid_accepted',
        'Bid accepted',
        f'The shipper accepted your bid on load #{label}.',
        {
            'trip_id': str(trip.id),
            'agreed_price': str(trip.agreed_price),
            'conversation_id': str(conv.id),
        },
    )
    data = _bid_accept_response_data(trip)
    data['conversation_id'] = conv.id
    return api_response(
        200,
        'Bid accepted and trip created.',
        data=data,
    )


@api_view(['POST'])
@permission_classes([IsAuthenticated, IsShipper])
def chat_reject_bid(request, conversation_id):
    try:
        conv = Conversation.objects.select_related('shipment').get(pk=conversation_id, shipper=request.user)
    except Conversation.DoesNotExist:
        return api_response(404, 'Conversation not found.', error='Not found.')
    bid = _get_active_bid(conv.shipment, conv.transporter_id)
    if not bid:
        return api_response(400, 'No active bid found for this chat.', error='No active bid.')
    if _is_bid_locked(conv.shipment):
        return api_response(400, 'Cannot reject a bid on a locked load.', error='Bid locked.')

    reason = (request.data.get('reason') or request.data.get('message') or '').strip()
    if reason:
        bid.message = reason
        bid.status = Bid.Status.REJECTED
        bid.save(update_fields=['status', 'message', 'updated_at'])
    else:
        bid.status = Bid.Status.REJECTED
        bid.save(update_fields=['status', 'updated_at'])

    label = _shipment_label(conv.shipment)
    reject_extra = {'reason': reason, 'conversation_id': str(conv.id)} if reason else {'conversation_id': str(conv.id)}
    _notify_transporter_shipper_bid_action(
        bid,
        'bid_rejected',
        'Bid rejected',
        f'Your bid on load #{label} was rejected by the shipper.',
        reject_extra,
    )
    return api_response(200, 'Bid rejected successfully.', data=BidDetailSerializer(bid).data)



# ---- Shipper 6: POD ----
@api_view(['GET'])
@permission_classes([IsAuthenticated, IsShipper])
def trip_pod(request, trip_pk):
    if not Trip.objects.filter(pk=trip_pk, shipment__shipper=request.user).exists():
        return api_response(404, 'Trip not found.', error='Not found.')
    try:
        pod = ProofOfDelivery.objects.get(trip_id=trip_pk)
    except ProofOfDelivery.DoesNotExist:
        return api_response(404, 'POD not yet submitted.', error='Not found.')
    return api_response(200, 'POD retrieved successfully.', data=PODSerializer(pod, context={'request': request}).data)


@api_view(['GET'])
@permission_classes([IsAuthenticated, IsShipper])
def shipper_shipment_nearby_drivers(request, shipment_pk):
    try:
        shipment = Shipment.objects.get(pk=shipment_pk, shipper=request.user)
    except Shipment.DoesNotExist:
        return api_response(404, 'Shipment not found.', error='Not found.')

    ref_lat = shipment.pickup_lat
    ref_lon = shipment.pickup_lon
    if ref_lat is None or ref_lon is None:
        return api_response(
            400,
            'Shipment pickup coordinates are required to fetch nearby drivers.',
            error='Missing coordinates.',
        )

    try:
        positions_map = get_latest_positions_map()
    except ValueError as exc:
        return api_response(400, 'Unable to fetch drivers location from Traccar.', error=str(exc))

    profiles = (
        TransporterProfile.objects.filter(documents_verified=True)
        .exclude(tc_id__isnull=True)
        .exclude(tc_id='')
        .select_related('user', 'user__role')
        .prefetch_related('user__fleet_vehicles')
    )

    ref_lat_f = float(ref_lat)
    ref_lon_f = float(ref_lon)
    radius_km = 5.0
    nearby = _drivers_within_radius(
        ref_lat_f,
        ref_lon_f,
        radius_km,
        profiles,
        positions_map,
        owner_only=True,
    )

    return api_response(
        200,
        'Nearby verified active transporters retrieved successfully.',
        data={
            'shipment_id': shipment.id,
            'radius_km': radius_km,
            'reference': {'lat': ref_lat_f, 'lon': ref_lon_f},
            'count': len(nearby),
            'drivers': nearby,
        },
    )


@api_view(['POST'])
@permission_classes([IsAuthenticated, IsShipper])
def shipper_driver_locations(request):
    ser = ShipperDriverLocationsSerializer(data=request.data)
    if not ser.is_valid():
        return api_response(400, 'Validation failed.', error=ser.errors)

    country_code = ser.validated_data['country_code']
    shipment_id = ser.validated_data['shipment_id']
    ref_lat_f = float(ser.validated_data['lat'])
    ref_lon_f = float(ser.validated_data['lng'])

    try:
        shipment = Shipment.objects.get(pk=shipment_id, shipper=request.user)
    except Shipment.DoesNotExist:
        return api_response(404, 'Shipment not found.', error='Not found.')

    vehicle_type_required = shipment.vehicle_type_required
    radius_km, radius_source = zone_search_radius_km(country_code)

    try:
        positions_map = get_latest_positions_map()
    except ValueError as exc:
        return api_response(400, 'Unable to fetch drivers location from Traccar.', error=str(exc))

    profiles = _shipper_driver_profiles_queryset()
    nearby = _drivers_within_radius(
        ref_lat_f,
        ref_lon_f,
        radius_km,
        profiles,
        positions_map,
        owner_only=False,
        vehicle_type_required=vehicle_type_required,
    )

    return api_response(
        200,
        'Driver locations retrieved successfully.',
        data={
            'shipment_id': shipment.id,
            'vehicle_type_required': vehicle_type_required,
            'country_code': country_code,
            'radius_km': radius_km,
            'radius_source': radius_source,
            'reference': {'lat': ref_lat_f, 'lon': ref_lon_f},
            'count': len(nearby),
            'drivers': nearby,
        },
    )


@api_view(['GET'])
@permission_classes([IsAuthenticated, IsShipper])
def shipper_transporter_current_location(request, transporter_id):
    try:
        transporter = User.objects.get(pk=transporter_id, role__role=UserRole.Role.TRANSPORTER)
    except User.DoesNotExist:
        return api_response(404, 'Transporter not found.', error='Not found.')

    try:
        profile = transporter.transporter_profile
    except TransporterProfile.DoesNotExist:
        return api_response(404, 'Transporter profile not found.', error='Not found.')

    tc_id = str(profile.tc_id or '').strip()
    if not tc_id:
        return api_response(
            400,
            'Driver tracking device is not configured.',
            error='Missing tc_id on transporter profile.',
        )

    try:
        position = get_latest_device_position(tc_id)
    except ValueError as exc:
        return api_response(400, 'Unable to fetch driver location from Traccar.', error=str(exc))

    lat = position.get('latitude')
    lon = position.get('longitude')
    if lat is None or lon is None:
        return api_response(400, 'Driver location is invalid.', error='Missing latitude/longitude.')

    return api_response(
        200,
        'Transporter current location retrieved successfully.',
        data={
            'transporter_id': transporter.id,
            'tc_id': tc_id,
            'position': {'lat': float(lat), 'lon': float(lon)},
            'raw': position.get('raw'),
        },
    )


@api_view(['GET'])
@permission_classes([IsAuthenticated])
def trip_driver_current_location(request, trip_pk):
    """
    Latest Traccar position for the trip's assigned driver (or transporter when unassigned).
    Accessible to the shipment shipper, fleet owner (transporter), and assigned driver.
    """
    try:
        trip = _trip_detail_queryset().get(pk=trip_pk)
    except Trip.DoesNotExist:
        return api_response(404, 'Trip not found.', error='Not found.')

    if not _user_can_access_trip_driver_location(request.user, trip):
        return api_response(404, 'Trip not found.', error='Not found.')

    driver, tc_id, tc_u_id = _resolve_trip_driver_traccar(trip)
    if not driver:
        return api_response(400, 'Trip has no driver assigned.', error='Missing driver.')
    if not tc_id:
        return api_response(
            400,
            'Driver tracking device is not configured.',
            error='Missing tc_id on driver profile.',
        )

    try:
        position = get_latest_device_position(tc_id)
    except ValueError as exc:
        return api_response(400, 'Unable to fetch driver location from Traccar.', error=str(exc))

    lat = position.get('latitude')
    lon = position.get('longitude')
    if lat is None or lon is None:
        return api_response(400, 'Driver location is invalid.', error='Missing latitude/longitude.')

    raw = position.get('raw') or {}
    recorded_at = raw.get('fixTime') or raw.get('deviceTime')

    return api_response(
        200,
        'Driver current location retrieved successfully.',
        data={
            'trip_id': trip.id,
            'unique_id': trip.shipment.unique_id if getattr(trip, 'shipment', None) else None,
            'driver': _trip_driver_summary(driver),
            'tc_id': tc_id,
            'tc_u_id': tc_u_id,
            'position': {'lat': float(lat), 'lon': float(lon)},
            'recorded_at': recorded_at,
            'raw': raw,
        },
    )


# ---- Transporter: Registration (public) ----
@api_view(['POST'])
@permission_classes([AllowAny])
def transporter_register(request):
    ser = TransporterRegisterSerializer(data=request.data, context={'request': request})
    if not ser.is_valid():
        return api_response(400, 'Validation failed.', error=ser.errors)
    user = ser.save()
    token, _ = Token.objects.get_or_create(user=user)
    return api_response(201, 'Transporter registered successfully.', data={
        'token': token.key,
        'user_id': user.id,
        'email': user.email,
    })


@api_view(['GET'])
@permission_classes([AllowAny])
def transporter_gcc_countries(request):
    """Gulf countries for the fleet-owner / driver GCC ID country dropdown."""
    from core.countries import GCC_COUNTRY_CHOICES

    return api_response(
        200,
        'GCC countries retrieved successfully.',
        data=[
            {'code': code, 'name': name}
            for code, name in GCC_COUNTRY_CHOICES
        ],
    )


# ---- Transporter: Profile ----
class TransporterProfileView(APIView):
    permission_classes = [IsAuthenticated, IsTransporter]

    def _get_profile(self, user):
        return (
            TransporterProfile.objects.select_related('user', 'user__role')
            .get(user=user)
        )

    def get(self, request):
        try:
            profile = self._get_profile(request.user)
        except TransporterProfile.DoesNotExist:
            return api_response(404, 'Transporter profile not found.', error='Not found.')
        ser = TransporterProfileSerializer(profile, context={'request': request})
        return api_response(200, 'Profile retrieved successfully.', data=ser.data)

    def patch(self, request):
        try:
            profile = self._get_profile(request.user)
        except TransporterProfile.DoesNotExist:
            return api_response(404, 'Transporter profile not found.', error='Not found.')
        ser = TransporterProfileSerializer(
            profile, data=request.data, partial=True, context={'request': request},
        )
        if not ser.is_valid():
            return api_response(400, 'Validation failed.', error=ser.errors)
        ser.save()
        return api_response(200, 'Profile updated successfully.', data=ser.data)


# ---- Transporter: Documents ----
class TransporterDocumentListCreate(generics.ListCreateAPIView):
    permission_classes = [IsAuthenticated, IsTransporter]

    def _allowed_doc_types(self):
        try:
            account_type = self.request.user.transporter_profile.account_type
        except TransporterProfile.DoesNotExist:
            account_type = TransporterProfile.AccountType.DRIVER
        return transporter_doc_types_for_account(account_type)

    def get_queryset(self):
        return KYCDocument.objects.filter(
            user=self.request.user,
            document_type__in=self._allowed_doc_types(),
        ).order_by('-submitted_at')

    def list(self, request, *args, **kwargs):
        qs = self.get_queryset()
        ser = KYCDocumentSerializer(qs, many=True, context={'request': request})
        return api_response(200, 'Documents retrieved successfully.', data=ser.data)

    def create(self, request, *args, **kwargs):
        ser = TransporterDocumentUploadSerializer(
            data=request.data,
            context={'request': request, 'user': request.user},
        )
        if not ser.is_valid():
            return api_response(400, 'Validation failed.', error=ser.errors)
        kyc_rel = f"kyc/{timezone.now().strftime('%Y/%m/%d')}"
        ensure_upload_dir(kyc_rel)
        doc = ser.save(user=request.user)
        return api_response(
            201,
            'Document uploaded successfully.',
            data=KYCDocumentSerializer(doc, context={'request': request}).data
        )


def _latest_traccar_lat_lon(tc_id, positions_map=None):
    """Live Traccar lat/lon for a device id. Returns (lat, lon) or None."""
    tc_id = (str(tc_id or '')).strip()
    if not tc_id:
        return None
    if positions_map is not None:
        pos = positions_map.get(tc_id)
    else:
        try:
            pos = get_latest_device_position(tc_id)
        except Exception:
            return None
    if not pos:
        return None
    try:
        return float(pos['latitude']), float(pos['longitude'])
    except (TypeError, ValueError, KeyError):
        return None


def _fleet_linked_drivers(owner):
    return [
        link.driver
        for link in TransporterDriverLink.objects.filter(
            transporter=owner,
            is_active=True,
        ).select_related('driver', 'driver__role', 'driver__transporter_profile')
        if link.driver.is_active
    ]


def _fleet_has_approved_driver(owner):
    for driver in _fleet_linked_drivers(owner):
        try:
            if driver.transporter_profile.documents_verified:
                return True
        except TransporterProfile.DoesNotExist:
            continue
    return False


def _fleet_has_verified_vehicle(owner):
    return Vehicle.objects.filter(owner=owner, is_verified=True, is_active=True).exists()


def _fleet_unavailable_payload():
    return {
        'driver_location': None,
        'driver_locations': None,
        'count': 0,
        'shipments': None,
    }


def _driver_assigned_verified_vehicle(owner, driver):
    return (
        Vehicle.objects.filter(
            owner=owner,
            assigned_driver=driver,
            is_verified=True,
            is_active=True,
        )
        .order_by('-updated_at')
        .first()
    )


def _fleet_located_drivers(owner):
    """Live GPS, country, and zone radius for each approved linked driver."""
    drivers = _fleet_linked_drivers(owner)
    try:
        positions_map = get_latest_positions_map() if drivers else {}
    except Exception:
        positions_map = {}
    located = []
    location_rows = []
    for driver in drivers:
        try:
            driver_profile = driver.transporter_profile
        except TransporterProfile.DoesNotExist:
            continue
        if not driver_profile.documents_verified:
            continue
        if not driver_profile.tc_id:
            continue
        pos = _latest_traccar_lat_lon(driver_profile.tc_id, positions_map)
        if not pos:
            continue
        lat, lon = pos
        country = country_code_for_point(lat, lon)
        if not country:
            continue
        radius_km, _ = transporter_load_search_radius_km(country)
        vehicle = _driver_assigned_verified_vehicle(owner, driver)
        types = vehicle_type_list(vehicle) if vehicle else []
        row = {
            'id': driver.id,
            'email': driver.email,
            'first_name': driver.first_name,
            'last_name': driver.last_name,
            'location': {'lat': lat, 'lon': lon},
            'country_code': country,
            'vehicle_type': types[0] if types else None,
            'vehicle_types': types,
        }
        location_rows.append(row)
        located.append((driver, lat, lon, country, radius_km, vehicle, row))
    return located, location_rows


def _available_shipment_item(shipment, *, nearest_distance_km=None, nearby_drivers=None):
    item = TransporterAvailableShipmentSerializer(shipment).data
    item['suggested_price'] = (
        str(shipment.suggested_price) if shipment.suggested_price is not None else None
    )
    item['nearest_distance_km'] = (
        round(nearest_distance_km, 2) if nearest_distance_km is not None else None
    )
    item['nearby_drivers'] = nearby_drivers or []
    return item


def _parse_load_type_param(request):
    """Return (load_type, error_response). error_response is set on invalid values."""
    try:
        return parse_load_type_query(request.query_params.get('load_type')), None
    except ValueError as exc:
        return None, api_response(400, 'Validation failed.', error=str(exc))


def _fleet_owner_available_shipments(request, profile):
    """Published loads near linked drivers' live GPS (pickup country + zone radius)."""
    load_type, load_type_error = _parse_load_type_param(request)
    if load_type_error:
        return load_type_error

    # Fleet owners: ignore country_code if live GPS exists. Use each approved driver's live GPS country
    # (a PK query param must not hide a driver who is currently in AE).
    located, driver_locations = _fleet_located_drivers(request.user)
    driver_location = None
    if driver_locations:
        loc = driver_locations[0]['location']
        driver_location = {'lat': loc['lat'], 'lon': loc['lon']}

    if not located:
        # If no linked driver is currently located or approved, fall back to country_code query param or default 'PK'
        requested_country = parse_required_country_code(request.query_params.get('country_code')) or getattr(profile, 'user_country_code', None) or 'PK'
        fallback_loads = []
        fleet_vehicles = transporter_verified_active_vehicles(request.user, profile.account_type)
        for shipment in _published_loads_for_country(requested_country, load_type=load_type):
            if not transporter_can_access_shipment(profile, shipment):
                continue
            if not shipment_matches_load_type_filter(shipment, load_type):
                continue
            if fleet_vehicles and not shipment_matches_transporter_vehicle_types(shipment, fleet_vehicles):
                continue
            fallback_loads.append(_available_shipment_item(
                shipment,
                nearest_distance_km=None,
                nearby_drivers=[],
            ))
        fallback_loads.sort(key=lambda item: item.get('created_at') or '', reverse=True)
        return api_response(
            200,
            'Available shipments retrieved successfully.',
            data={
                'driver_location': driver_location,
                'driver_locations': driver_locations,
                'count': len(fallback_loads),
                'shipments': fallback_loads,
            },
        )

    countries = []
    seen = set()
    for _driver, _lat, _lon, country, _radius, _vehicle, _row in located:
        if country not in seen:
            seen.add(country)
            countries.append(country)

    matches = []
    for country_code in countries:
        for shipment in _published_loads_for_country(country_code, load_type=load_type):
            if not shipment_matches_load_type_filter(shipment, load_type):
                continue
            pickup_lat = float(shipment.pickup_lat)
            pickup_lon = float(shipment.pickup_lon)
            nearby = []
            best_dist = None
            for _driver, lat, lon, driver_country, radius_km, vehicle, row in located:
                if driver_country != country_code:
                    continue
                distance_km = _haversine_km(lat, lon, pickup_lat, pickup_lon)
                if not _discovery_keeps_shipment(
                    profile, shipment, country_code, distance_km, radius_km,
                ):
                    continue
                if vehicle is not None and not vehicle_matches_required_type(
                    vehicle, shipment.vehicle_type_required,
                ):
                    continue
                nearby.append({
                    'id': row['id'],
                    'email': row['email'],
                    'first_name': row['first_name'],
                    'last_name': row['last_name'],
                    'distance_km': round(distance_km, 2),
                    'location': row['location'],
                    'vehicle_type': row['vehicle_type'],
                    'vehicle_types': row.get('vehicle_types') or (
                        [row['vehicle_type']] if row.get('vehicle_type') else []
                    ),
                })
                if best_dist is None or distance_km < best_dist:
                    best_dist = distance_km
            if not nearby:
                continue
            nearby.sort(key=lambda item: item['distance_km'])
            matches.append(_available_shipment_item(
                shipment,
                nearest_distance_km=best_dist,
                nearby_drivers=nearby,
            ))

    matches.sort(key=lambda item: item.get('created_at') or '', reverse=True)
    return api_response(
        200,
        'Available shipments retrieved successfully.',
        data={
            'driver_location': driver_location,
            'driver_locations': driver_locations,
            'count': len(matches),
            'shipments': matches,
        },
    )


def _individual_driver_available_shipments(request, profile):
    """Nearby published loads for the logged-in driver's Traccar GPS and vehicle type."""
    load_type, load_type_error = _parse_load_type_param(request)
    if load_type_error:
        return load_type_error

    country_code = parse_required_country_code(request.query_params.get('country_code'))
    if not country_code:
        return api_response(
            400,
            'Validation failed.',
            error='country_code is required and must be a valid ISO 3166-1 alpha-2 code.',
        )

    # Helper function to return all accessible published loads for the country
    def _fallback_all_country_loads():
        vehicles = transporter_verified_active_vehicles(request.user, profile.account_type)
        data = []
        for shipment in _published_loads_for_country(country_code, load_type=load_type):
            if not transporter_can_access_shipment(profile, shipment):
                continue
            if not shipment_matches_load_type_filter(shipment, load_type):
                continue
            if vehicles and not shipment_matches_transporter_vehicle_types(shipment, vehicles):
                continue
            data.append(_available_shipment_item(shipment, nearest_distance_km=None))
        data.sort(key=lambda item: item.get('created_at') or '', reverse=True)
        return api_response(
            200,
            'Available shipments retrieved successfully.',
            data={
                'driver_location': None,
                'count': len(data),
                'shipments': data,
            },
        )

    if not profile.tc_id:
        return _fallback_all_country_loads()

    try:
        position = get_latest_device_position(profile.tc_id)
    except Exception:
        return _fallback_all_country_loads()

    if not position:
        return _fallback_all_country_loads()

    lat = position.get('latitude')
    lon = position.get('longitude')
    if lat is None or lon is None:
        return _fallback_all_country_loads()

    driver_location = {'lat': float(lat), 'lon': float(lon)}

    driver_country = country_code_for_point(lat, lon)
    if driver_country != country_code:
        return _fallback_all_country_loads()

    search_radius_km, _radius_source = transporter_load_search_radius_km(driver_country)
    vehicles = transporter_verified_active_vehicles(request.user, profile.account_type)
    if not vehicles:
        return _fallback_all_country_loads()

    data = []
    for shipment in _published_loads_for_country(country_code, load_type=load_type):
        if not transporter_can_access_shipment(profile, shipment):
            continue
        if not shipment_matches_load_type_filter(shipment, load_type):
            continue
        if not shipment_matches_transporter_vehicle_types(shipment, vehicles):
            continue
        pickup_lat = float(shipment.pickup_lat)
        pickup_lon = float(shipment.pickup_lon)
        distance_km = _haversine_km(float(lat), float(lon), pickup_lat, pickup_lon)
        # Strict C2C filter is not radius-gated (matches load-discovery).
        if load_type != LOAD_TYPE_COUNTRY_TO_COUNTRY and distance_km > search_radius_km:
            continue
        data.append(_available_shipment_item(shipment, nearest_distance_km=distance_km))

    data.sort(key=lambda item: item.get('created_at') or '', reverse=True)
    return api_response(
        200,
        'Available shipments retrieved successfully.',
        data={
            'driver_location': driver_location,
            'count': len(data),
            'shipments': data,
        },
    )


@api_view(['GET'])
@permission_classes([IsAuthenticated, IsTransporter, IsTransporterVerified])
def transporter_available_shipments(request):
    """List published loads.

    Always fetches live Traccar positions on this request.
    Individual / fleet-linked drivers: required ``country_code``, live GPS
    in that country, zone radius, and matching verified+active vehicle type.
    Fleet owners: loads near linked drivers' live GPS (pickup country + zone).
    ``country_code`` is ignored; GPS country of each approved driver is used.
    """
    try:
        profile = request.user.transporter_profile
    except TransporterProfile.DoesNotExist:
        return api_response(404, 'Transporter profile not found.', error='Not found.')

    if profile.account_type == TransporterProfile.AccountType.FLEET_OWNER:
        return _fleet_owner_available_shipments(request, profile)
    return _individual_driver_available_shipments(request, profile)


def _discovery_load_item(request, shipment, distance_km, matched_vehicle, nearby_drivers):
    item = TransporterAvailableShipmentSerializer(
        shipment, context={'request': request},
    ).data
    item['suggested_price'] = (
        str(shipment.suggested_price) if shipment.suggested_price is not None else None
    )
    item['distance_from_driver_km'] = round(distance_km, 2)
    item['matched_vehicle_type'] = (
        matching_vehicle_type(matched_vehicle, shipment.vehicle_type_required)
        if matched_vehicle
        else None
    )
    capacity_tons = vehicle_capacity_tons(matched_vehicle) if matched_vehicle else None
    item['max_capacity_for_type'] = float(capacity_tons) if capacity_tons is not None else None
    item['favorite_for_this_shipper'] = FavoriteTransporter.objects.filter(
        shipper=shipment.shipper, transporter=request.user
    ).exists()
    item['nearby_drivers'] = nearby_drivers
    return item


def _published_loads_for_country(country_code, *, load_type=None):
    """Published loads for pickup country; optional strict load_type SQL filter."""
    from core.load_type import LOAD_TYPE_COUNTRY_TO_COUNTRY, LOAD_TYPE_LOCAL

    qs = Shipment.objects.filter(
        status=Shipment.Status.PUBLISHED,
        pickup_country_code__iexact=country_code,
    ).exclude(
        pickup_lat__isnull=True,
    ).exclude(
        pickup_lon__isnull=True,
    ).select_related(
        'shipper',
        'shipper__role',
        'shipper__shipper_profile',
    ).order_by('-created_at')

    if load_type == LOAD_TYPE_LOCAL:
        qs = qs.filter(local=True, country_to_country=False)
    elif load_type == LOAD_TYPE_COUNTRY_TO_COUNTRY:
        # Strict C2C: flag set, not local, and delivery country present.
        # Same-country rows are dropped in the Python filter (pickup != delivery).
        qs = qs.filter(local=False, country_to_country=True).exclude(
            delivery_country_code__isnull=True,
        ).exclude(
            delivery_country_code='',
        )
    return qs


def _is_cross_border_c2c_load(shipment, country_code):
    """C2C load with pickup in country_code and drop-off in a different country."""
    if not shipment.country_to_country:
        return False
    pickup = normalize_country_code(shipment.pickup_country_code)
    delivery = normalize_country_code(shipment.delivery_country_code)
    return pickup == country_code and bool(delivery) and delivery != pickup


def _discovery_keeps_shipment(profile, shipment, country_code, distance_km, radius_km):
    """Local drivers: nearby local loads only. C2C drivers: nearby local + in-country C2C."""
    if profile.local:
        return bool(shipment.local) and distance_km <= radius_km
    if profile.country_to_country:
        if shipment.local:
            return distance_km <= radius_km
        return _is_cross_border_c2c_load(shipment, country_code)
    return False


@api_view(['GET'])
@permission_classes([IsAuthenticated, IsTransporter, IsTransporterVerified])
def transporter_load_discovery(request):
    try:
        profile = request.user.transporter_profile
    except TransporterProfile.DoesNotExist:
        return api_response(404, 'Transporter profile not found.', error='Not found.')

    load_type, load_type_error = _parse_load_type_param(request)
    if load_type_error:
        return load_type_error

    country_code = parse_required_country_code(request.query_params.get('country_code'))
    if not country_code:
        return api_response(
            400,
            'Validation failed.',
            error='country_code is required and must be a valid ISO 3166-1 alpha-2 code.',
        )

    if not profile.tc_id:
        return api_response(
            400,
            'Driver tracking device is not configured.',
            error='Missing tc_id on transporter profile.',
        )

    try:
        position = get_latest_device_position(profile.tc_id)
    except ValueError as exc:
        return api_response(400, 'Unable to fetch driver location from Traccar.', error=str(exc))

    lat = position.get('latitude')
    lon = position.get('longitude')
    if lat is None or lon is None:
        return api_response(400, 'Driver location is invalid.', error='Missing latitude/longitude.')

    search_radius_km, radius_source = transporter_load_search_radius_km(country_code)

    fleet_vehicles = transporter_verified_active_vehicles(request.user, profile.account_type)

    matches = []
    for shipment in _published_loads_for_country(country_code, load_type=load_type):
        if not shipment_matches_load_type_filter(shipment, load_type):
            continue
        if fleet_vehicles and not shipment_matches_transporter_vehicle_types(shipment, fleet_vehicles):
            continue

        pickup_lat = float(shipment.pickup_lat)
        pickup_lon = float(shipment.pickup_lon)
        distance_km = _haversine_km(float(lat), float(lon), pickup_lat, pickup_lon)
        if not _discovery_keeps_shipment(
            profile, shipment, country_code, distance_km, search_radius_km,
        ):
            continue

        matched_vehicle = find_matching_vehicle(shipment, fleet_vehicles) if fleet_vehicles else None
        matches.append(_discovery_load_item(
            request, shipment, distance_km, matched_vehicle, [],
        ))

    matches.sort(key=lambda item: item.get('created_at') or '', reverse=True)

    return api_response(
        200,
        'Nearby loads retrieved successfully.',
        data={
            'driver_location': {
                'lat': float(lat),
                'lon': float(lon),
            },
            'country_code': country_code,
            'radius_km': search_radius_km,
            'radius_source': radius_source,
            'count': len(matches),
            'loads': matches,
        },
    )


@api_view(['GET'])
@permission_classes([IsAuthenticated, IsTransporter])
def transporter_vehicles_assigned(request):
    """Vehicles where the logged-in user is assigned_driver (fleet owner assigned this driver)."""
    qs = (
        Vehicle.objects.filter(assigned_driver=request.user)
        .select_related('owner', 'assigned_driver')
        .order_by('-updated_at')
    )
    data = VehicleSerializer(qs, many=True).data
    return api_response(
        200,
        'Assigned vehicles retrieved successfully.',
        data={'count': len(data), 'vehicles': data},
    )


class TransporterVehicleListCreate(generics.ListCreateAPIView):
    permission_classes = [IsAuthenticated, IsTransporter]

    def get_queryset(self):
        return Vehicle.objects.filter(owner=self.request.user)

    def list(self, request, *args, **kwargs):
        ser = VehicleSerializer(self.get_queryset(), many=True, context={'request': request})
        return api_response(200, 'Vehicles retrieved successfully.', data=ser.data)

    def create(self, request, *args, **kwargs):
        ser = VehicleSerializer(data=request.data, context={'request': request})
        if not ser.is_valid():
            return api_response(400, 'Validation failed.', error=ser.errors)
        try:
            vehicle = ser.save(owner=request.user, is_active=False, is_verified=False)
        except IntegrityError:
            return api_response(
                400,
                'Validation failed.',
                error={
                    'registration_number': [
                        'You already have a vehicle with this registration number.',
                    ],
                },
            )
        from accounts.vehicle_docs import copy_kyc_vehicle_registration_to_vehicle

        copy_kyc_vehicle_registration_to_vehicle(vehicle)
        return api_response(
            201,
            'Vehicle created successfully.',
            data=VehicleSerializer(vehicle, context={'request': request}).data,
        )


class TransporterVehicleDetail(generics.RetrieveUpdateAPIView):
    permission_classes = [IsAuthenticated, IsTransporter]

    def get_queryset(self):
        return Vehicle.objects.filter(owner=self.request.user)

    def retrieve(self, request, *args, **kwargs):
        vehicle = self.get_object()
        return api_response(
            200,
            'Vehicle retrieved successfully.',
            data=VehicleSerializer(vehicle, context={'request': request}).data,
        )

    def update(self, request, *args, **kwargs):
        vehicle = self.get_object()
        ser = VehicleSerializer(vehicle, data=request.data, partial=True, context={'request': request})
        if not ser.is_valid():
            return api_response(400, 'Validation failed.', error=ser.errors)
        try:
            updated = ser.save()
        except IntegrityError:
            return api_response(
                400,
                'Validation failed.',
                error={
                    'registration_number': [
                        'You already have a vehicle with this registration number.',
                    ],
                },
            )
        return api_response(
            200,
            'Vehicle updated successfully.',
            data=VehicleSerializer(updated, context={'request': request}).data,
        )


@api_view(['POST'])
@permission_classes([IsAuthenticated, IsTransporter])
def transporter_vehicle_assign_driver(request, vehicle_pk):
    try:
        vehicle = Vehicle.objects.get(pk=vehicle_pk, owner=request.user)
    except Vehicle.DoesNotExist:
        return api_response(404, 'Vehicle not found.', error='Not found.')
    ser = VehicleDriverAssignSerializer(data=request.data)
    if not ser.is_valid():
        return api_response(400, 'Validation failed.', error=ser.errors)

    driver_id = ser.validated_data.get('driver_id')
    if driver_id is None:
        vehicle.assigned_driver = None
        vehicle.save(update_fields=['assigned_driver', 'updated_at'])
        return api_response(200, 'Driver unassigned successfully.', data=VehicleSerializer(vehicle).data)

    try:
        driver = User.objects.get(pk=driver_id)
    except User.DoesNotExist:
        return api_response(404, 'Driver not found.', error='Not found.')
    try:
        if driver.role.role != UserRole.Role.TRANSPORTER:
            return api_response(400, 'Assigned user must be a transporter/driver.', error='Invalid driver role.')
    except UserRole.DoesNotExist:
        return api_response(400, 'Assigned user has no role assigned.', error='Invalid driver role.')

    vehicle.assigned_driver = driver
    vehicle.save(update_fields=['assigned_driver', 'updated_at'])
    return api_response(200, 'Driver assigned successfully.', data=VehicleSerializer(vehicle).data)


@api_view(['POST'])
@permission_classes([IsAuthenticated, IsFleetOwnerTransporter, IsTransporterVerified])
def transporter_trip_assign_driver(request, trip_pk):
    try:
        trip = Trip.objects.select_related('transporter', 'assigned_driver', 'shipment').get(
            pk=trip_pk, transporter=request.user,
        )
    except Trip.DoesNotExist:
        return api_response(404, 'Trip not found.', error='Not found.')

    ser = TripAssignDriverSerializer(data=request.data)
    if not ser.is_valid():
        return api_response(400, 'Validation failed.', error=ser.errors)

    driver_id = ser.validated_data.get('driver_id')
    if driver_id is None:
        trip.assigned_driver = None
        trip.save(update_fields=['assigned_driver', 'updated_at'])
        return api_response(200, 'Driver unassigned successfully.', data=TransporterTripDetailSerializer(trip).data)

    try:
        driver = User.objects.get(pk=driver_id)
    except User.DoesNotExist:
        return api_response(404, 'Driver not found.', error='Not found.')

    try:
        if driver.role.role != UserRole.Role.TRANSPORTER:
            return api_response(400, 'Assigned user must be a transporter/driver.', error='Invalid driver role.')
    except UserRole.DoesNotExist:
        return api_response(400, 'Assigned user has no role assigned.', error='Invalid driver role.')

    if not TransporterDriverLink.objects.filter(transporter=request.user, driver=driver, is_active=True).exists():
        return api_response(400, 'Driver is not linked to this transporter.', error='Invalid driver link.')

    trip.assigned_driver = driver
    trip.save(update_fields=['assigned_driver', 'updated_at'])
    # Ensure trip is linked on the shipper↔owner conversation so chat access derives from assigned_driver.
    try:
        conv = trip.conversation
    except Conversation.DoesNotExist:
        conv = _get_or_create_negotiation_conversation(trip.shipment, trip.transporter, trip=trip)
    else:
        if conv.trip_id != trip.id:
            conv.trip = trip
            conv.save(update_fields=['trip'])
    notify_users(
        [driver.id],
        'TRIP_ASSIGNED',
        {
            'trip_id': str(trip.id),
            'shipment_id': str(trip.shipment_id),
            'conversation_id': str(conv.id),
            'assigned_by': str(request.user.id),
        },
        title='Trip assigned',
        body='You have been assigned a new trip and added to the trip chat.',
    )
    return api_response(200, 'Driver assigned successfully.', data=TransporterTripDetailSerializer(trip).data)


# ---- Fleet Owner: Driver CRUD ----
@api_view(['GET', 'POST'])
@permission_classes([IsAuthenticated, IsFleetOwnerTransporter])
def fleet_driver_list_create(request):
    if request.method == 'GET':
        links = (
            TransporterDriverLink.objects
            .filter(transporter=request.user, is_active=True)
            .select_related('driver', 'driver__role', 'driver__transporter_profile')
            .order_by('-created_at')
        )
        data = FleetDriverSerializer(links, many=True, context={'request': request}).data
        return api_response(200, 'Fleet drivers retrieved successfully.', data={'count': len(data), 'drivers': data})

    ser = FleetDriverCreateSerializer(
        data=request.data,
        context={'fleet_owner': request.user, 'request': request},
    )
    if not ser.is_valid():
        return api_response(400, 'Validation failed.', error=ser.errors)
    try:
        driver_user = ser.save()
    except serializers.ValidationError as exc:
        return api_response(400, 'Validation failed.', error=exc.detail)
    link = TransporterDriverLink.objects.select_related(
        'driver', 'driver__role', 'driver__transporter_profile',
    ).get(transporter=request.user, driver=driver_user)
    return api_response(
        201,
        'Driver added to fleet successfully.',
        data=FleetDriverSerializer(link, context={'request': request}).data,
    )


@api_view(['GET', 'PATCH', 'DELETE'])
@permission_classes([IsAuthenticated, IsFleetOwnerTransporter])
def fleet_driver_detail(request, driver_id):
    try:
        link = (
            TransporterDriverLink.objects
            .select_related('driver', 'driver__role', 'driver__transporter_profile')
            .get(transporter=request.user, driver_id=driver_id)
        )
    except TransporterDriverLink.DoesNotExist:
        return api_response(404, 'Driver not found in your fleet.', error='Not found.')

    if request.method == 'GET':
        return api_response(
            200,
            'Driver retrieved successfully.',
            data=FleetDriverSerializer(link, context={'request': request}).data,
        )

    if request.method == 'PATCH':
        ser = FleetDriverUpdateSerializer(data=request.data, context={'request': request})
        if not ser.is_valid():
            return api_response(400, 'Validation failed.', error=ser.errors)
        driver = link.driver
        changed_user = []
        changed_role = []

        if 'email' in ser.validated_data:
            new_email = ser.validated_data['email'].strip().lower()
            if new_email != driver.email:
                if User.objects.filter(username=new_email).exclude(pk=driver.pk).exists():
                    return api_response(400, 'Email already in use by another account.', error='Duplicate email.')
                driver.username = new_email
                driver.email = new_email
                changed_user.extend(['username', 'email'])

        if 'password' in ser.validated_data:
            driver.set_password(ser.validated_data['password'])
            changed_user.append('password')

        for field in ('first_name', 'last_name'):
            if field in ser.validated_data:
                setattr(driver, field, ser.validated_data[field])
                changed_user.append(field)
        if changed_user:
            driver.save(update_fields=changed_user)

        if 'phone' in ser.validated_data:
            try:
                driver.role.phone = ser.validated_data['phone']
                changed_role.append('phone')
            except UserRole.DoesNotExist:
                pass
        if 'language' in ser.validated_data:
            try:
                driver.role.language = ser.validated_data['language']
                changed_role.append('language')
            except UserRole.DoesNotExist:
                pass
        if changed_role:
            driver.role.save(update_fields=changed_role + ['updated_at'])

        avatar_file = ser.validated_data.get('avatar')
        if avatar_file:
            try:
                profile = driver.transporter_profile
            except TransporterProfile.DoesNotExist:
                profile = None
            if profile is not None:
                profile.avatar_url = save_avatar_and_get_url(
                    avatar_file,
                    request,
                    kind='user',
                )
                profile.save(update_fields=['avatar_url', 'updated_at'])

        if 'documents' in ser.validated_data:
            try:
                upsert_driver_kyc_documents(
                    driver,
                    ser.validated_data.get('documents') or [],
                    request=request,
                )
            except serializers.ValidationError as exc:
                return api_response(400, 'Validation failed.', error=exc.detail)

        link.refresh_from_db()
        return api_response(
            200,
            'Driver updated successfully.',
            data=FleetDriverSerializer(link, context={'request': request}).data,
        )

    # DELETE — deactivate link
    link.is_active = False
    link.save(update_fields=['is_active', 'updated_at'])
    return api_response(200, 'Driver removed from fleet.', data={'driver_id': driver_id, 'is_active': False})


@api_view(['GET', 'POST'])
@permission_classes([IsAuthenticated, IsFleetOwnerTransporter])
def fleet_driver_documents(request, driver_id):
    """List/upload KYC docs for a linked fleet driver (same types/expiry as individual driver)."""
    try:
        link = (
            TransporterDriverLink.objects
            .select_related('driver')
            .get(transporter=request.user, driver_id=driver_id, is_active=True)
        )
    except TransporterDriverLink.DoesNotExist:
        return api_response(404, 'Driver not found in your fleet.', error='Not found.')

    driver = link.driver
    if request.method == 'GET':
        qs = KYCDocument.objects.filter(
            user=driver,
            document_type__in=TRANSPORTER_DRIVER_DOC_TYPES,
        ).order_by('-submitted_at')
        ser = KYCDocumentSerializer(qs, many=True, context={'request': request})
        return api_response(200, 'Driver documents retrieved successfully.', data=ser.data)

    ser = TransporterDocumentUploadSerializer(
        data=request.data,
        context={
            'request': request,
            'user': driver,
            'force_account_type': TransporterProfile.AccountType.DRIVER,
        },
    )
    if not ser.is_valid():
        return api_response(400, 'Validation failed.', error=ser.errors)
    kyc_rel = f"kyc/{timezone.now().strftime('%Y/%m/%d')}"
    ensure_upload_dir(kyc_rel)
    doc = ser.save(user=driver)
    return api_response(
        201,
        'Driver document uploaded successfully.',
        data=KYCDocumentSerializer(doc, context={'request': request}).data,
    )


@api_view(['GET'])
@permission_classes([IsAuthenticated, IsFleetOwnerTransporter])
def fleet_driver_current_location(request, driver_id):
    """
    Latest Traccar position for a linked fleet driver.
    Resolves tc_id from the driver's transporter profile.
    """
    try:
        link = (
            TransporterDriverLink.objects
            .select_related('driver', 'driver__role', 'driver__transporter_profile')
            .get(transporter=request.user, driver_id=driver_id, is_active=True)
        )
    except TransporterDriverLink.DoesNotExist:
        return api_response(404, 'Driver not found in your fleet.', error='Not found.')

    driver = link.driver
    try:
        profile = driver.transporter_profile
    except TransporterProfile.DoesNotExist:
        return api_response(404, 'Driver profile not found.', error='Not found.')

    tc_id = str(profile.tc_id or '').strip()
    if not tc_id:
        return api_response(
            400,
            'Driver tracking device is not configured.',
            error='Missing tc_id on driver profile.',
        )

    try:
        position = get_latest_device_position(tc_id)
    except ValueError as exc:
        return api_response(400, 'Unable to fetch driver location from Traccar.', error=str(exc))

    lat = position.get('latitude')
    lon = position.get('longitude')
    if lat is None or lon is None:
        return api_response(400, 'Driver location is invalid.', error='Missing latitude/longitude.')

    raw = position.get('raw') or {}
    recorded_at = raw.get('fixTime') or raw.get('deviceTime')

    return api_response(
        200,
        'Driver current location retrieved successfully.',
        data={
            'driver': _trip_driver_summary(driver),
            'tc_id': tc_id,
            'tc_u_id': profile.tc_u_id or None,
            'position': {'lat': float(lat), 'lon': float(lon)},
            'recorded_at': recorded_at,
            'raw': raw,
        },
    )


class VehicleDocumentListCreate(generics.ListCreateAPIView):
    permission_classes = [IsAuthenticated, IsTransporter]

    def _get_vehicle(self):
        return Vehicle.objects.filter(pk=self.kwargs['vehicle_pk'], owner=self.request.user).first()

    def get_queryset(self):
        vehicle = self._get_vehicle()
        if not vehicle:
            return VehicleDocument.objects.none()
        return vehicle.documents.all()

    def list(self, request, *args, **kwargs):
        vehicle = self._get_vehicle()
        if not vehicle:
            return api_response(404, 'Vehicle not found.', error='Not found.')
        ser = VehicleDocumentSerializer(self.get_queryset(), many=True, context={'request': request})
        return api_response(200, 'Vehicle documents retrieved successfully.', data=ser.data)

    def create(self, request, *args, **kwargs):
        vehicle = self._get_vehicle()
        if not vehicle:
            return api_response(404, 'Vehicle not found.', error='Not found.')
        ser = VehicleDocumentUploadSerializer(data=request.data)
        if not ser.is_valid():
            return api_response(400, 'Validation failed.', error=ser.errors)
        rel = f"vehicle-docs/{timezone.now().strftime('%Y/%m/%d')}"
        ensure_upload_dir(rel)
        doc = ser.save(vehicle=vehicle)
        return api_response(
            201,
            'Vehicle document uploaded successfully.',
            data=VehicleDocumentSerializer(doc, context={'request': request}).data
        )


@api_view(['PATCH'])
@permission_classes([IsAuthenticated, IsPlatformAdmin])
def admin_vehicle_document_review(request, document_pk):
    try:
        document = VehicleDocument.objects.get(pk=document_pk)
    except VehicleDocument.DoesNotExist:
        return api_response(404, 'Vehicle document not found.', error='Not found.')
    ser = VehicleDocumentReviewSerializer(data=request.data)
    if not ser.is_valid():
        return api_response(400, 'Validation failed.', error=ser.errors)
    document.review_status = ser.validated_data['review_status']
    document.review_notes = ser.validated_data.get('review_notes', '')
    document.reviewed_at = timezone.now()
    document.reviewed_by = request.user
    update_fields = ['review_status', 'review_notes', 'reviewed_at', 'reviewed_by']
    if 'expiry_date' in ser.validated_data:
        document.expiry_date = ser.validated_data['expiry_date']
        update_fields.append('expiry_date')
    document.save(update_fields=update_fields)
    return api_response(
        200,
        'Vehicle document review updated successfully.',
        data=VehicleDocumentSerializer(document, context={'request': request}).data
    )


@api_view(['PATCH'])
@permission_classes([IsAuthenticated, IsPlatformAdmin])
def admin_vehicle_verify(request, vehicle_pk):
    try:
        vehicle = Vehicle.objects.get(pk=vehicle_pk)
    except Vehicle.DoesNotExist:
        return api_response(404, 'Vehicle not found.', error='Not found.')

    ser = VehicleVerificationSerializer(data=request.data)
    if not ser.is_valid():
        return api_response(400, 'Validation failed.', error=ser.errors)

    is_verified = ser.validated_data['is_verified']
    is_active = ser.validated_data.get('is_active', vehicle.is_active if not is_verified else True)
    update_fields = ['is_verified', 'is_active', 'updated_at']
    vehicle.is_verified = is_verified
    vehicle.is_active = is_active
    if is_verified:
        vehicle.verified_at = timezone.now()
        vehicle.verified_by = request.user
    else:
        vehicle.verified_at = None
        vehicle.verified_by = None
    update_fields.extend(['verified_at', 'verified_by'])
    vehicle.save(update_fields=update_fields)
    return api_response(200, 'Vehicle verification status updated successfully.', data=VehicleSerializer(vehicle).data)


@api_view(['GET', 'PATCH'])
@permission_classes([IsAuthenticated, IsPlatformAdmin])
def admin_platform_settings(request):
    ps = PlatformSettings.load()
    if request.method == 'GET':
        return api_response(
            200,
            'Platform settings retrieved successfully.',
            data=PlatformSettingsSerializer(ps).data,
        )
    if not request.user.is_superuser:
        return api_response(403, 'Only superusers may update platform settings.')
    ser = PlatformSettingsSerializer(ps, data=request.data, partial=True)
    if not ser.is_valid():
        return api_response(400, 'Validation failed.', error=ser.errors)
    ser.save()
    from accounts.platform_config import apply_platform_settings_to_django

    apply_platform_settings_to_django()
    return api_response(
        200,
        'Platform settings updated successfully.',
        data=PlatformSettingsSerializer(ps).data,
    )


# ---- Driver: Push location ----
@api_view(['POST'])
@permission_classes([IsAuthenticated, IsTransporter])
def driver_push_location(request, trip_pk):
    try:
        trip = Trip.objects.select_related('shipment').get(pk=trip_pk)
    except Trip.DoesNotExist:
        return api_response(404, 'Trip not found.', error='Not found.')
    if request.user.id not in (trip.transporter_id, trip.assigned_driver_id):
        return api_response(404, 'Trip not found.', error='Not found.')
    lat = request.data.get('lat')
    lon = request.data.get('lon')
    if lat is None or lon is None:
        return api_response(400, 'lat and lon are required.', error='Missing fields.')
    TripLocation.objects.create(trip=trip, lat=lat, lon=lon)
    trip.current_lat = lat
    trip.current_lon = lon
    trip.save(update_fields=['current_lat', 'current_lon'])
    return api_response(201, 'Location updated successfully.', data={'unique_id': trip.shipment.unique_id})


@api_view(['PATCH'])
@permission_classes([IsAuthenticated, IsTransporter])
def driver_trip_update_status(request, trip_pk):
    try:
        trip = Trip.objects.select_related('shipment').get(pk=trip_pk)
    except Trip.DoesNotExist:
        return api_response(404, 'Trip not found.', error='Not found.')
    if request.user.id not in (trip.transporter_id, trip.assigned_driver_id):
        return api_response(404, 'Trip not found.', error='Not found.')

    ser = DriverTripStatusSerializer(data=request.data)
    if not ser.is_valid():
        return api_response(400, 'Validation failed.', error=ser.errors)

    new_status = ser.validated_data['status']
    if new_status not in Trip.Status.values:
        return api_response(400, 'Invalid trip status value.', error='Invalid status.')

    allowed = TRIP_SEQUENTIAL_NEXT.get(trip.status, set())
    if new_status not in allowed:
        return api_response(
            400,
            f'Transition from {trip.status} to {new_status} is not allowed.',
            error='Invalid transition.',
        )

    err = _validate_trip_status_gps_time(ser.validated_data)
    if err:
        return api_response(400, err, error=err)

    lat = ser.validated_data.get('lat')
    lon = ser.validated_data.get('lon')
    update_fields = ['status', 'updated_at']
    if lat is not None and lon is not None:
        TripLocation.objects.create(trip=trip, lat=lat, lon=lon)
        trip.current_lat = lat
        trip.current_lon = lon
        update_fields.extend(['current_lat', 'current_lon'])

    trip.status = new_status
    trip.save(update_fields=update_fields)
    record_trip_status(trip, new_status)
    _sync_shipment_status_from_trip(trip, new_status)
    if new_status == Trip.Status.COMPLETED:
        ensure_invoice_for_completed_trip(trip=trip)
    trip = Trip.objects.select_related('shipment').get(pk=trip.pk)
    _notify_trip_progress(
        trip,
        request.user,
        'TRIP_STATUS',
        {'trip_id': str(trip.id), 'shipment_id': str(trip.shipment_id), 'status': str(new_status)},
        title='Trip update',
        body=f'Trip status: {new_status}',
    )
    return api_response(
        200,
        'Trip status updated successfully.',
        data=TripListSerializer(trip).data,
    )


# ---- Driver: Submit POD ----
@api_view(['GET', 'POST'])
@permission_classes([IsAuthenticated, IsTransporter])
def driver_submit_pod(request, trip_pk):
    try:
        trip = Trip.objects.select_related('shipment').get(pk=trip_pk)
    except Trip.DoesNotExist:
        return api_response(404, 'Trip not found.', error='Not found.')
    if request.user.id not in (trip.transporter_id, trip.assigned_driver_id):
        return api_response(404, 'Trip not found.', error='Not found.')

    if request.method == 'GET':
        try:
            pod = ProofOfDelivery.objects.prefetch_related('photos').get(trip=trip)
        except ProofOfDelivery.DoesNotExist:
            return api_response(404, 'POD not yet submitted.', error='Not found.')
        return api_response(200, 'POD retrieved successfully.', data=PODSerializer(pod, context={'request': request}).data)

    max_km = float(getattr(django_settings, 'POD_MAX_DELIVERY_DISTANCE_KM', 0) or 0)
    if max_km > 0:
        dlat, dlon = request.data.get('delivery_lat'), request.data.get('delivery_lon')
        sh = trip.shipment
        if (
            dlat not in (None, '')
            and dlon not in (None, '')
            and sh.delivery_lat is not None
            and sh.delivery_lon is not None
        ):
            try:
                gap = _haversine_km(float(dlat), float(dlon), float(sh.delivery_lat), float(sh.delivery_lon))
            except (TypeError, ValueError):
                gap = None
            if gap is not None and gap > max_km:
                return api_response(
                    400,
                    f'POD GPS is farther than {max_km} km from the shipment delivery coordinates.',
                    error='POD location validation failed.',
                )

    existing_pod = ProofOfDelivery.objects.filter(trip=trip).first()

    if existing_pod:
        if trip.status not in (Trip.Status.IN_TRANSIT, Trip.Status.ARRIVED_DELIVERY, Trip.Status.DELIVERED):
            return api_response(
                400,
                'POD can only be submitted while the trip is in transit, arrived at delivery, or delivered.',
                error='Invalid trip status for POD.',
            )
    else:
        if trip.status not in (Trip.Status.IN_TRANSIT, Trip.Status.ARRIVED_DELIVERY):
            return api_response(
                400,
                'POD can only be submitted while the trip is in transit or arrived at delivery.',
                error='Invalid trip status for POD.',
            )

    receiver_name = (
        request.data.get('receiver_name')
        or request.data.get('name')
        or request.data.get('receiver')
        or ''
    )
    if isinstance(receiver_name, str):
        receiver_name = receiver_name.strip()

    if not existing_pod and not receiver_name:
        return api_response(400, 'receiver_name is required.', error='Missing field.')

    # Ensure POD upload directories exist with permissions before saving
    date_path = timezone.now().strftime('%Y/%m/%d')
    ensure_upload_dir(f'pod/signatures/{date_path}')
    ensure_upload_dir(f'pod/photos/{date_path}')

    _delivered = request.data.get('delivered_at')
    if _delivered:
        try:
            if isinstance(_delivered, str):
                delivered_at = datetime.fromisoformat(_delivered.replace('Z', '+00:00'))
                delivered_at = timezone.make_aware(delivered_at) if timezone.is_naive(delivered_at) else delivered_at
            else:
                delivered_at = _delivered
        except Exception:
            delivered_at = timezone.now()
    else:
        delivered_at = timezone.now() if not existing_pod else existing_pod.delivered_at

    signature_file = request.FILES.get('receiver_signature') or request.FILES.get('signature')

    # Collect all uploaded photos/POD files from multiple keys or list formats
    uploaded_photos = []
    seen_files = set()
    for key in request.FILES:
        if key in ('receiver_signature', 'signature'):
            continue
        for f in request.FILES.getlist(key):
            if f not in seen_files:
                seen_files.add(f)
                uploaded_photos.append(f)

    if existing_pod:
        pod = existing_pod
        update_fields = []
        if receiver_name:
            pod.receiver_name = receiver_name
            update_fields.append('receiver_name')
        if signature_file:
            pod.receiver_signature = signature_file
            update_fields.append('receiver_signature')
        if request.data.get('delivery_lat') is not None:
            pod.delivery_lat = request.data.get('delivery_lat')
            update_fields.append('delivery_lat')
        if request.data.get('delivery_lon') is not None:
            pod.delivery_lon = request.data.get('delivery_lon')
            update_fields.append('delivery_lon')
        if _delivered:
            pod.delivered_at = delivered_at
            update_fields.append('delivered_at')
        if update_fields:
            pod.save(update_fields=update_fields)
    else:
        pod = ProofOfDelivery.objects.create(
            trip=trip,
            receiver_name=receiver_name,
            receiver_signature=signature_file,
            delivery_lat=request.data.get('delivery_lat'),
            delivery_lon=request.data.get('delivery_lon'),
            delivered_at=delivered_at,
        )

    for img in uploaded_photos:
        PODPhoto.objects.create(pod=pod, image=img)

    if trip.status != Trip.Status.DELIVERED:
        trip.status = Trip.Status.DELIVERED
        trip.save(update_fields=['status', 'updated_at'])
        record_trip_status(trip, Trip.Status.DELIVERED)
        shipment = trip.shipment
        shipment.status = Shipment.Status.DELIVERED
        shipment.save(update_fields=['status', 'updated_at'])
        trip = Trip.objects.select_related('shipment').get(pk=trip.pk)
        _notify_trip_progress(
            trip,
            request.user,
            'TRIP_STATUS',
            {'trip_id': str(trip.id), 'shipment_id': str(shipment.id), 'status': str(Trip.Status.DELIVERED)},
            title='Trip update',
            body=f'Trip status: {Trip.Status.DELIVERED}',
        )
        _notify_trip_progress(
            trip,
            request.user,
            'POD_SUBMITTED',
            {'trip_id': str(trip.id), 'shipment_id': str(shipment.id)},
            title='Proof of delivery',
            body='POD was submitted for your shipment.',
        )

    return api_response(
        201 if not existing_pod else 200,
        'POD submitted successfully.' if not existing_pod else 'POD updated successfully.',
        data=PODSerializer(pod, context={'request': request}).data
    )


@api_view(['GET'])
@permission_classes([IsAuthenticated, IsTransporter])
def transporter_trip_detail(request, trip_pk):
    try:
        trip = Trip.objects.select_related(
            'shipment',
            'shipment__shipper',
            'shipment__shipper__role',
            'shipment__shipper__shipper_profile',
            'accepted_bid',
            'transporter',
            'assigned_driver',
        ).get(
            pk=trip_pk
        )
    except Trip.DoesNotExist:
        return api_response(404, 'Trip not found.', error='Not found.')
    if request.user.id not in (trip.transporter_id, trip.assigned_driver_id):
        return api_response(404, 'Trip not found.', error='Not found.')
    return api_response(
        200,
        'Trip retrieved successfully.',
        data=TransporterTripDetailSerializer(trip).data,
    )


@api_view(['GET'])
@permission_classes([IsAuthenticated, IsTransporter])
def transporter_trip_navigation(request, trip_pk):
    try:
        trip = Trip.objects.select_related('shipment', 'transporter__transporter_profile').get(pk=trip_pk)
    except Trip.DoesNotExist:
        return api_response(404, 'Trip not found.', error='Not found.')
    if request.user.id not in (trip.transporter_id, trip.assigned_driver_id):
        return api_response(404, 'Trip not found.', error='Not found.')
    s = trip.shipment
    profile = getattr(trip.transporter, 'transporter_profile', None)
    tc_id = str(profile.tc_id) if profile and profile.tc_id else None
    tc_u_id = profile.tc_u_id if profile else None
    base = (os.getenv('TRACCAR_URL', '') or '').strip().rstrip('/')
    traccar_hint = f'{base}/#/devices/{tc_id}' if base and tc_id else None
    pickup = {
        'address': s.pickup_address,
        'company': s.pickup_company or '',
        'lat': str(s.pickup_lat) if s.pickup_lat is not None else None,
        'lon': str(s.pickup_lon) if s.pickup_lon is not None else None,
    }
    delivery = {
        'address': s.delivery_address,
        'company': s.dropoff_company or '',
        'lat': str(s.delivery_lat) if s.delivery_lat is not None else None,
        'lon': str(s.delivery_lon) if s.delivery_lon is not None else None,
    }
    maps_url = None
    naive_eta_minutes = None
    d_km = None
    try:
        if (
            s.pickup_lat is not None
            and s.pickup_lon is not None
            and s.delivery_lat is not None
            and s.delivery_lon is not None
        ):
            d_km = _haversine_km(
                float(s.pickup_lat),
                float(s.pickup_lon),
                float(s.delivery_lat),
                float(s.delivery_lon),
            )
            speed = float(getattr(django_settings, 'NAVIGATION_NAIVE_SPEED_KPH', 45) or 45)
            if speed > 0:
                naive_eta_minutes = int(round((d_km / speed) * 60))
            plat, plon = float(s.pickup_lat), float(s.pickup_lon)
            dlat, dlon = float(s.delivery_lat), float(s.delivery_lon)
            maps_url = (
                f'https://www.google.com/maps/dir/?api=1'
                f'&origin={plat},{plon}&destination={dlat},{dlon}&travelmode=driving'
            )
    except (TypeError, ValueError):
        pass

    return api_response(
        200,
        'Navigation context retrieved successfully.',
        data={
            'trip_id': trip.id,
            'unique_id': trip.shipment.unique_id,
            'shipment_id': s.id,
            'currency': currency_for_shipment(s),
            'pickup': pickup,
            'delivery': delivery,
            'transporter_profile': {'tc_id': tc_id, 'tc_u_id': tc_u_id},
            'traccar_device_url_hint': traccar_hint,
            'google_maps_directions_url': maps_url,
            'naive_route_distance_km': round(d_km, 2) if d_km is not None else None,
            'naive_eta_minutes': naive_eta_minutes,
        },
    )


@api_view(['GET'])
@permission_classes([IsAuthenticated, IsTransporter])
def transporter_rate_requests_list(request):
    """List rate requests for the authenticated driver/transporter."""
    status_filter = request.query_params.get('status')
    qs = RateRequest.objects.filter(driver=request.user).select_related('shipment', 'shipment__shipper', 'driver').order_by('-created_at')
    if status_filter:
        qs = qs.filter(status=status_filter.upper())
    ser = RateRequestSerializer(qs, many=True, context={'request': request})
    return api_response(200, 'Rate requests retrieved successfully.', data={'count': len(ser.data), 'results': ser.data})


@api_view(['POST'])
@permission_classes([IsAuthenticated, IsTransporter, IsTransporterVerified])
def transporter_rate_request_accept(request, pk):
    """Accept a rate request -> assigns load and creates trip."""
    try:
        rate_request = RateRequest.objects.select_related('shipment', 'driver').get(pk=pk, driver=request.user)
    except RateRequest.DoesNotExist:
        return api_response(404, 'Rate request not found.', error='Not found.')

    from core.rate_distribution import handle_rate_request_accept
    try:
        trip, updated_rr = handle_rate_request_accept(rate_request)
    except ValueError as exc:
        return api_response(400, str(exc), error='Accept failed.')

    return api_response(200, 'Rate request accepted and trip created successfully.', data={
        'rate_request': RateRequestSerializer(updated_rr, context={'request': request}).data,
        'trip_id': trip.id,
        'shipment_id': trip.shipment_id,
        'unique_id': trip.shipment.unique_id,
    })


@api_view(['POST'])
@permission_classes([IsAuthenticated, IsTransporter, IsTransporterVerified])
def transporter_rate_request_reject(request, pk):
    """Reject a rate request with optional reason -> advances next batch if all rejected."""
    try:
        rate_request = RateRequest.objects.select_related('shipment', 'driver').get(pk=pk, driver=request.user)
    except RateRequest.DoesNotExist:
        return api_response(404, 'Rate request not found.', error='Not found.')

    ser = RateRequestRejectSerializer(data=request.data)
    if not ser.is_valid():
        return api_response(400, 'Validation failed.', error=ser.errors)

    from core.rate_distribution import handle_rate_request_reject
    try:
        updated_rr = handle_rate_request_reject(rate_request, reason=ser.validated_data.get('reason', ''))
    except ValueError as exc:
        return api_response(400, str(exc), error='Reject failed.')

    return api_response(200, 'Rate request rejected successfully.', data=RateRequestSerializer(updated_rr, context={'request': request}).data)

