import json
import os
from datetime import datetime, timedelta
from decimal import Decimal

from django.conf import settings as django_settings
from django.contrib import messages
from django.contrib.auth import get_user_model
from django.core.paginator import Paginator
from django.db import connection, transaction
from django.http import HttpResponse, JsonResponse
from django.db.models import Count, Max, OuterRef, Prefetch, Q, Subquery, Sum
from django.db.models.functions import TruncDate, TruncMonth
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.utils import timezone
from django.views.decorators.http import require_http_methods, require_POST
from rest_framework.serializers import ValidationError as DRFValidationError

from accounts.decorators import admin_required, superuser_required
from accounts.models import (
    KYCDocument,
    PlatformSettings,
    ShipperProfile,
    TransporterDriverLink,
    TransporterProfile,
    UserRole,
    Vehicle,
    VehicleDocument,
)
from api.notify import notify_users
from api.serializers import (
    ShipperRegisterSerializer,
    TransporterRegisterSerializer,
    resolve_trip_assigned_vehicle,
)
from api.traccar import get_latest_device_position, get_latest_positions_map
from api.views import _sync_shipment_status_from_trip
from billing.models import Payment
from core.countries import (
    COUNTRY_BY_CODE,
    COUNTRY_CHOICES,
    country_name_for_code,
    country_name_to_code,
    is_valid_country_code,
    normalize_country_code,
)
from core.currencies import currency_choices, currency_code_for_country, currency_map_for_frontend
from core.datatables import (
    apply_datatables_order,
    datatables_response,
    is_datatables_request,
    paginate_datatables_queryset,
    parse_datatables_request,
)
from core.geocoding import reverse_geocode_address
from core.models import AdminTripAction, Bid, Conversation, FreightRoute, Message, ProofOfDelivery, RateRequest, Shipment, Trip, TripLocation, Zone
from core.freight_import import bulk_upsert_freight_routes, parse_freight_route_upload

from .forms import (
    AdminUserCreateForm,
    AdminUserEditForm,
    KYCDocumentReviewWebForm,
    PlatformSettingsForm,
    TripStatusOverrideWebForm,
    VehicleDocumentReviewWebForm,
)

User = get_user_model()

_DRIVER_ACCOUNT_TYPES = (
    TransporterProfile.AccountType.DRIVER,
    TransporterProfile.AccountType.TRANSPORTER_DRIVER,
)

PRE_PICKUP_STATUSES = {
    Trip.Status.ASSIGNED,
    Trip.Status.EN_ROUTE,
    Trip.Status.ARRIVED_PICKUP,
}

TRIP_TERMINAL_STATUSES = {Trip.Status.CLOSED, Trip.Status.COMPLETED}
COMMISSION_RATE = Decimal('0.20')
STALE_TRIP_STATUSES_LABEL = 'En Route, In Transit'


def _daily_count_series(qs, date_field, days):
    end = timezone.localdate()
    start = end - timedelta(days=days - 1)
    rows = (
        qs.filter(**{f'{date_field}__date__gte': start, f'{date_field}__date__lte': end})
        .annotate(bucket=TruncDate(date_field))
        .values('bucket')
        .annotate(c=Count('id'))
    )
    counts = {row['bucket']: row['c'] for row in rows}
    labels, data = [], []
    for offset in range(days):
        day = start + timedelta(days=offset)
        labels.append(day.strftime('%a') if days <= 7 else day.strftime('%b %d'))
        data.append(counts.get(day, 0))
    return {'labels': labels, 'data': data}


def _weekly_count_series(qs, date_field, weeks=4):
    end = timezone.localdate()
    start = end - timedelta(days=(weeks * 7) - 1)
    rows = (
        qs.filter(**{f'{date_field}__date__gte': start, f'{date_field}__date__lte': end})
        .annotate(bucket=TruncDate(date_field))
        .values('bucket')
        .annotate(c=Count('id'))
    )
    counts = {row['bucket']: row['c'] for row in rows}
    labels, data = [], []
    for week_idx in range(weeks):
        week_start = start + timedelta(days=week_idx * 7)
        week_end = min(week_start + timedelta(days=6), end)
        total = sum(counts.get(week_start + timedelta(days=d), 0) for d in range((week_end - week_start).days + 1))
        labels.append(f'Week {week_idx + 1}')
        data.append(total)
    return {'labels': labels, 'data': data}


def _monthly_count_series(qs, date_field, months=6):
    now = timezone.now()
    month_starts = []
    year, month = now.year, now.month
    for _ in range(months):
        month_starts.append((year, month))
        month -= 1
        if month == 0:
            month = 12
            year -= 1
    month_starts.reverse()
    start_dt = timezone.make_aware(datetime(month_starts[0][0], month_starts[0][1], 1))
    rows = (
        qs.filter(**{f'{date_field}__gte': start_dt})
        .annotate(bucket=TruncMonth(date_field))
        .values('bucket')
        .annotate(c=Count('id'))
    )
    counts = {(row['bucket'].year, row['bucket'].month): row['c'] for row in rows if row['bucket']}
    labels, data = [], []
    for y, m in month_starts:
        labels.append(datetime(y, m, 1).strftime('%b'))
        data.append(counts.get((y, m), 0))
    return {'labels': labels, 'data': data}


def _revenue_trend_series(period, year):
    pay_qs = Payment.objects.filter(status=Payment.Status.CAPTURED)
    labels, revenue, commission = [], [], []

    if period == 'weekly':
        end = timezone.localdate()
        start = end - timedelta(days=27)
        rows = (
            pay_qs.filter(created_at__date__gte=start, created_at__date__lte=end)
            .annotate(bucket=TruncDate('created_at'))
            .values('bucket')
            .annotate(total=Sum('amount'))
        )
        totals = {row['bucket']: row['total'] or Decimal('0') for row in rows}
        for week_idx in range(4):
            week_start = start + timedelta(days=week_idx * 7)
            week_end = min(week_start + timedelta(days=6), end)
            amount = sum(totals.get(week_start + timedelta(days=d), Decimal('0')) for d in range((week_end - week_start).days + 1))
            labels.append(f'Week {week_idx + 1}')
            revenue.append(float(amount))
            commission.append(float(amount * COMMISSION_RATE))
        return {'labels': labels, 'revenue': revenue, 'commission': commission}

    if period == 'yearly':
        current_year = timezone.localdate().year
        start_year = current_year - 4
        rows = (
            pay_qs.filter(created_at__year__gte=start_year, created_at__year__lte=current_year)
            .values('created_at__year')
            .annotate(total=Sum('amount'))
            .order_by('created_at__year')
        )
        totals = {row['created_at__year']: row['total'] or Decimal('0') for row in rows}
        for y in range(start_year, current_year + 1):
            amount = totals.get(y, Decimal('0'))
            labels.append(str(y))
            revenue.append(float(amount))
            commission.append(float(amount * COMMISSION_RATE))
        return {'labels': labels, 'revenue': revenue, 'commission': commission}

    rows = (
        pay_qs.filter(created_at__year=year)
        .annotate(bucket=TruncMonth('created_at'))
        .values('bucket')
        .annotate(total=Sum('amount'))
    )
    totals = {(row['bucket'].year, row['bucket'].month): row['total'] or Decimal('0') for row in rows if row['bucket']}
    for month in range(1, 13):
        labels.append(datetime(year, month, 1).strftime('%b'))
        amount = totals.get((year, month), Decimal('0'))
        revenue.append(float(amount))
        commission.append(float(amount * COMMISSION_RATE))
    return {'labels': labels, 'revenue': revenue, 'commission': commission}


def _drivers_on_trip_ids():
    active_trips = Trip.objects.exclude(status__in=TRIP_TERMINAL_STATUSES)
    user_ids = set(active_trips.values_list('transporter_id', flat=True))
    user_ids.update(active_trips.exclude(assigned_driver_id__isnull=True).values_list('assigned_driver_id', flat=True))
    user_ids.discard(None)
    return user_ids


def _build_dashboard_active_trips(traccar_positions=None):
    traccar_positions = traccar_positions or {}
    trips = (
        Trip.objects.exclude(status__in=TRIP_TERMINAL_STATUSES)
        .select_related(
            'shipment',
            'transporter',
            'transporter__transporter_profile',
            'assigned_driver',
        )
        .order_by('-updated_at')[:40]
    )
    rows = []
    for trip in trips:
        lat = trip.current_lat
        lon = trip.current_lon
        profile = getattr(trip.transporter, 'transporter_profile', None)
        tc_id = str(profile.tc_id).strip() if profile and profile.tc_id else ''
        if tc_id:
            pos = traccar_positions.get(tc_id)
            if pos:
                lat = pos.get('latitude', lat)
                lon = pos.get('longitude', lon)
        driver = trip.assigned_driver or trip.transporter
        driver_name = f'{driver.first_name} {driver.last_name}'.strip() or driver.email
        rows.append({
            'id': trip.id,
            'unique_id': getattr(trip.shipment, 'unique_id', None),
            'status': trip.status,
            'status_display': trip.get_status_display(),
            'driver_name': driver_name,
            'pickup': trip.shipment.pickup_address,
            'delivery': trip.shipment.delivery_address,
            'updated_at': trip.updated_at.isoformat() if trip.updated_at else None,
            'lat': float(lat) if lat is not None else None,
            'lon': float(lon) if lon is not None else None,
        })
    return rows


def _build_dashboard_alerts(stale_trips, stale_hours):
    alerts = []
    if stale_trips:
        alerts.append({
            'level': 'warning',
            'message': f'{stale_trips} trip(s) overdue with no update for over {stale_hours}h (En Route or In Transit).',
        })
    pending_vehicle_docs = VehicleDocument.objects.filter(review_status=VehicleDocument.ReviewStatus.PENDING).count()
    if pending_vehicle_docs:
        alerts.append({
            'level': 'info',
            'message': f'{pending_vehicle_docs} vehicle document(s) awaiting review.',
        })
    pending_kyc = KYCDocument.objects.filter(verified=False).count()
    if pending_kyc:
        alerts.append({
            'level': 'info',
            'message': f'{pending_kyc} KYC document(s) pending verification.',
        })
    unpublished = Shipment.objects.filter(status=Shipment.Status.PUBLISHED).count()
    if unpublished:
        alerts.append({
            'level': 'info',
            'message': f'{unpublished} published load(s) on the board.',
        })
    active_trips = Trip.objects.exclude(status__in=TRIP_TERMINAL_STATUSES).count()
    if active_trips:
        alerts.append({
            'level': 'info',
            'message': f'{active_trips} active trip(s) in progress.',
        })
    if not alerts:
        alerts.append({'level': 'success', 'message': 'No critical alerts right now.'})
    return alerts


def _build_dashboard_payload():
    shipper_role = UserRole.Role.SHIPPER
    transporter_role = UserRole.Role.TRANSPORTER
    stale_hours = int(getattr(django_settings, 'ADMIN_STALE_TRIP_HOURS', 48) or 48)
    stale_cutoff = timezone.now() - timedelta(hours=stale_hours)

    trips_by_status = {
        row['status']: row['c'] for row in Trip.objects.values('status').annotate(c=Count('id'))
    }
    shipments_by_status = {
        row['status']: row['c'] for row in Shipment.objects.values('status').annotate(c=Count('id'))
    }
    active_trips = Trip.objects.exclude(status__in=TRIP_TERMINAL_STATUSES).count()
    stale_trips = Trip.objects.filter(
        status__in=[Trip.Status.EN_ROUTE, Trip.Status.IN_TRANSIT],
        updated_at__lt=stale_cutoff,
    ).count()
    pay_captured = Payment.objects.filter(status=Payment.Status.CAPTURED)
    pay_agg = pay_captured.aggregate(total=Sum('amount'), n=Count('id'))
    vehicles_total = Vehicle.objects.count()
    vehicles_verified = Vehicle.objects.filter(is_verified=True).count()
    vehicles_unverified = vehicles_total - vehicles_verified

    user_qs = User.objects.all()
    transporter_qs = User.objects.filter(role__role=transporter_role)
    trip_qs = Trip.objects.all()

    shipper_qs = user_qs.filter(role__role=shipper_role)
    shippers_total = shipper_qs.count()
    shippers_verified = shipper_qs.filter(shipper_profile__kyc_verified=True).count()
    shippers_unverified = shippers_total - shippers_verified

    fleet_owner_qs = user_qs.filter(
        role__role=transporter_role,
        transporter_profile__account_type=TransporterProfile.AccountType.FLEET_OWNER,
    )
    transporters_total = fleet_owner_qs.count()
    transporters_verified = fleet_owner_qs.filter(
        transporter_profile__documents_verified=True,
    ).count()
    transporters_unverified = transporters_total - transporters_verified

    individual_driver_qs = user_qs.filter(
        role__role=transporter_role,
        transporter_profile__account_type=TransporterProfile.AccountType.DRIVER,
    )
    fleet_driver_qs = user_qs.filter(
        role__role=transporter_role,
        transporter_profile__account_type=TransporterProfile.AccountType.TRANSPORTER_DRIVER,
    )
    individual_drivers_total = individual_driver_qs.count()
    individual_drivers_verified = individual_driver_qs.filter(
        transporter_profile__documents_verified=True,
    ).count()
    fleet_drivers_total = fleet_driver_qs.count()
    fleet_drivers_verified = fleet_driver_qs.filter(
        transporter_profile__documents_verified=True,
    ).count()
    drivers_total = individual_drivers_total + fleet_drivers_total

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

    on_trip_ids = _drivers_on_trip_ids()
    driver_rows = []
    vehicles = (
        Vehicle.objects.filter(is_active=True, is_verified=True, assigned_driver__isnull=False)
        .select_related('assigned_driver')
        .order_by('registration_number')[:50]
    )
    for vehicle in vehicles:
        driver = vehicle.assigned_driver
        name = f'{driver.first_name} {driver.last_name}'.strip() or driver.email
        on_trip = driver.id in on_trip_ids
        types = vehicle.type_list() if hasattr(vehicle, 'type_list') else (
            [vehicle.vehicle_type] if vehicle.vehicle_type else []
        )
        driver_rows.append({
            'name': name,
            'status': 'On Trip' if on_trip else 'Available',
            'vehicle_model': ', '.join(types) if types else vehicle.vehicle_type,
            'vehicle_no': vehicle.registration_number,
        })

    loadboard = []
    for shipment in Shipment.objects.filter(status=Shipment.Status.PUBLISHED).order_by('-created_at')[:25]:
        loadboard.append({
            'id': shipment.id,
            'unique_id': shipment.unique_id,
            'pickup': shipment.pickup_address,
            'delivery': shipment.delivery_address,
            'cargo_type': shipment.cargo_type,
            'weight': shipment.weight,
            'dimensions': shipment.dimensions or '—',
            'vehicle_type': shipment.vehicle_type_required,
            'distance': f'{shipment.distance_km} km' if shipment.distance_km is not None else '—',
            'price': str(shipment.suggested_price) if shipment.suggested_price is not None else '—',
        })

    current_year = timezone.localdate().year
    return {
        'updated_at': timezone.now().isoformat(),
        'counts': {
            'users': user_qs.count(),
            'shippers': {
                'total': shippers_total,
                'verified': shippers_verified,
                'unverified': shippers_unverified,
            },
            'transporters': {
                'total': transporters_total,
                'verified': transporters_verified,
                'unverified': transporters_unverified,
            },
            'drivers': {
                'total': drivers_total,
                'individual_driver': {
                    'total': individual_drivers_total,
                    'verified': individual_drivers_verified,
                    'unverified': individual_drivers_total - individual_drivers_verified,
                },
                'fleet_driver': {
                    'total': fleet_drivers_total,
                    'verified': fleet_drivers_verified,
                    'unverified': fleet_drivers_total - fleet_drivers_verified,
                },
            },
            'vehicles': {
                'total': vehicles_total,
                'verified': vehicles_verified,
                'unverified': vehicles_unverified,
            },
        },
        'kpis': {
            'active_trips': active_trips,
            'stale_trips': stale_trips,
            'stale_hours': stale_hours,
            'stale_statuses_label': STALE_TRIP_STATUSES_LABEL,
            'trips_by_status': trips_by_status,
            'shipments_by_status': shipments_by_status,
            'payments_captured_count': pay_agg['n'] or 0,
            'payments_captured_total': str(pay_agg['total'] or Decimal('0')),
            'vehicles_total': vehicles_total,
            'vehicles_verified': vehicles_verified,
            'transporters_documents_verified': TransporterProfile.objects.filter(documents_verified=True).count(),
        },
        'charts': {
            'user_registrations': {
                'week': _daily_count_series(user_qs, 'date_joined', 7),
                'month': _weekly_count_series(user_qs, 'date_joined', 4),
                'sixmonths': _monthly_count_series(user_qs, 'date_joined', 6),
            },
            'trip_history': {
                'week': _daily_count_series(trip_qs, 'created_at', 7),
                'month': _weekly_count_series(trip_qs, 'created_at', 4),
                'sixmonths': _monthly_count_series(trip_qs, 'created_at', 6),
            },
            'transporters': {
                'week': _daily_count_series(transporter_qs, 'date_joined', 7),
                'month': _weekly_count_series(transporter_qs, 'date_joined', 4),
                'sixmonths': _monthly_count_series(transporter_qs, 'date_joined', 6),
            },
            'revenue': {
                'weekly': _revenue_trend_series('weekly', current_year),
                'monthly': _revenue_trend_series('monthly', current_year),
                'yearly': _revenue_trend_series('yearly', current_year),
            },
        },
        'loadboard': loadboard,
        'active_trips': _build_dashboard_active_trips(traccar_positions),
        'drivers': driver_rows,
        'alerts': _build_dashboard_alerts(stale_trips, stale_hours),
    }


def _auth_user_has_platform_column():
    try:
        with connection.cursor() as cursor:
            columns = connection.introspection.get_table_description(cursor, User._meta.db_table)
    except Exception:
        return False
    return any(column.name == 'is_platformuser' for column in columns)


def _platform_user_queryset(qs):
    if not _auth_user_has_platform_column():
        return qs.none()
    return qs.extra(where=[f'{User._meta.db_table}.is_platformuser = %s'], params=[True])


def _set_platform_user(user, value=True):
    if not _auth_user_has_platform_column():
        return
    with connection.cursor() as cursor:
        cursor.execute(
            f'UPDATE {User._meta.db_table} SET is_platformuser = %s WHERE id = %s',
            [bool(value), user.id],
        )


def _is_ajax(request):
    return request.headers.get('x-requested-with') == 'XMLHttpRequest'


def _json_error(message, errors=None, status=400):
    return JsonResponse({'success': False, 'message': message, 'errors': errors or {}}, status=status)


def _kyc_document_ajax_data(doc):
    return {
        'id': doc.id,
        'verified': bool(doc.verified),
        'review_status': doc.review_status,
        'review_notes': doc.review_notes or '',
        'reviewed_at': doc.reviewed_at.strftime('%Y-%m-%d %H:%M') if doc.reviewed_at else '',
    }


def _vehicle_document_ajax_data(doc):
    return {
        'id': doc.id,
        'review_status': doc.review_status,
        'review_notes': doc.review_notes or '',
        'expiry_date': doc.expiry_date.strftime('%Y-%m-%d') if doc.expiry_date else '',
        'reviewed_at': doc.reviewed_at.strftime('%Y-%m-%d %H:%M') if doc.reviewed_at else '',
    }


def _admin_user_edit_ajax_payload(user, next_page=None):
    payload = {'success': True, 'message': 'User updated successfully.', 'user': _user_row_data(user)}
    row = None
    if next_page == 'shipper':
        row = _shipper_list_row_data(user)
        payload['user']['kyc_verified'] = row['kyc_verified']
    elif next_page == 'transporter':
        row = _transporter_list_row_data(user)
        payload['user']['documents_verified'] = row['documents_verified']
    elif next_page == 'drivers':
        row = _driver_list_row_data(user)
        payload['user']['documents_verified'] = row['documents_verified']
    return payload


def _user_row_data(user):
    role = getattr(user, 'role', None)
    return {
        'id': user.id,
        'first_name': user.first_name or '',
        'last_name': user.last_name or '',
        'name': f'{user.first_name} {user.last_name}'.strip(),
        'email': user.email or '',
        'phone': getattr(role, 'phone', '') or '',
        'role': getattr(role, 'role', UserRole.Role.ADMIN),
        'role_display': role.get_role_display() if role else '—',
        'is_active': bool(user.is_active),
        'active_display': 'Yes' if user.is_active else 'No',
        'joined': user.date_joined.strftime('%Y-%m-%d') if user.date_joined else '',
    }


def _zone_row_data(zone):
    radius = zone.radius_km
    rate = zone.rate_per_km
    return {
        'id': zone.id,
        'country_code': zone.country_code,
        'country_display': zone.country_display,
        'radius_km': str(radius) if radius is not None else '',
        'radius_km_display': f'{radius} km' if radius is not None else '—',
        'rate_per_km': str(rate) if rate is not None else '',
        'rate_per_km_display': str(rate) if rate is not None else '—',
        'allow_return_trips': zone.allow_return_trips,
        'allow_return_trips_display': 'Yes' if zone.allow_return_trips else 'No',
        'created_at': zone.created_at.strftime('%Y-%m-%d') if zone.created_at else '',
    }


def _parse_zone_radius_km(raw):
    value = (raw or '').strip()
    if not value:
        return None, 'Radius (km) is required.'
    try:
        radius_km = Decimal(value)
    except Exception:
        return None, 'Radius must be a valid number.'
    if radius_km <= 0:
        return None, 'Radius must be greater than 0.'
    return radius_km, None


def _parse_zone_rate_per_km(raw):
    value = (raw or '').strip()
    if not value:
        return None, None
    try:
        rate = Decimal(value)
    except Exception:
        return None, 'Price per km must be a valid number.'
    if rate < 0:
        return None, 'Price per km cannot be negative.'
    return rate, None


def _user_ownership_label(user):
    """Company / Individual driver (transporter) or shipper account-type label."""
    if user is None:
        return '—'
    transporter = getattr(user, 'transporter_profile', None)
    if transporter is not None:
        if transporter.account_type == TransporterProfile.AccountType.FLEET_OWNER:
            return 'Company'
        if transporter.account_type == TransporterProfile.AccountType.DRIVER:
            return 'Individual driver'
        return transporter.get_account_type_display()
    shipper = getattr(user, 'shipper_profile', None)
    if shipper is not None:
        if shipper.account_type == ShipperProfile.AccountType.BUSINESS:
            return 'Company'
        if shipper.account_type == ShipperProfile.AccountType.INDIVIDUAL:
            return 'Individual'
        return shipper.get_account_type_display()
    return '—'


def _vehicle_ownership_label(vehicle):
    """Company-owned (fleet owner) vs individual driver vehicle."""
    owner = getattr(vehicle, 'owner', None)
    if owner is None:
        return '—'
    return _user_ownership_label(owner)


def _vehicle_assigned_driver_name(vehicle):
    driver = getattr(vehicle, 'assigned_driver', None)
    if driver is None:
        return ''
    full_name = f'{driver.first_name} {driver.last_name}'.strip()
    return full_name or driver.email or '—'


def _vehicle_row_data(vehicle):
    types = vehicle.type_list() if hasattr(vehicle, 'type_list') else (
        [vehicle.vehicle_type] if vehicle.vehicle_type else []
    )
    return {
        'id': vehicle.id,
        'registration_number': vehicle.registration_number,
        'vehicle_type': ', '.join(types) if types else vehicle.vehicle_type,
        'vehicle_types': types,
        'owner_id': vehicle.owner_id,
        'owner_email': vehicle.owner.email if vehicle.owner_id else '',
        'ownership': _vehicle_ownership_label(vehicle),
        'assigned_driver_id': vehicle.assigned_driver_id,
        'assigned_driver_name': _vehicle_assigned_driver_name(vehicle),
        'load_capacity': str(vehicle.load_capacity),
        'is_verified': vehicle.is_verified,
        'is_active': vehicle.is_active,
        'created_at': vehicle.created_at.strftime('%Y-%m-%d') if vehicle.created_at else '',
        'verify_modal_url': reverse('core:vehicle_verify_modal', args=[vehicle.id]),
    }


def _loadboard_row_data(shipment):
    return {
        'shipment_id': shipment.unique_id or f'#{shipment.id}',
        'pickup': shipment.pickup_address,
        'delivery': shipment.delivery_address,
        'cargo_type': shipment.cargo_type,
        'weight': shipment.weight,
        'dimensions': shipment.dimensions or '—',
        'vehicle_type': shipment.vehicle_type_required,
        'distance': f'{shipment.distance_km} km' if shipment.distance_km is not None else '—',
        'price': str(shipment.suggested_price) if shipment.suggested_price is not None else '—',
    }


def _dashboard_loadboard_datatables(request):
    base_qs = Shipment.objects.filter(status=Shipment.Status.PUBLISHED)
    return paginate_datatables_queryset(
        request,
        base_qs,
        _loadboard_row_data,
        search_fields=[
            'unique_id',
            'pickup_address',
            'delivery_address',
            'cargo_type',
            'weight',
            'dimensions',
            'vehicle_type_required',
        ],
        order_fields=[
            'unique_id',
            'pickup_address',
            'delivery_address',
            'cargo_type',
            'weight',
            'dimensions',
            'vehicle_type_required',
            'distance_km',
            'suggested_price',
        ],
        default_order=['-created_at'],
    )


def _sync_shipment_after_trip_override(trip, new_trip_status):
    _sync_shipment_status_from_trip(trip, new_trip_status)
    if new_trip_status == Trip.Status.CLOSED:
        shipment = trip.shipment
        if shipment.status != Shipment.Status.CLOSED:
            shipment.status = Shipment.Status.CLOSED
            shipment.save(update_fields=['status', 'updated_at'])


def _create_portal_user_from_form(form, acting_user):
    """Create shipper/transporter via existing registration serializers (no HTTP API)."""
    cd = form.cleaned_data
    if cd['role'] == UserRole.Role.ADMIN:
        if not acting_user.is_superuser:
            raise ValueError('Only superusers may create Admin accounts.')
        email = cd['email']
        with transaction.atomic():
            user = User.objects.create_user(
                username=email,
                email=email,
                password=cd['password'],
                first_name=cd.get('first_name') or '',
                last_name=cd.get('last_name') or '',
            )
            UserRole.objects.create(
                user=user,
                role=UserRole.Role.ADMIN,
                phone=cd.get('phone') or '',
                language='en',
            )
        return user

    payload = {
        'email': cd['email'],
        'password': cd['password'],
        'first_name': cd.get('first_name') or '',
        'last_name': cd.get('last_name') or '',
        'phone': cd.get('phone') or '',
        'company_name': cd.get('company_name') or '',
        'account_type': cd['account_type'],
    }
    if cd['role'] == UserRole.Role.SHIPPER:
        payload['national_id_number'] = cd.get('national_id_number') or ''
        payload['national_id_expiry_date'] = cd.get('national_id_expiry_date')
        payload['passport_number'] = cd.get('passport_number') or ''
        payload['passport_expiry_date'] = cd.get('passport_expiry_date')
        ser = ShipperRegisterSerializer(data=payload)
    else:
        payload['language'] = 'en'
        ser = TransporterRegisterSerializer(data=payload)
    if not ser.is_valid():
        raise ValueError(ser.errors)
    try:
        return ser.save()
    except DRFValidationError as ve:
        detail = ve.detail
        if isinstance(detail, list):
            raise ValueError({'non_field_errors': [str(x) for x in detail]})
        if not isinstance(detail, dict):
            raise ValueError({'non_field_errors': [str(detail)]})
        out = {}
        for k, v in detail.items():
            out[k] = v if isinstance(v, list) else [str(v)]
        raise ValueError(out)


@admin_required
def dashboard(request):
    return render(request, 'core/dashboard.html')


@admin_required
@require_http_methods(['GET'])
def dashboard_data(request):
    if is_datatables_request(request) and (request.GET.get('table') or '').strip().lower() == 'loadboard':
        return _dashboard_loadboard_datatables(request)
    payload = _build_dashboard_payload()
    year_param = request.GET.get('year')
    if year_param:
        try:
            year = int(year_param)
            payload['charts']['revenue']['monthly'] = _revenue_trend_series('monthly', year)
            payload['charts']['revenue_year'] = year
        except ValueError:
            pass
    response = JsonResponse(payload)
    response['Cache-Control'] = 'no-store, no-cache, must-revalidate'
    return response


@admin_required
def sidebar(request):
    return render(request, 'core/sidebar.html')


def _shippers_list_queryset():
    return User.objects.filter(role__role=UserRole.Role.SHIPPER).select_related(
        'role',
        'shipper_profile',
    )


def _shipper_id_or_passport_display(profile):
    if not profile:
        return '—'
    national = (profile.national_id_number or '').strip()
    passport = (profile.passport_number or '').strip()
    if national and passport:
        return f'{national} / {passport}'
    if national:
        return national
    if passport:
        return passport
    return '—'


def _shipper_list_row_data(user):
    profile = getattr(user, 'shipper_profile', None)
    name = f'{user.first_name} {user.last_name}'.strip()
    return {
        'id': user.id,
        'name': name,
        'email': user.email or '',
        'phone': getattr(getattr(user, 'role', None), 'phone', '') or '—',
        'kyc_verified': bool(profile.kyc_verified) if profile else False,
        'account_type_display': profile.get_account_type_display() if profile else '—',
        'id_or_passport': _shipper_id_or_passport_display(profile),
        'joined': user.date_joined.strftime('%Y-%m-%d') if user.date_joined else '',
    }


@admin_required
def shipper(request):
    base_qs = _shippers_list_queryset()
    if is_datatables_request(request):
        return paginate_datatables_queryset(
            request,
            base_qs,
            _shipper_list_row_data,
            search_fields=[
                'first_name',
                'last_name',
                'email',
                'role__phone',
                'shipper_profile__national_id_number',
                'shipper_profile__passport_number',
            ],
            order_fields=[
                'first_name',
                'email',
                'role__phone',
                'shipper_profile__kyc_verified',
                'shipper_profile__account_type',
                'shipper_profile__national_id_number',
                'date_joined',
                None,
            ],
            default_order=['-date_joined'],
        )
    return render(request, 'core/shipper.html')


@admin_required
@require_http_methods(['GET'])
def admin_shipper_modal(request, user_id):
    u = get_object_or_404(
        _shippers_list_queryset()
        .prefetch_related(
            Prefetch(
                'kyc_documents',
                queryset=_person_kyc_documents_queryset(),
            )
        ),
        pk=user_id,
    )
    documents = _uploaded_kyc_documents(u.kyc_documents.all())
    return render(request, 'core/includes/shipper_modal.html', {'u': u, 'documents': documents})


def _transporters_list_queryset():
    return User.objects.filter(
        role__role=UserRole.Role.TRANSPORTER,
        transporter_profile__account_type=TransporterProfile.AccountType.FLEET_OWNER,
    ).select_related('role', 'transporter_profile')


def _transporter_list_row_data(user):
    profile = getattr(user, 'transporter_profile', None)
    name = f'{user.first_name} {user.last_name}'.strip()
    return {
        'id': user.id,
        'name': name,
        'company_name': profile.company_name if profile and profile.company_name else '—',
        'email': user.email or '',
        'phone': getattr(getattr(user, 'role', None), 'phone', '') or '—',
        'account_type_display': profile.get_account_type_display() if profile else '—',
        'documents_verified': bool(profile.documents_verified) if profile else False,
        'joined': user.date_joined.strftime('%Y-%m-%d') if user.date_joined else '',
    }


@admin_required
def transporter(request):
    base_qs = _transporters_list_queryset()
    if is_datatables_request(request):
        return paginate_datatables_queryset(
            request,
            base_qs,
            _transporter_list_row_data,
            search_fields=[
                'first_name',
                'last_name',
                'email',
                'transporter_profile__company_name',
                'role__phone',
            ],
            order_fields=[
                'first_name',
                'transporter_profile__company_name',
                'email',
                'role__phone',
                'transporter_profile__account_type',
                'transporter_profile__documents_verified',
                'date_joined',
                None,
            ],
            default_order=['-date_joined'],
        )
    return render(request, 'core/transporter.html')


@admin_required
@require_http_methods(['GET'])
def admin_transporter_modal(request, user_id):
    u = get_object_or_404(
        _transporters_list_queryset()
        .prefetch_related(
            Prefetch(
                'kyc_documents',
                queryset=_person_kyc_documents_queryset(),
            )
        ),
        pk=user_id,
    )
    documents = _uploaded_kyc_documents(u.kyc_documents.all())
    return render(request, 'core/includes/transporter_modal.html', {'u': u, 'documents': documents})


@admin_required
@require_http_methods(['GET'])
def admin_users(request):
    base_qs = (
        User.objects.select_related('role', 'shipper_profile', 'transporter_profile')
        .filter(is_staff=True, is_superuser=False)
    )
    if is_datatables_request(request):
        return paginate_datatables_queryset(
            request,
            base_qs,
            _user_row_data,
            search_fields=['email', 'username', 'first_name', 'last_name'],
            order_fields=['first_name', 'email', 'is_active', 'date_joined', None],
            default_order=['-date_joined'],
        )
    qs = base_qs.order_by('-id')
    q = (request.GET.get('q') or '').strip()
    if q:
        qs = qs.filter(
            Q(email__icontains=q) | Q(username__icontains=q) | Q(first_name__icontains=q) | Q(last_name__icontains=q)
        )
    if request.GET.get('format') == 'json' or _is_ajax(request):
        return JsonResponse({'data': [_user_row_data(user) for user in qs]})
    return render(request, 'core/admin/user_list.html', {'users': qs, 'q': q})


@admin_required
@require_http_methods(['GET', 'POST'])
def admin_user_create(request):
    if request.method == 'POST' and request.GET.get('next') == 'users':
        email = (request.POST.get('email') or '').strip().lower()
        password = request.POST.get('password') or ''
        if not email:
            if _is_ajax(request):
                return _json_error('Email is required.', {'email': ['Email is required.']})
            messages.error(request, 'Email is required.')
            return redirect('core:users')
        if len(password) < 8:
            if _is_ajax(request):
                return _json_error('Password must be at least 8 characters.', {'password': ['Password must be at least 8 characters.']})
            messages.error(request, 'Password must be at least 8 characters.')
            return redirect('core:users')
        if User.objects.filter(username=email).exists():
            if _is_ajax(request):
                return _json_error('A user with this email already exists.', {'email': ['A user with this email already exists.']})
            messages.error(request, 'A user with this email already exists.')
            return redirect('core:users')
        with transaction.atomic():
            created_user = User.objects.create_user(
                username=email,
                email=email,
                password=password,
                first_name=request.POST.get('first_name') or '',
                last_name=request.POST.get('last_name') or '',
                is_staff=True,
                is_active=True,
            )
            UserRole.objects.create(
                user=created_user,
                role=UserRole.Role.ADMIN,
                phone=request.POST.get('phone') or '',
                language='en',
            )
            _set_platform_user(created_user, True)
        if _is_ajax(request):
            return JsonResponse({'success': True, 'message': 'User created successfully.', 'user': _user_row_data(created_user)})
        messages.success(request, 'User created successfully.')
        return redirect('core:users')

    form = AdminUserCreateForm(
        request.POST or None,
        allow_admin_role=request.user.is_superuser,
    )
    if request.method == 'POST' and form.is_valid():
        try:
            created_user = _create_portal_user_from_form(form, request.user)
        except ValueError as exc:
            err = exc.args[0] if exc.args else 'Could not create user.'
            if isinstance(err, dict):
                for field, msgs in err.items():
                    fld = None if field in (None, 'non_field_errors') else field
                    seq = msgs if isinstance(msgs, list) else [msgs]
                    for m in seq:
                        form.add_error(fld, str(m))
            else:
                form.add_error(None, str(err))
        else:
            if request.GET.get('next') == 'users':
                _set_platform_user(created_user, True)
            messages.success(request, 'User created successfully.')
            return redirect('core:users')
    return render(request, 'core/admin/user_create.html', {'form': form})


@admin_required
@require_http_methods(['GET', 'POST'])
def admin_user_edit(request, user_id):
    target = get_object_or_404(User.objects.select_related('role'), pk=user_id)

    if request.method == 'POST':
        form = AdminUserEditForm(request.POST)
    else:
        form = AdminUserEditForm.for_user(target)
    if not request.user.is_superuser:
        form.fields['role'].choices = [c for c in UserRole.Role.choices if c[0] != UserRole.Role.ADMIN]

    if request.method == 'POST' and form.is_valid():
        cd = form.cleaned_data
        new_role = cd['role']
        new_email = (cd.get('email') or target.email or '').strip().lower()
        new_password = cd.get('password') or ''
        if new_role == UserRole.Role.ADMIN and not request.user.is_superuser:
            if _is_ajax(request):
                return _json_error('Only superusers may assign the Admin role.')
            messages.error(request, 'Only superusers may assign the Admin role.')
            return redirect('core:users_edit', user_id=user_id)
        if target.id == request.user.id and not cd.get('is_active'):
            if _is_ajax(request):
                return _json_error('You cannot deactivate your own account.')
            messages.error(request, 'You cannot deactivate your own account.')
            return redirect('core:users_edit', user_id=user_id)
        if new_email and new_email != target.email.lower() and User.objects.filter(username=new_email).exclude(pk=target.pk).exists():
            if _is_ajax(request):
                return _json_error('A user with this email already exists.', {'email': ['A user with this email already exists.']})
            messages.error(request, 'A user with this email already exists.')
            if request.GET.get('next') == 'users':
                return redirect('core:users')
            return redirect('core:users_edit', user_id=user_id)

        with transaction.atomic():
            update_fields = ['first_name', 'last_name', 'is_active']
            target.first_name = cd.get('first_name') or ''
            target.last_name = cd.get('last_name') or ''
            target.is_active = bool(cd.get('is_active'))
            if new_email:
                target.email = new_email
                target.username = new_email
                update_fields.extend(['email', 'username'])
            if new_password:
                target.set_password(new_password)
                update_fields.append('password')
            target.save(update_fields=update_fields)

            ur, created = UserRole.objects.get_or_create(
                user=target,
                defaults={
                    'role': new_role,
                    'phone': cd.get('phone') or '',
                    'language': 'en',
                },
            )
            if not created:
                ur.role = new_role
                ur.phone = cd.get('phone') or ''
                ur.save(update_fields=['role', 'phone', 'updated_at'])

            if request.GET.get('next') != 'users':
                try:
                    sp = target.shipper_profile
                    sp.kyc_verified = bool(cd.get('kyc_verified'))
                    sp.credit_approved = bool(cd.get('credit_approved'))
                    sp.save(update_fields=['kyc_verified', 'credit_approved', 'updated_at'])
                except ShipperProfile.DoesNotExist:
                    pass
                try:
                    tp = target.transporter_profile
                    tp.documents_verified = bool(cd.get('documents_verified'))
                    tp.save(update_fields=['documents_verified', 'updated_at'])
                except TransporterProfile.DoesNotExist:
                    pass
                if bool(cd.get('kyc_verified')) or bool(cd.get('documents_verified')):
                    KYCDocument.objects.filter(user=target).update(
                        verified=True,
                        reviewed_at=timezone.now(),
                    )
            if request.GET.get('next') == 'users':
                _set_platform_user(target, True)

        if _is_ajax(request):
            target.refresh_from_db()
            try:
                target.role
            except UserRole.DoesNotExist:
                pass
            try:
                target.shipper_profile
            except ShipperProfile.DoesNotExist:
                pass
            try:
                target.transporter_profile
            except TransporterProfile.DoesNotExist:
                pass
            return JsonResponse(_admin_user_edit_ajax_payload(target, request.GET.get('next')))
        messages.success(request, 'User updated successfully.')
        if request.GET.get('next') == 'shipper':
            return redirect('core:shipper')
        if request.GET.get('next') == 'transporter':
            return redirect('core:transporter')
        if request.GET.get('next') == 'drivers':
            return redirect('core:drivers')
        if request.GET.get('next') == 'users':
            return redirect('core:users')
        return redirect('core:users')

    if request.method == 'POST' and _is_ajax(request):
        return _json_error('Validation failed.', form.errors.get_json_data())
    return render(request, 'core/admin/user_edit.html', {'form': form, 'edit_user': target})


@admin_required
@require_POST
def admin_user_delete(request, user_id):
    target = get_object_or_404(User.objects.select_related('role'), pk=user_id)
    if target.id == request.user.id:
        if _is_ajax(request):
            return _json_error('You cannot delete your own account.')
        messages.error(request, 'You cannot delete your own account.')
        return redirect('core:users')
    if target.is_superuser and not request.user.is_superuser:
        if _is_ajax(request):
            return _json_error('Only superusers may delete a superuser account.')
        messages.error(request, 'Only superusers may delete a superuser account.')
        return redirect('core:users')
    target.delete()
    if _is_ajax(request):
        return JsonResponse({'success': True, 'message': 'User deleted successfully.'})
    messages.success(request, 'User deleted successfully.')
    return redirect('core:users')


def _driver_account_type_display(profile):
    if not profile:
        return '—'
    if profile.account_type == TransporterProfile.AccountType.TRANSPORTER_DRIVER:
        return 'Fleet driver'
    return profile.get_account_type_display()


def _driver_company_or_transporter_display(user):
    profile = getattr(user, 'transporter_profile', None)
    if not profile:
        return '—'
    if profile.account_type == TransporterProfile.AccountType.DRIVER:
        company = profile.company_name or '—'
        return f'Company: {company}'
    if profile.account_type == TransporterProfile.AccountType.TRANSPORTER_DRIVER:
        link = next((row for row in user.fleet_driver_links.all() if row.is_active), None)
        if not link:
            return '—'
        transporter = link.transporter
        name = f'{transporter.first_name} {transporter.last_name}'.strip()
        if not name:
            owner_profile = getattr(transporter, 'transporter_profile', None)
            if owner_profile and owner_profile.company_name:
                name = owner_profile.company_name
            else:
                name = transporter.email
        return f'Transporter: {name}'
    company = profile.company_name or '—'
    return f'Company: {company}'


def _drivers_list_queryset():
    return (
        User.objects.filter(
            role__role=UserRole.Role.TRANSPORTER,
            transporter_profile__account_type__in=_DRIVER_ACCOUNT_TYPES,
        )
        .select_related('role', 'transporter_profile')
        .prefetch_related(
            Prefetch(
                'fleet_driver_links',
                queryset=TransporterDriverLink.objects.filter(is_active=True).select_related(
                    'transporter',
                    'transporter__transporter_profile',
                ),
            ),
        )
    )


def _driver_list_row_data(user):
    profile = getattr(user, 'transporter_profile', None)
    name = f'{user.first_name} {user.last_name}'.strip()
    return {
        'id': user.id,
        'name': name,
        'company_or_transporter_display': _driver_company_or_transporter_display(user),
        'email': user.email or '',
        'phone': getattr(getattr(user, 'role', None), 'phone', '') or '—',
        'account_type_display': _driver_account_type_display(profile),
        'documents_verified': bool(profile.documents_verified) if profile else False,
        'joined': user.date_joined.strftime('%Y-%m-%d') if user.date_joined else '',
    }


@admin_required
def admin_drivers(request):
    base_qs = _drivers_list_queryset()
    if is_datatables_request(request):
        return paginate_datatables_queryset(
            request,
            base_qs,
            _driver_list_row_data,
            search_fields=[
                'first_name',
                'last_name',
                'email',
                'transporter_profile__company_name',
                'role__phone',
            ],
            order_fields=[
                'first_name',
                'transporter_profile__company_name',
                'email',
                'role__phone',
                'transporter_profile__account_type',
                'transporter_profile__documents_verified',
                'date_joined',
                None,
            ],
            default_order=['-date_joined'],
        )
    return render(request, 'core/drivers.html')


@admin_required
@require_http_methods(['GET'])
def admin_driver_modal(request, user_id):
    u = get_object_or_404(
        _drivers_list_queryset()
        .prefetch_related(
            Prefetch(
                'kyc_documents',
                queryset=_person_kyc_documents_queryset(),
            ),
        ),
        pk=user_id,
    )
    profile = u.transporter_profile
    is_individual_driver = (
        profile is not None and profile.account_type == TransporterProfile.AccountType.DRIVER
    )
    documents = _uploaded_kyc_documents(u.kyc_documents.all())
    return render(
        request,
        'core/includes/driver_modal.html',
        {'u': u, 'is_individual_driver': is_individual_driver, 'documents': documents},
    )


@admin_required
def admin_zones(request):
    if is_datatables_request(request):
        return paginate_datatables_queryset(
            request,
            Zone.objects.all(),
            _zone_row_data,
            search_fields=['country_code'],
            order_fields=['country_code', 'radius_km', 'rate_per_km', 'allow_return_trips', 'created_at', None],
            default_order=['country_code'],
        )
    if request.GET.get('format') == 'json':
        zones = Zone.objects.all()
        return JsonResponse({
            'data': [_zone_row_data(zone) for zone in zones],
        })
    return render(request, 'core/zones.html', {
        'country_choices': COUNTRY_CHOICES,
    })


@admin_required
@require_POST
def admin_zone_create(request):
    country_code = normalize_country_code(request.POST.get('country_code'))
    if not country_code:
        message = 'Country is required.'
        if _is_ajax(request):
            return _json_error(message)
        messages.error(request, message)
        return redirect('core:zones')
    if not is_valid_country_code(country_code):
        message = 'Select a valid country.'
        if _is_ajax(request):
            return _json_error(message)
        messages.error(request, message)
        return redirect('core:zones')
    if Zone.objects.filter(country_code=country_code).exists():
        message = f'Zone for {country_code} already exists.'
        if _is_ajax(request):
            return _json_error(message)
        messages.error(request, message)
        return redirect('core:zones')
    radius_km, radius_error = _parse_zone_radius_km(request.POST.get('radius_km'))
    if radius_error:
        if _is_ajax(request):
            return _json_error(radius_error)
        messages.error(request, radius_error)
        return redirect('core:zones')
    rate_per_km, rate_error = _parse_zone_rate_per_km(request.POST.get('rate_per_km'))
    if rate_error:
        if _is_ajax(request):
            return _json_error(rate_error)
        messages.error(request, rate_error)
        return redirect('core:zones')
    try:
        zone = Zone.objects.create(
            country_code=country_code,
            radius_km=radius_km,
            rate_per_km=rate_per_km,
            allow_return_trips=request.POST.get('allow_return_trips') == 'on',
        )
        message = f'Zone for {zone.country_display} created.'
        if _is_ajax(request):
            return JsonResponse({'success': True, 'message': message, 'zone': _zone_row_data(zone)})
        messages.success(request, message)
    except Exception:
        message = 'Could not create zone.'
        if _is_ajax(request):
            return _json_error(message, status=500)
        messages.error(request, message)
    return redirect('core:zones')


@admin_required
@require_POST
def admin_zone_edit(request, pk):
    zone = get_object_or_404(Zone, pk=pk)
    country_code = normalize_country_code(request.POST.get('country_code'))
    if not country_code:
        message = 'Country is required.'
        if _is_ajax(request):
            return _json_error(message)
        messages.error(request, message)
        return redirect('core:zones')
    if not is_valid_country_code(country_code):
        message = 'Select a valid country.'
        if _is_ajax(request):
            return _json_error(message)
        messages.error(request, message)
        return redirect('core:zones')
    if Zone.objects.filter(country_code=country_code).exclude(pk=zone.pk).exists():
        message = f'Zone for {country_code} already exists.'
        if _is_ajax(request):
            return _json_error(message)
        messages.error(request, message)
        return redirect('core:zones')
    radius_km, radius_error = _parse_zone_radius_km(request.POST.get('radius_km'))
    if radius_error:
        if _is_ajax(request):
            return _json_error(radius_error)
        messages.error(request, radius_error)
        return redirect('core:zones')
    rate_per_km, rate_error = _parse_zone_rate_per_km(request.POST.get('rate_per_km'))
    if rate_error:
        if _is_ajax(request):
            return _json_error(rate_error)
        messages.error(request, rate_error)
        return redirect('core:zones')
    zone.country_code = country_code
    zone.radius_km = radius_km
    zone.rate_per_km = rate_per_km
    zone.allow_return_trips = request.POST.get('allow_return_trips') == 'on'
    try:
        zone.save()
        message = f'Zone for {zone.country_display} updated.'
        if _is_ajax(request):
            return JsonResponse({'success': True, 'message': message, 'zone': _zone_row_data(zone)})
        messages.success(request, message)
    except Exception:
        message = 'Could not update zone.'
        if _is_ajax(request):
            return _json_error(message, status=500)
        messages.error(request, message)
    return redirect('core:zones')


@admin_required
@require_POST
def admin_zone_delete(request, pk):
    zone = get_object_or_404(Zone, pk=pk)
    label = zone.country_display
    try:
        zone.delete()
        message = f'Zone for {label} deleted.'
        if _is_ajax(request):
            return JsonResponse({'success': True, 'message': message, 'id': pk})
        messages.success(request, message)
    except Exception:
        message = 'Could not delete zone.'
        if _is_ajax(request):
            return _json_error(message, status=500)
        messages.error(request, message)
    return redirect('core:zones')


FREIGHT_CURRENCY_CHOICES = currency_choices()


def _freight_route_row_data(route):
    return {
        'id': route.id,
        'origin_country_code': route.origin_country_code,
        'origin_country_name': country_name_for_code(route.origin_country_code),
        'destination_country_code': route.destination_country_code,
        'destination_country_name': country_name_for_code(route.destination_country_code),
        'min_freight': str(route.min_freight),
        'min_freight_display': f'{route.min_freight} {route.currency}',
        'currency': route.currency,
        'updated_at': route.updated_at.strftime('%Y-%m-%d %H:%M') if route.updated_at else '',
    }


def _parse_min_freight(raw):
    value = (raw or '').strip()
    if not value:
        return None, 'Minimum freight is required.'
    try:
        amount = Decimal(value)
    except Exception:
        return None, 'Minimum freight must be a valid number.'
    if amount < 0:
        return None, 'Minimum freight cannot be negative.'
    return amount, None


def _parse_currency(raw):
    """Kept for compatibility; prefer currency_code_for_country(origin)."""
    code = (raw or '').strip().upper()
    if not code:
        return None, None
    if len(code) != 3 or not code.isalpha():
        return None, 'Currency must be a 3-letter ISO code.'
    return code, None


def _country_codes_matching_search(search):
    needle = (search or '').strip()
    if not needle:
        return []
    codes = set()
    exact = country_name_to_code(needle)
    if exact:
        codes.add(exact)
    lowered = needle.lower()
    for code, name in COUNTRY_BY_CODE.items():
        if lowered in code.lower() or lowered in name.lower():
            codes.add(code)
    return list(codes)


def _freight_routes_datatables(request, qs):
    draw, start, length, search = parse_datatables_request(request)
    records_total = qs.count()
    if search:
        matched_codes = _country_codes_matching_search(search)
        name_q = Q()
        if matched_codes:
            name_q = (
                Q(origin_country_code__in=matched_codes)
                | Q(destination_country_code__in=matched_codes)
            )
        qs = qs.filter(
            Q(origin_country_code__icontains=search)
            | Q(destination_country_code__icontains=search)
            | name_q
        )
    records_filtered = qs.count()
    order_fields = [
        'origin_country_code',
        'destination_country_code',
        'min_freight',
        'currency',
        'updated_at',
        None,
    ]
    ordered_qs = qs.order_by(
        *apply_datatables_order(request, order_fields, ['origin_country_code', 'destination_country_code']),
    )
    page = ordered_qs[start:start + length]
    return datatables_response(
        draw,
        records_total,
        records_filtered,
        [_freight_route_row_data(item) for item in page],
    )


@admin_required
def admin_freight_routes(request):
    qs = FreightRoute.objects.all()
    if is_datatables_request(request):
        origin = normalize_country_code(request.GET.get('origin'))
        destination = normalize_country_code(request.GET.get('destination'))
        if origin:
            qs = qs.filter(origin_country_code=origin)
        if destination:
            qs = qs.filter(destination_country_code=destination)
        return _freight_routes_datatables(request, qs)
    return render(request, 'core/freight_routes.html', {
        'country_choices': COUNTRY_CHOICES,
        'currency_choices': FREIGHT_CURRENCY_CHOICES,
        'currency_by_country_json': json.dumps(currency_map_for_frontend()),
    })


@admin_required
@require_POST
def admin_freight_route_create(request):
    origin = normalize_country_code(request.POST.get('origin_country_code'))
    destination = normalize_country_code(request.POST.get('destination_country_code'))
    if not origin or not destination:
        message = 'Origin and destination are required.'
        return _json_error(message) if _is_ajax(request) else (
            messages.error(request, message) or redirect('core:freight_routes')
        )
    if not is_valid_country_code(origin) or not is_valid_country_code(destination):
        message = 'Select valid origin and destination countries.'
        return _json_error(message) if _is_ajax(request) else (
            messages.error(request, message) or redirect('core:freight_routes')
        )
    if origin == destination:
        message = 'Origin and destination must be different countries.'
        return _json_error(message) if _is_ajax(request) else (
            messages.error(request, message) or redirect('core:freight_routes')
        )
    if FreightRoute.objects.filter(
        origin_country_code=origin,
        destination_country_code=destination,
    ).exists():
        message = f'A route from {origin} to {destination} already exists.'
        return _json_error(message) if _is_ajax(request) else (
            messages.error(request, message) or redirect('core:freight_routes')
        )
    min_freight, freight_error = _parse_min_freight(request.POST.get('min_freight'))
    if freight_error:
        return _json_error(freight_error) if _is_ajax(request) else (
            messages.error(request, freight_error) or redirect('core:freight_routes')
        )
    currency = currency_code_for_country(origin)
    try:
        route = FreightRoute.objects.create(
            origin_country_code=origin,
            destination_country_code=destination,
            min_freight=min_freight,
            currency=currency,
        )
        message = f'Route {origin} → {destination} created.'
        if _is_ajax(request):
            return JsonResponse({
                'success': True,
                'message': message,
                'route': _freight_route_row_data(route),
            })
        messages.success(request, message)
    except Exception:
        message = 'Could not create freight route.'
        if _is_ajax(request):
            return _json_error(message, status=500)
        messages.error(request, message)
    return redirect('core:freight_routes')


@admin_required
@require_POST
def admin_freight_route_edit(request, pk):
    route = get_object_or_404(FreightRoute, pk=pk)
    origin = normalize_country_code(request.POST.get('origin_country_code'))
    destination = normalize_country_code(request.POST.get('destination_country_code'))
    if not origin or not destination:
        message = 'Origin and destination are required.'
        return _json_error(message) if _is_ajax(request) else (
            messages.error(request, message) or redirect('core:freight_routes')
        )
    if not is_valid_country_code(origin) or not is_valid_country_code(destination):
        message = 'Select valid origin and destination countries.'
        return _json_error(message) if _is_ajax(request) else (
            messages.error(request, message) or redirect('core:freight_routes')
        )
    if origin == destination:
        message = 'Origin and destination must be different countries.'
        return _json_error(message) if _is_ajax(request) else (
            messages.error(request, message) or redirect('core:freight_routes')
        )
    if (
        FreightRoute.objects
        .filter(origin_country_code=origin, destination_country_code=destination)
        .exclude(pk=route.pk)
        .exists()
    ):
        message = f'A route from {origin} to {destination} already exists.'
        return _json_error(message) if _is_ajax(request) else (
            messages.error(request, message) or redirect('core:freight_routes')
        )
    min_freight, freight_error = _parse_min_freight(request.POST.get('min_freight'))
    if freight_error:
        return _json_error(freight_error) if _is_ajax(request) else (
            messages.error(request, freight_error) or redirect('core:freight_routes')
        )
    currency = currency_code_for_country(origin)
    route.origin_country_code = origin
    route.destination_country_code = destination
    route.min_freight = min_freight
    route.currency = currency
    try:
        route.save()
        message = f'Route {origin} → {destination} updated.'
        if _is_ajax(request):
            return JsonResponse({
                'success': True,
                'message': message,
                'route': _freight_route_row_data(route),
            })
        messages.success(request, message)
    except Exception:
        message = 'Could not update freight route.'
        if _is_ajax(request):
            return _json_error(message, status=500)
        messages.error(request, message)
    return redirect('core:freight_routes')


@admin_required
@require_POST
def admin_freight_route_delete(request, pk):
    route = get_object_or_404(FreightRoute, pk=pk)
    label = f'{route.origin_country_code} → {route.destination_country_code}'
    try:
        route.delete()
        message = f'Route {label} deleted.'
        if _is_ajax(request):
            return JsonResponse({'success': True, 'message': message, 'id': pk})
        messages.success(request, message)
    except Exception:
        message = 'Could not delete freight route.'
        if _is_ajax(request):
            return _json_error(message, status=500)
        messages.error(request, message)
    return redirect('core:freight_routes')


@admin_required
@require_POST
def admin_freight_route_bulk_import(request):
    uploaded = request.FILES.get('file')
    if not uploaded:
        message = 'Choose a CSV or Excel file to import.'
        return _json_error(message) if _is_ajax(request) else (
            messages.error(request, message) or redirect('core:freight_routes')
        )
    try:
        rows = parse_freight_route_upload(uploaded)
        summary = bulk_upsert_freight_routes(rows)
    except ValueError as exc:
        message = str(exc)
        return _json_error(message) if _is_ajax(request) else (
            messages.error(request, message) or redirect('core:freight_routes')
        )
    except Exception:
        message = 'Could not import freight routes.'
        return _json_error(message, status=500) if _is_ajax(request) else (
            messages.error(request, message) or redirect('core:freight_routes')
        )

    message = (
        f"Import complete: {summary['created']} created, "
        f"{summary['updated']} updated, {len(summary['errors'])} errors."
    )
    if _is_ajax(request):
        return JsonResponse({'success': True, 'message': message, 'summary': summary})
    messages.success(request, message)
    return redirect('core:freight_routes')


@superuser_required
@require_http_methods(['GET', 'POST'])
def admin_settings(request):
    ps = PlatformSettings.load()
    form = PlatformSettingsForm(request.POST or None, instance=ps)
    if request.method == 'POST' and form.is_valid():
        form.save()
        from accounts.platform_config import apply_platform_settings_to_django

        apply_platform_settings_to_django()
        messages.success(request, 'Platform settings saved.')
        return redirect('core:settings')
    return render(request, 'core/settings.html', {'form': form})


@admin_required
def admin_shipments(request):
    latest_conv_subq = (
        Conversation.objects.filter(shipment_id=OuterRef('pk'))
        .order_by('-created_at')
        .values('id')[:1]
    )
    base_qs = (
        Shipment.objects.select_related('shipper', 'trip')
        .annotate(latest_conversation_id=Subquery(latest_conv_subq))
        .order_by('-id')
    )
    st = (request.GET.get('status') or '').strip()
    if st:
        base_qs = base_qs.filter(status=st)
    if is_datatables_request(request):
        return paginate_datatables_queryset(
            request,
            base_qs,
            _shipment_list_row_data,
            search_fields=[
                'unique_id',
                'shipper__email',
                'pickup_address',
                'delivery_address',
                'cargo_type',
                'status',
            ],
            order_fields=[
                'unique_id',
                'shipper__email',
                'pickup_address',
                'delivery_address',
                'cargo_type',
                'status',
                None,
            ],
            default_order=['-id'],
        )
    return render(
        request,
        'core/shipments.html',
        {
            'status_filter': st,
            'shipment_status_choices': Shipment.Status.choices,
        },
    )


@admin_required
@require_http_methods(['GET'])
def admin_shipment_modal(request, shipment_id):
    shipment = get_object_or_404(
        Shipment.objects.select_related('shipper', 'trip'),
        pk=shipment_id,
    )
    rate_requests = RateRequest.objects.filter(shipment=shipment).select_related('driver').order_by('batch_number', 'created_at')
    return render(
        request,
        'core/includes/shipment_modal.html',
        {
            'shipment': shipment,
            'rate_requests': rate_requests,
        },
    )


def _vehicle_document_has_file_q(prefix=''):
    field = f'{prefix}file' if prefix else 'file'
    return ~(Q(**{field: ''}) | Q(**{f'{field}__isnull': True}))


def _existing_vehicle_documents_qs():
    return VehicleDocument.objects.filter(_vehicle_document_has_file_q())


def _vehicle_documents_list_queryset():
    pending = VehicleDocument.ReviewStatus.PENDING
    rejected = VehicleDocument.ReviewStatus.REJECTED
    approved = VehicleDocument.ReviewStatus.APPROVED
    has_file = _vehicle_document_has_file_q('documents__')
    return Vehicle.objects.select_related(
        'owner',
        'owner__transporter_profile',
    ).annotate(
        document_count=Count('documents', filter=has_file, distinct=True),
        pending_count=Count(
            'documents',
            filter=has_file & Q(documents__review_status=pending),
            distinct=True,
        ),
        rejected_count=Count(
            'documents',
            filter=has_file & Q(documents__review_status=rejected),
            distinct=True,
        ),
        approved_count=Count(
            'documents',
            filter=has_file & Q(documents__review_status=approved),
            distinct=True,
        ),
        last_submitted_at=Max('documents__submitted_at', filter=has_file),
        last_reviewed_at=Max('documents__reviewed_at', filter=has_file),
    ).filter(document_count__gt=0)


def _vehicle_document_list_row_data(vehicle):
    pending = getattr(vehicle, 'pending_count', 0) or 0
    rejected = getattr(vehicle, 'rejected_count', 0) or 0
    approved = getattr(vehicle, 'approved_count', 0) or 0
    total = getattr(vehicle, 'document_count', 0) or 0
    last_submitted = getattr(vehicle, 'last_submitted_at', None)
    last_reviewed = getattr(vehicle, 'last_reviewed_at', None)
    owner = getattr(vehicle, 'owner', None)
    if pending:
        review_status = VehicleDocument.ReviewStatus.PENDING
        review_status_display = f'Pending ({pending})' if total > 1 else 'Pending'
    elif rejected:
        review_status = VehicleDocument.ReviewStatus.REJECTED
        review_status_display = f'Rejected ({rejected})' if total > 1 else 'Rejected'
    elif approved:
        review_status = VehicleDocument.ReviewStatus.APPROVED
        review_status_display = 'Approved'
    else:
        review_status = VehicleDocument.ReviewStatus.PENDING
        review_status_display = '—'
    return {
        'id': vehicle.id,
        'vehicle_id': vehicle.id,
        'registration_number': vehicle.registration_number or '',
        'owner_id': vehicle.owner_id,
        'owner_email': owner.email if owner else '',
        'ownership': _vehicle_ownership_label(vehicle),
        'document_count': total,
        'review_status': review_status,
        'review_status_display': review_status_display,
        'submitted_at': last_submitted.strftime('%Y-%m-%d %H:%M') if last_submitted else '',
        'reviewed_at': last_reviewed.strftime('%Y-%m-%d') if last_reviewed else '—',
    }


@admin_required
def admin_vehicle_documents(request):
    # Mirror legacy KYC vehicle registration onto VehicleDocument NOC so individual (and any)
    # owner registrations appear here — before both HTML and DataTables responses.
    from accounts.vehicle_docs import sync_missing_kyc_vehicle_registrations
    sync_missing_kyc_vehicle_registrations()

    base_qs = _vehicle_documents_list_queryset()
    if is_datatables_request(request):
        return paginate_datatables_queryset(
            request,
            base_qs,
            _vehicle_document_list_row_data,
            search_fields=[
                'registration_number',
                'owner__email',
            ],
            order_fields=[
                'registration_number',
                'owner__email',
                'owner__transporter_profile__account_type',
                'document_count',
                'pending_count',
                'last_submitted_at',
                'last_reviewed_at',
                None,
            ],
            default_order=['-last_submitted_at'],
        )

    return render(request, 'core/vehicle_documents.html')


@admin_required
@require_http_methods(['GET'])
def admin_vehicle_documents_modal(request, vehicle_id):
    vehicle = get_object_or_404(Vehicle.objects.select_related('owner'), pk=vehicle_id)
    documents = _uploaded_vehicle_documents(
        _existing_vehicle_documents_qs().filter(vehicle_id=vehicle_id).select_related(
            'reviewed_by',
        ).order_by('-submitted_at')
    )
    return render(
        request,
        'core/includes/vehicle_documents_modal.html',
        {
            'vehicle': vehicle,
            'documents': documents,
            'single_document': False,
            'focus_document_id': None,
        },
    )


@admin_required
@require_http_methods(['GET', 'POST'])
def admin_vehicle_document_review(request, pk):
    doc = get_object_or_404(VehicleDocument.objects.select_related('vehicle', 'vehicle__owner'), pk=pk)
    form = VehicleDocumentReviewWebForm(request.POST or None)
    if request.method == 'POST' and form.is_valid():
        cd = form.cleaned_data
        doc.review_status = cd['review_status']
        doc.review_notes = cd.get('review_notes') or ''
        doc.reviewed_at = timezone.now()
        doc.reviewed_by = request.user
        update_fields = ['review_status', 'review_notes', 'reviewed_at', 'reviewed_by']
        if cd.get('expiry_date'):
            doc.expiry_date = cd['expiry_date']
            update_fields.append('expiry_date')
        doc.save(update_fields=update_fields)
        if _is_ajax(request):
            return JsonResponse({
                'success': True,
                'message': 'Document review saved.',
                'document': _vehicle_document_ajax_data(doc),
            })
        messages.success(request, 'Document review saved.')
        return redirect('core:vehicle_documents')
    if request.method == 'POST' and _is_ajax(request):
        error_bits = []
        for field, errs in form.errors.items():
            for err in errs:
                error_bits.append(f'{field}: {err}' if field != '__all__' else str(err))
        return _json_error(
            '; '.join(error_bits) if error_bits else 'Validation failed.',
            form.errors.get_json_data(),
        )
    return render(request, 'core/vehicle_document_review.html', {'document': doc, 'form': form})


@admin_required
@require_POST
def admin_vehicle_documents_approve_all(request, vehicle_id):
    vehicle = get_object_or_404(Vehicle, pk=vehicle_id)
    now = timezone.now()
    docs = list(
        _existing_vehicle_documents_qs().filter(vehicle=vehicle).exclude(
            review_status=VehicleDocument.ReviewStatus.APPROVED,
        )
    )
    for doc in docs:
        doc.review_status = VehicleDocument.ReviewStatus.APPROVED
        doc.reviewed_at = now
        doc.reviewed_by = request.user
    if docs:
        VehicleDocument.objects.bulk_update(docs, ['review_status', 'reviewed_at', 'reviewed_by'])
    refreshed = _existing_vehicle_documents_qs().filter(vehicle=vehicle).order_by('-submitted_at')
    return JsonResponse({
        'success': True,
        'message': f'{len(docs)} document(s) approved.',
        'updated_count': len(docs),
        'documents': [_vehicle_document_ajax_data(doc) for doc in refreshed],
    })


@admin_required
def admin_vehicles(request):
    base_qs = Vehicle.objects.select_related(
        'owner',
        'owner__transporter_profile',
        'assigned_driver',
        'assigned_driver__transporter_profile',
        'verified_by',
    )
    if is_datatables_request(request):
        return paginate_datatables_queryset(
            request,
            base_qs,
            _vehicle_row_data,
            search_fields=[
                'registration_number',
                'vehicle_type',
                'owner__email',
                'assigned_driver__email',
                'assigned_driver__first_name',
                'assigned_driver__last_name',
            ],
            order_fields=[
                'registration_number',
                'vehicle_type',
                'owner__transporter_profile__account_type',
                'assigned_driver__first_name',
                'load_capacity',
                'is_verified',
                'is_active',
                'created_at',
                None,
            ],
            default_order=['-created_at'],
        )
    return render(request, 'core/vehicles.html')


@admin_required
@require_http_methods(['GET'])
def admin_vehicle_owner_modal(request, user_id):
    owner = get_object_or_404(
        User.objects.select_related('role', 'transporter_profile', 'shipper_profile'),
        pk=user_id,
    )
    transporter = getattr(owner, 'transporter_profile', None)
    shipper = getattr(owner, 'shipper_profile', None)
    profile = transporter or shipper
    ownership_label = _user_ownership_label(owner)
    if transporter is not None:
        account_type_display = transporter.get_account_type_display()
        documents_verified = bool(transporter.documents_verified)
        company_name = transporter.company_name
    elif shipper is not None:
        account_type_display = shipper.get_account_type_display()
        documents_verified = bool(shipper.kyc_verified)
        company_name = shipper.company_name
    else:
        account_type_display = '—'
        documents_verified = False
        company_name = ''

    full_name = f'{owner.first_name} {owner.last_name}'.strip()
    role = getattr(owner, 'role', None)
    return render(
        request,
        'core/includes/vehicle_owner_modal.html',
        {
            'owner': owner,
            'profile': profile,
            'owner_name': full_name or owner.email,
            'phone': role.phone if role else '',
            'ownership_label': ownership_label,
            'account_type_display': account_type_display,
            'documents_verified': documents_verified,
            'company_name': company_name,
        },
    )


@admin_required
@require_http_methods(['GET'])
def admin_vehicle_driver_modal(request, user_id):
    driver = get_object_or_404(
        User.objects.select_related('role', 'transporter_profile').prefetch_related(
            Prefetch(
                'fleet_driver_links',
                queryset=TransporterDriverLink.objects.filter(is_active=True).select_related(
                    'transporter',
                    'transporter__transporter_profile',
                ),
            ),
        ),
        pk=user_id,
    )
    profile = getattr(driver, 'transporter_profile', None)
    account_type_display = profile.get_account_type_display() if profile else '—'
    full_name = f'{driver.first_name} {driver.last_name}'.strip()
    role = getattr(driver, 'role', None)
    return render(
        request,
        'core/includes/vehicle_driver_modal.html',
        {
            'driver': driver,
            'profile': profile,
            'driver_name': full_name or driver.email,
            'phone': role.phone if role else '',
            'account_type_display': account_type_display,
            'company_or_transporter': _driver_company_or_transporter_display(driver),
        },
    )


def _update_vehicle_verification(vehicle, *, user, is_verified, is_active):
    vehicle.is_verified = is_verified
    vehicle.is_active = is_active
    if is_verified:
        vehicle.verified_at = timezone.now()
        vehicle.verified_by = user
    else:
        vehicle.verified_at = None
        vehicle.verified_by = None
    vehicle.save(
        update_fields=['is_verified', 'is_active', 'verified_at', 'verified_by', 'updated_at'],
    )


@admin_required
@require_http_methods(['GET'])
def admin_vehicle_verify(request, pk):
    """Legacy URL: verification now opens from the vehicles table modal."""
    return redirect('core:vehicles')


@admin_required
@require_http_methods(['GET', 'POST'])
def admin_vehicle_verify_modal(request, pk):
    vehicle = get_object_or_404(
        Vehicle.objects.select_related(
            'owner',
            'assigned_driver',
            'verified_by',
        ),
        pk=pk,
    )
    if request.method == 'POST':
        is_verified = request.POST.get('is_verified') == 'on'
        is_active = request.POST.get('is_active') == 'on'
        _update_vehicle_verification(
            vehicle,
            user=request.user,
            is_verified=is_verified,
            is_active=is_active,
        )
        if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
            return JsonResponse({
                'success': True,
                'message': 'Vehicle verification updated.',
                'vehicle': {
                    'id': vehicle.id,
                    'is_verified': vehicle.is_verified,
                    'is_active': vehicle.is_active,
                },
            })
        messages.success(request, 'Vehicle verification updated.')
        return redirect('core:vehicles')
    return render(request, 'core/includes/vehicle_verify_modal.html', {'vehicle': vehicle})


def _file_field_uploaded(field_file):
    if not field_file:
        return False
    name = (getattr(field_file, 'name', None) or '').strip()
    return bool(name)


def _uploaded_kyc_documents(docs):
    return [
        d for d in docs
        if _file_field_uploaded(getattr(d, 'file', None))
        or _file_field_uploaded(getattr(d, 'file_back', None))
    ]


def _uploaded_vehicle_documents(docs):
    return [d for d in docs if _file_field_uploaded(getattr(d, 'file', None))]


def _kyc_document_has_file_q(prefix=''):
    file_field = f'{prefix}file' if prefix else 'file'
    back_field = f'{prefix}file_back' if prefix else 'file_back'
    empty_front = Q(**{file_field: ''}) | Q(**{f'{file_field}__isnull': True})
    empty_back = Q(**{back_field: ''}) | Q(**{f'{back_field}__isnull': True})
    return ~(empty_front & empty_back)


def _person_kyc_documents_queryset():
    """KYC rows for people/company identity — not vehicle registration."""
    return KYCDocument.objects.exclude(
        document_type__in=KYCDocument.LEGACY_VEHICLE_KYC_TYPES,
    ).filter(_kyc_document_has_file_q()).order_by('-submitted_at')


def _kyc_person_docs_relation_q():
    return _kyc_document_has_file_q('kyc_documents__') & ~Q(
        kyc_documents__document_type__in=KYCDocument.LEGACY_VEHICLE_KYC_TYPES,
    )


def _kyc_documents_list_queryset(pending_only=False):
    person_docs = _kyc_person_docs_relation_q()
    pending = KYCDocument.ReviewStatus.PENDING
    rejected = KYCDocument.ReviewStatus.REJECTED
    approved = KYCDocument.ReviewStatus.APPROVED
    qs = User.objects.select_related(
        'transporter_profile',
        'shipper_profile',
    ).annotate(
        document_count=Count('kyc_documents', filter=person_docs, distinct=True),
        pending_count=Count(
            'kyc_documents',
            filter=person_docs & Q(kyc_documents__review_status=pending),
            distinct=True,
        ),
        rejected_count=Count(
            'kyc_documents',
            filter=person_docs & Q(kyc_documents__review_status=rejected),
            distinct=True,
        ),
        approved_count=Count(
            'kyc_documents',
            filter=person_docs & Q(kyc_documents__review_status=approved),
            distinct=True,
        ),
        last_submitted_at=Max('kyc_documents__submitted_at', filter=person_docs),
        last_reviewed_at=Max('kyc_documents__reviewed_at', filter=person_docs),
    ).filter(document_count__gt=0)
    if pending_only:
        qs = qs.filter(Q(pending_count__gt=0) | Q(rejected_count__gt=0))
    return qs


def _kyc_document_list_row_data(user):
    pending = getattr(user, 'pending_count', 0) or 0
    rejected = getattr(user, 'rejected_count', 0) or 0
    approved = getattr(user, 'approved_count', 0) or 0
    total = getattr(user, 'document_count', 0) or 0
    last_submitted = getattr(user, 'last_submitted_at', None)
    last_reviewed = getattr(user, 'last_reviewed_at', None)
    if pending:
        review_status = KYCDocument.ReviewStatus.PENDING
        verified_display = f'Pending ({pending})' if total > 1 else 'Pending'
    elif rejected:
        review_status = KYCDocument.ReviewStatus.REJECTED
        verified_display = f'Rejected ({rejected})' if total > 1 else 'Rejected'
    elif approved:
        review_status = KYCDocument.ReviewStatus.APPROVED
        verified_display = 'Approved'
    else:
        review_status = KYCDocument.ReviewStatus.PENDING
        verified_display = '—'
    return {
        'id': user.id,
        'user_id': user.id,
        'user_email': user.email or '',
        'ownership': _user_ownership_label(user),
        'document_count': total,
        'review_status': review_status,
        'verified_display': verified_display,
        'submitted_at': last_submitted.strftime('%Y-%m-%d') if last_submitted else '',
        'reviewed_at': last_reviewed.strftime('%Y-%m-%d') if last_reviewed else '—',
    }


@admin_required
def admin_kyc_documents(request):
    pending_only = request.GET.get('pending') == '1'
    base_qs = _kyc_documents_list_queryset(pending_only=pending_only)
    if is_datatables_request(request):
        return paginate_datatables_queryset(
            request,
            base_qs,
            _kyc_document_list_row_data,
            search_fields=[
                'email',
                'first_name',
                'last_name',
            ],
            order_fields=[
                'email',
                None,
                'document_count',
                'pending_count',
                'last_submitted_at',
                'last_reviewed_at',
                None,
            ],
            default_order=['-last_submitted_at'],
        )
    return render(
        request,
        'core/kyc_documents.html',
        {'pending_only': pending_only},
    )


@admin_required
@require_http_methods(['GET'])
def admin_kyc_documents_modal(request, user_id):
    user = get_object_or_404(User.objects.all(), pk=user_id)
    documents = _uploaded_kyc_documents(
        _person_kyc_documents_queryset().filter(user_id=user_id)
    )
    return render(
        request,
        'core/includes/kyc_documents_modal.html',
        {
            'user': user,
            'documents': documents,
            'single_document': False,
            'focus_document_id': None,
        },
    )


@admin_required
@require_POST
def admin_kyc_document_verify(request, pk):
    """Approve or reject a single KYC document (legacy URL name kept)."""
    from accounts.kyc_review import apply_kyc_review

    doc = get_object_or_404(KYCDocument, pk=pk)
    # Prefer new review_status payload; fall back to legacy verified=1/0.
    if 'review_status' in request.POST:
        form = KYCDocumentReviewWebForm(request.POST)
        if not form.is_valid():
            if _is_ajax(request):
                return _json_error('Validation failed.', form.errors.get_json_data())
            messages.error(request, 'Validation failed.')
            return redirect('core:kyc_documents')
        try:
            apply_kyc_review(
                doc,
                review_status=form.cleaned_data['review_status'],
                review_notes=form.cleaned_data.get('review_notes') or '',
                reviewer=request.user,
                notify=True,
            )
        except ValueError as exc:
            if _is_ajax(request):
                return _json_error(str(exc), {'review_notes': [str(exc)]})
            messages.error(request, str(exc))
            return redirect('core:kyc_documents')
    else:
        flag = request.POST.get('verified') == '1'
        apply_kyc_review(
            doc,
            review_status=(
                KYCDocument.ReviewStatus.APPROVED
                if flag
                else KYCDocument.ReviewStatus.PENDING
            ),
            review_notes='',
            reviewer=request.user if flag else None,
            notify=False,
        )

    if _is_ajax(request):
        return JsonResponse({
            'success': True,
            'message': 'KYC document updated.',
            'document': _kyc_document_ajax_data(doc),
        })
    messages.success(request, 'KYC document updated.')
    if request.GET.get('next') == 'shipper':
        return redirect('core:shipper')
    if request.GET.get('next') == 'transporter':
        return redirect('core:transporter')
    if request.GET.get('next') == 'drivers':
        return redirect('core:drivers')
    return redirect('core:kyc_documents')


@admin_required
@require_POST
def admin_kyc_user_verify_all(request, user_id):
    from accounts.kyc_review import apply_kyc_review

    user = get_object_or_404(User, pk=user_id)
    docs = list(
        _person_kyc_documents_queryset().filter(user=user).exclude(
            review_status=KYCDocument.ReviewStatus.APPROVED,
        )
    )
    for doc in docs:
        apply_kyc_review(
            doc,
            review_status=KYCDocument.ReviewStatus.APPROVED,
            review_notes='',
            reviewer=request.user,
            notify=False,
        )
    documents = _person_kyc_documents_queryset().filter(user=user)
    return JsonResponse({
        'success': True,
        'message': f'{len(docs)} document(s) approved.',
        'updated_count': len(docs),
        'documents': [_kyc_document_ajax_data(doc) for doc in documents],
    })


def _shipment_list_row_data(shipment):
    def truncate_words(text, count=8):
        words = (text or '').split()
        if len(words) <= count:
            return text or ''
        return ' '.join(words[:count]) + '…'

    return {
        'id': shipment.id,
        'shipment_id': shipment.unique_id or f'#{shipment.id}',
        'shipper_email': shipment.shipper.email if shipment.shipper_id else '',
        'pickup_address': truncate_words(shipment.pickup_address),
        'delivery_address': truncate_words(shipment.delivery_address),
        'cargo_type': shipment.cargo_type or '',
        'status': shipment.status,
        'status_display': shipment.get_status_display(),
        'conversation_id': shipment.latest_conversation_id,
    }


def _trips_list_queryset():
    return Trip.objects.select_related(
        'shipment',
        'shipment__shipper',
        'transporter',
        'transporter__transporter_profile',
        'assigned_driver',
        'pod',
    ).prefetch_related('pod__photos')


def _user_display_name(user):
    if not user:
        return '—'
    name = f'{user.first_name} {user.last_name}'.strip()
    return name or user.email or '—'


def _trip_list_row_data(trip):
    shipment_label = trip.shipment.unique_id or f'#{trip.shipment_id}'
    profile = getattr(trip.transporter, 'transporter_profile', None) if trip.transporter_id else None
    is_individual_driver = (
        profile is not None and profile.account_type == TransporterProfile.AccountType.DRIVER
    )

    if is_individual_driver:
        transporter_display = 'Individual Driver'
        driver_display = _user_display_name(trip.transporter)
    else:
        name = (
            f'{trip.transporter.first_name} {trip.transporter.last_name}'.strip()
            if trip.transporter_id
            else ''
        )
        if name:
            transporter_display = name
        elif profile and profile.company_name:
            transporter_display = profile.company_name
        else:
            transporter_display = _user_display_name(trip.transporter)
        driver_display = (
            _user_display_name(trip.assigned_driver) if trip.assigned_driver_id else '—'
        )

    return {
        'id': trip.id,
        'shipment_id': shipment_label,
        'shipper_email': trip.shipment.shipper.email if trip.shipment_id else '',
        'transporter_display': transporter_display,
        'transporter_is_individual': is_individual_driver,
        'driver_display': driver_display,
        'status': trip.status,
        'status_display': trip.get_status_display(),
        'updated_at': trip.updated_at.strftime('%Y-%m-%d %H:%M') if trip.updated_at else '',
    }


def _build_trip_row_context(trips):
    trips = list(trips)
    trip_ids = [trip.id for trip in trips]

    locations_by_trip = {trip_id: [] for trip_id in trip_ids}
    if trip_ids:
        for location in TripLocation.objects.filter(trip_id__in=trip_ids).order_by('-recorded_at'):
            bucket = locations_by_trip[location.trip_id]
            if len(bucket) < 50:
                bucket.append(location)

    actions_by_trip = {trip_id: [] for trip_id in trip_ids}
    if trip_ids:
        for action in (
            AdminTripAction.objects.filter(trip_id__in=trip_ids)
            .select_related('performed_by')
            .order_by('-created_at')
        ):
            bucket = actions_by_trip[action.trip_id]
            if len(bucket) < 20:
                bucket.append(action)

    trip_rows = []
    for trip in trips:
        locations = locations_by_trip.get(trip.id, [])
        trip_rows.append({
            'trip': trip,
            'recent_actions': actions_by_trip.get(trip.id, []),
            'locations': locations,
            'locations_map_data': [
                {
                    'lat': float(loc.lat),
                    'lon': float(loc.lon),
                    'recorded_at': loc.recorded_at.strftime('%Y-%m-%d %H:%M:%S'),
                }
                for loc in reversed(locations)
            ],
            'locations_script_id': f'trip-locations-data-{trip.id}',
            'override_form': TripStatusOverrideWebForm(prefix=f'o{trip.id}'),
        })
    return trip_rows


@admin_required
def admin_trips(request):
    base_qs = _trips_list_queryset().order_by('-id')
    if is_datatables_request(request):
        return paginate_datatables_queryset(
            request,
            base_qs,
            _trip_list_row_data,
            search_fields=[
                'shipment__unique_id',
                'shipment__shipper__email',
                'transporter__email',
                'transporter__first_name',
                'transporter__last_name',
                'transporter__transporter_profile__company_name',
                'assigned_driver__email',
                'assigned_driver__first_name',
                'assigned_driver__last_name',
                'status',
            ],
            order_fields=[
                'shipment__unique_id',
                'shipment__shipper__email',
                'transporter__transporter_profile__company_name',
                'assigned_driver__first_name',
                'status',
                'updated_at',
                None,
            ],
            default_order=['-id'],
        )
    return render(request, 'core/trips.html')


@admin_required
@require_http_methods(['GET'])
def admin_trip_modals(request, trip_id):
    trip = get_object_or_404(_trips_list_queryset(), pk=trip_id)
    rows = _build_trip_row_context([trip])
    return render(request, 'core/includes/trip_modals.html', {'row': rows[0]})


def public_trip_tracking(request, trip_id):
    trip = get_object_or_404(
        Trip.objects.select_related(
            'shipment',
            'shipment__shipper',
            'transporter',
            'assigned_driver',
            'assigned_driver__role',
            'assigned_driver__transporter_profile',
        ),
        pk=trip_id,
    )
    shipment = trip.shipment
    # Public tracking always identifies the driver via Trip.assigned_driver (not transporter).
    driver = trip.assigned_driver
    profile = getattr(driver, 'transporter_profile', None) if driver else None
    tc_id = str(profile.tc_id).strip() if profile and profile.tc_id else None

    driver_position = None
    traccar_error = None
    if not driver:
        traccar_error = 'No driver is assigned to this trip.'
    elif tc_id:
        try:
            pos = get_latest_device_position(tc_id)
            lat = pos.get('latitude')
            lon = pos.get('longitude')
            if lat is not None and lon is not None:
                lat_f, lon_f = float(lat), float(lon)
                driver_position = {
                    'lat': lat_f,
                    'lon': lon_f,
                    'address': reverse_geocode_address(lat_f, lon_f) or '',
                }
            else:
                traccar_error = 'Traccar returned invalid driver coordinates.'
        except Exception as exc:
            traccar_error = str(exc)
    else:
        traccar_error = 'Driver tracking is not configured (missing tc_id).'

    pickup = None
    if shipment.pickup_lat is not None and shipment.pickup_lon is not None:
        pickup_lat = float(shipment.pickup_lat)
        pickup_lon = float(shipment.pickup_lon)
        pickup_address = (shipment.pickup_address or '').strip()
        if not pickup_address:
            pickup_address = reverse_geocode_address(pickup_lat, pickup_lon) or ''
        pickup = {
            'lat': pickup_lat,
            'lon': pickup_lon,
            'address': pickup_address,
        }
    destination = None
    if shipment.delivery_lat is not None and shipment.delivery_lon is not None:
        dest_lat = float(shipment.delivery_lat)
        dest_lon = float(shipment.delivery_lon)
        dest_address = (shipment.delivery_address or '').strip()
        if not dest_address:
            dest_address = reverse_geocode_address(dest_lat, dest_lon) or ''
        destination = {
            'lat': dest_lat,
            'lon': dest_lon,
            'address': dest_address,
        }

    status = trip.status
    tracking_mode = 'driver_to_destination'
    route_legs = []
    if driver_position and destination:
        if status in PRE_PICKUP_STATUSES and pickup:
            tracking_mode = 'driver_to_pickup_to_destination'
            route_legs = [
                {'from': driver_position, 'to': pickup, 'name': 'driver_to_pickup'},
                {'from': pickup, 'to': destination, 'name': 'pickup_to_destination'},
            ]
        else:
            route_legs = [
                {'from': driver_position, 'to': destination, 'name': 'driver_to_destination'},
            ]

    base = (os.getenv('TRACCAR_URL', '') or '').strip().rstrip('/')
    traccar_device_url_hint = f'{base}/#/devices/{tc_id}' if base and tc_id else None
    if driver:
        driver_name = f'{driver.first_name} {driver.last_name}'.strip() or driver.email
        driver_info = {
            'id': driver.id,
            'name': driver_name,
            'email': driver.email,
            'phone': getattr(getattr(driver, 'role', None), 'phone', '') or '',
            'tc_id': tc_id,
            'traccar_device_url_hint': traccar_device_url_hint,
        }
    else:
        driver_info = {
            'id': None,
            'name': '—',
            'email': '—',
            'phone': '',
            'tc_id': None,
            'traccar_device_url_hint': None,
        }

    vehicle = resolve_trip_assigned_vehicle(trip)
    if vehicle:
        types = vehicle.type_list() if hasattr(vehicle, 'type_list') else []
        vehicle_info = {
            'id': vehicle.id,
            'registration_number': vehicle.registration_number,
            'vehicle_type': vehicle.vehicle_type or (types[0] if types else ''),
            'vehicle_types': types,
            'load_capacity': vehicle.load_capacity,
            'max_length_m': vehicle.max_length_m,
            'max_width_m': vehicle.max_width_m,
            'max_height_m': vehicle.max_height_m,
            'special_features': (vehicle.special_features or '').strip(),
            'avatar_url': (vehicle.avatar_url or '').strip(),
            'is_verified': vehicle.is_verified,
            'is_active': vehicle.is_active,
        }
    else:
        vehicle_info = None

    context = {
        'trip': trip,
        'shipment': shipment,
        'driver_info': driver_info,
        'vehicle_info': vehicle_info,
        'status': status,
        'driver_position': driver_position,
        'pickup': pickup,
        'destination': destination,
        'tracking_mode': tracking_mode,
        'traccar_error': traccar_error,
        'driver_json': json.dumps(driver_position) if driver_position else 'null',
        'pickup_json': json.dumps(pickup) if pickup else 'null',
        'destination_json': json.dumps(destination) if destination else 'null',
        'route_legs_json': json.dumps(route_legs),
    }
    return render(request, 'core/public_trip_tracking.html', context)


@admin_required
def admin_trip_detail(request, trip_id):
    trip = get_object_or_404(
        Trip.objects.select_related('shipment', 'shipment__shipper', 'transporter', 'accepted_bid'),
        pk=trip_id,
    )
    has_pod = ProofOfDelivery.objects.filter(trip=trip).exists()
    recent_actions = AdminTripAction.objects.filter(trip=trip).select_related('performed_by').order_by('-created_at')[:20]
    return render(
        request,
        'core/trip_detail.html',
        {'trip': trip, 'has_pod': has_pod, 'recent_actions': recent_actions},
    )


@admin_required
def admin_trip_locations(request, trip_id):
    trip = get_object_or_404(Trip, pk=trip_id)
    qs = TripLocation.objects.filter(trip=trip).order_by('-recorded_at')
    paginator = Paginator(qs, 50)
    page = paginator.get_page(request.GET.get('page') or 1)
    return render(request, 'core/trip_locations.html', {'trip': trip, 'page_obj': page})


@admin_required
def admin_trip_pod(request, trip_id):
    trip = get_object_or_404(Trip, pk=trip_id)
    pod = get_object_or_404(ProofOfDelivery.objects.prefetch_related('photos'), trip=trip)
    return render(request, 'core/trip_pod.html', {'trip': trip, 'pod': pod})


@admin_required
@require_http_methods(['GET', 'POST'])
def admin_trip_status_override(request, trip_id):
    trip = get_object_or_404(Trip.objects.select_related('shipment'), pk=trip_id)
    form = TripStatusOverrideWebForm(request.POST or None)
    if request.method == 'POST' and form.is_valid():
        new_status = form.cleaned_data['new_status']
        justification = form.cleaned_data['justification'].strip()
        prev = trip.status
        if prev == new_status:
            messages.error(request, 'Trip is already in this status.')
        else:
            with transaction.atomic():
                trip.status = new_status
                trip.save(update_fields=['status', 'updated_at'])
                _sync_shipment_after_trip_override(trip, new_status)
                AdminTripAction.objects.create(
                    trip=trip,
                    performed_by=request.user,
                    previous_status=prev,
                    new_status=new_status,
                    justification=justification,
                )
            notify_users(
                [trip.shipment.shipper_id, trip.transporter_id],
                'TRIP_STATUS',
                {
                    'trip_id': str(trip.id),
                    'shipment_id': str(trip.shipment_id),
                    'status': str(new_status),
                    'admin_override': True,
                },
                title='Trip update (admin)',
                body=f'Trip status set to {new_status}',
            )
            messages.success(request, 'Trip status updated and shipment synced where applicable.')
            if request.GET.get('next') == 'trips':
                return redirect('core:trips')
            return redirect('core:trip_detail', trip_id=trip.id)
    return render(request, 'core/trip_override.html', {'trip': trip, 'form': form})


@admin_required
def admin_pricing(request):
    return render(request, 'core/admin/section_page.html', {'page_title': 'Pricing'})


@admin_required
def admin_financial(request):
    captured = Payment.objects.filter(status=Payment.Status.CAPTURED)
    agg = captured.aggregate(total=Sum('amount'))
    total = agg['total']
    total_str = str(total) if total is not None else '0'
    return render(
        request,
        'core/admin/section_page.html',
        {
            'page_title': 'Financial management',
            'extra_note': f'Captured payments: {captured.count()} records, total amount {total_str}.',
        },
    )


@admin_required
def admin_invoices_accounting(request):
    return render(request, 'core/admin/section_page.html', {'page_title': 'Invoice & accounting'})


@admin_required
def admin_tickets(request):
    return render(request, 'core/admin/section_page.html', {'page_title': 'Ticket management'})


# ---- Admin: Driver–Shipper Communications Mirroring & Dispute Logs ----

def _conversations_list_queryset():
    return (
        Conversation.objects.select_related(
            'shipment',
            'shipment__shipper',
            'shipper',
            'shipper__role',
            'shipper__shipper_profile',
            'transporter',
            'transporter__role',
            'transporter__transporter_profile',
            'trip',
            'trip__shipment',
            'trip__assigned_driver',
            'trip__assigned_driver__role',
            'trip__assigned_driver__transporter_profile',
        )
        .prefetch_related(
            'messages',
            'messages__sender',
            'messages__sender__role',
        )
    )


def _conversation_sender_role_label(user, conversation):
    """Determine sender display role within the specific conversation context."""
    if not user:
        return 'System'
    if conversation.shipper_id and user.id == conversation.shipper_id:
        return 'Shipper'
    if conversation.trip_id and conversation.trip.assigned_driver_id and user.id == conversation.trip.assigned_driver_id:
        return 'Driver'
    if conversation.transporter_id and user.id == conversation.transporter_id:
        tp = getattr(user, 'transporter_profile', None)
        if tp and tp.account_type == TransporterProfile.AccountType.DRIVER:
            return 'Driver'
        return 'Transporter'
    # Fallback to UserRole
    role_obj = getattr(user, 'role', None)
    if role_obj:
        return role_obj.get_role_display() if hasattr(role_obj, 'get_role_display') else role_obj.role
    return 'User'


def _conversation_list_row_data(c):
    shipment_label = c.shipment.unique_id or f'#{c.shipment_id}' if c.shipment_id else '—'
    pickup = c.shipment.pickup_address if c.shipment_id else ''
    delivery = c.shipment.delivery_address if c.shipment_id else ''
    cargo = c.shipment.cargo_type if c.shipment_id else '—'

    shipper_display = _user_display_name(c.shipper)
    transporter_display = _user_display_name(c.transporter)
    assigned_driver_display = _user_display_name(c.trip.assigned_driver) if (c.trip_id and c.trip.assigned_driver_id) else None

    # Compute message statistics
    msgs = list(c.messages.all())
    messages_count = len(msgs)
    last_msg = msgs[-1] if msgs else None

    last_message_preview = '—'
    last_message_time = '—'
    last_message_type = 'TEXT'
    if last_msg:
        last_message_type = last_msg.message_type
        if last_msg.message_type == Message.MessageType.VOICE:
            last_message_preview = '🎤 Voice recording'
        else:
            text = (last_msg.text or '').strip()
            last_message_preview = text[:80] + ('…' if len(text) > 80 else '') if text else 'Empty message'
        last_message_time = last_msg.created_at.strftime('%Y-%m-%d %H:%M')

    trip_status = c.trip.status if c.trip_id else 'PRE_TRIP'
    trip_status_display = c.trip.get_status_display() if c.trip_id else 'Pre-Trip / Bidding'

    return {
        'id': c.id,
        'shipment_id': c.shipment_id,
        'shipment_label': shipment_label,
        'pickup': pickup,
        'delivery': delivery,
        'cargo': cargo,
        'shipper_id': c.shipper_id,
        'shipper_email': c.shipper.email if c.shipper else '',
        'shipper_display': shipper_display,
        'transporter_id': c.transporter_id,
        'transporter_email': c.transporter.email if c.transporter else '',
        'transporter_display': transporter_display,
        'assigned_driver_display': assigned_driver_display,
        'trip_id': c.trip_id,
        'trip_status': trip_status,
        'trip_status_display': trip_status_display,
        'messages_count': messages_count,
        'last_message_preview': last_message_preview,
        'last_message_type': last_message_type,
        'last_message_time': last_message_time,
        'created_at': c.created_at.strftime('%Y-%m-%d %H:%M'),
    }


@admin_required
def admin_conversations(request):
    base_qs = _conversations_list_queryset().order_by('-created_at')

    status_filter = (request.GET.get('status') or '').strip()
    if status_filter:
        if status_filter == 'PRE_TRIP':
            base_qs = base_qs.filter(trip__isnull=True)
        else:
            base_qs = base_qs.filter(trip__status=status_filter)

    shipment_filter = (request.GET.get('shipment_id') or '').strip()
    if shipment_filter:
        try:
            base_qs = base_qs.filter(
                Q(shipment_id=int(shipment_filter)) | Q(shipment__unique_id__iexact=shipment_filter)
            )
        except (ValueError, TypeError):
            base_qs = base_qs.filter(shipment__unique_id__iexact=shipment_filter)

    if is_datatables_request(request):
        return paginate_datatables_queryset(
            request,
            base_qs,
            _conversation_list_row_data,
            search_fields=[
                'shipment__unique_id',
                'shipment__pickup_address',
                'shipment__delivery_address',
                'shipment__cargo_type',
                'shipper__email',
                'shipper__first_name',
                'shipper__last_name',
                'transporter__email',
                'transporter__first_name',
                'transporter__last_name',
                'transporter__transporter_profile__company_name',
                'trip__assigned_driver__email',
                'trip__assigned_driver__first_name',
                'trip__assigned_driver__last_name',
                'messages__text',
            ],
            order_fields=[
                'id',
                'shipment__unique_id',
                'shipper__email',
                'transporter__email',
                'trip__status',
                'created_at',
                None,
            ],
            default_order=['-created_at'],
        )

    trip_status_choices = [
        ('PRE_TRIP', 'Pre-Trip / Bidding'),
        *Trip.Status.choices,
    ]
    return render(
        request,
        'core/conversations.html',
        {
            'trip_status_choices': trip_status_choices,
            'status_filter': status_filter,
            'total_conversations': Conversation.objects.count(),
            'total_messages': Message.objects.count(),
        },
    )


@admin_required
@require_http_methods(['GET'])
def admin_conversation_modal(request, conversation_id):
    conv = get_object_or_404(_conversations_list_queryset(), pk=conversation_id)
    messages_qs = conv.messages.select_related(
        'sender',
        'sender__role',
        'sender__shipper_profile',
        'sender__transporter_profile',
    ).order_by('created_at')

    message_items = []
    for m in messages_qs:
        role_label = _conversation_sender_role_label(m.sender, conv)
        is_shipper = (conv.shipper_id and m.sender_id == conv.shipper_id)
        voice_url = m.voice_file.url if m.voice_file else None
        message_items.append({
            'id': m.id,
            'sender_id': m.sender_id,
            'sender_name': _user_display_name(m.sender),
            'sender_email': m.sender.email if m.sender else '',
            'sender_role': role_label,
            'is_shipper': is_shipper,
            'message_type': m.message_type,
            'text': m.text,
            'voice_url': voice_url,
            'created_at': m.created_at,
            'created_at_display': m.created_at.strftime('%Y-%m-%d %H:%M:%S'),
        })

    return render(
        request,
        'core/includes/conversation_modal.html',
        {
            'conversation': conv,
            'shipment': conv.shipment,
            'trip': conv.trip,
            'shipper': conv.shipper,
            'transporter': conv.transporter,
            'assigned_driver': conv.trip.assigned_driver if (conv.trip and conv.trip.assigned_driver_id) else None,
            'chat_messages': message_items,
            'messages_count': len(message_items),
        },
    )


@admin_required
@require_http_methods(['GET'])
def admin_conversation_sync(request, conversation_id):
    conv = get_object_or_404(_conversations_list_queryset(), pk=conversation_id)
    after_id = request.GET.get('after_id')
    messages_qs = conv.messages.select_related(
        'sender',
        'sender__role',
        'sender__shipper_profile',
        'sender__transporter_profile',
    ).order_by('created_at')

    if after_id:
        try:
            messages_qs = messages_qs.filter(id__gt=int(after_id))
        except (ValueError, TypeError):
            pass

    items = []
    for m in messages_qs:
        role_label = _conversation_sender_role_label(m.sender, conv)
        is_shipper = (conv.shipper_id and m.sender_id == conv.shipper_id)
        voice_url = m.voice_file.url if m.voice_file else None
        items.append({
            'id': m.id,
            'sender_id': m.sender_id,
            'sender_name': _user_display_name(m.sender),
            'sender_email': m.sender.email if m.sender else '',
            'sender_role': role_label,
            'is_shipper': is_shipper,
            'message_type': m.message_type,
            'text': m.text or '',
            'voice_url': voice_url,
            'created_at_display': m.created_at.strftime('%Y-%m-%d %H:%M:%S'),
        })

    return JsonResponse({
        'status_code': 200,
        'conversation_id': conv.id,
        'new_messages': items,
        'total_new': len(items),
    })


@admin_required
def admin_conversation_detail(request, conversation_id):
    conv = get_object_or_404(_conversations_list_queryset(), pk=conversation_id)
    messages_qs = conv.messages.select_related(
        'sender',
        'sender__role',
        'sender__shipper_profile',
        'sender__transporter_profile',
    ).order_by('created_at')

    message_items = []
    for m in messages_qs:
        role_label = _conversation_sender_role_label(m.sender, conv)
        is_shipper = (conv.shipper_id and m.sender_id == conv.shipper_id)
        voice_url = m.voice_file.url if m.voice_file else None
        message_items.append({
            'id': m.id,
            'sender_id': m.sender_id,
            'sender_name': _user_display_name(m.sender),
            'sender_email': m.sender.email if m.sender else '',
            'sender_role': role_label,
            'is_shipper': is_shipper,
            'message_type': m.message_type,
            'text': m.text,
            'voice_url': voice_url,
            'created_at': m.created_at,
            'created_at_display': m.created_at.strftime('%Y-%m-%d %H:%M:%S UTC'),
        })

    # Try to find relevant bid
    bid = None
    if conv.trip_id and conv.trip.accepted_bid:
        bid = conv.trip.accepted_bid
    elif conv.shipment_id and conv.transporter_id:
        bid = Bid.objects.filter(shipment_id=conv.shipment_id, transporter_id=conv.transporter_id).order_by('-created_at').first()

    return render(
        request,
        'core/conversation_detail.html',
        {
            'conversation': conv,
            'shipment': conv.shipment,
            'trip': conv.trip,
            'bid': bid,
            'shipper': conv.shipper,
            'transporter': conv.transporter,
            'assigned_driver': conv.trip.assigned_driver if (conv.trip and conv.trip.assigned_driver_id) else None,
            'chat_messages': message_items,
            'messages_count': len(message_items),
            'generated_at': timezone.now().strftime('%Y-%m-%d %H:%M:%S UTC'),
        },
    )


@admin_required
def admin_conversation_export(request, conversation_id):
    conv = get_object_or_404(_conversations_list_queryset(), pk=conversation_id)
    messages_qs = conv.messages.select_related(
        'sender',
        'sender__role',
        'sender__shipper_profile',
        'sender__transporter_profile',
    ).order_by('created_at')

    fmt = (request.GET.get('format') or 'txt').lower().strip()
    generated_at_str = timezone.now().strftime('%Y-%m-%d %H:%M:%S UTC')

    # Build messages structure
    msg_list = []
    for idx, m in enumerate(messages_qs, start=1):
        role_label = _conversation_sender_role_label(m.sender, conv)
        voice_url = request.build_absolute_uri(m.voice_file.url) if (m.voice_file and hasattr(m.voice_file, 'url')) else None
        msg_list.append({
            'index': idx,
            'id': m.id,
            'timestamp': m.created_at.strftime('%Y-%m-%d %H:%M:%S UTC'),
            'sender_id': m.sender_id,
            'sender_name': _user_display_name(m.sender),
            'sender_email': m.sender.email if m.sender else '',
            'sender_role': role_label,
            'type': m.message_type,
            'text': m.text or '',
            'voice_url': voice_url,
        })

    if fmt == 'json':
        tp = getattr(conv.transporter, 'transporter_profile', None) if conv.transporter else None
        transporter_company = tp.company_name if (tp and tp.company_name) else None
        payload = {
            'dispute_evidence_case': f'CONV-{conv.id}',
            'generated_at': generated_at_str,
            'conversation_id': conv.id,
            'created_at': conv.created_at.strftime('%Y-%m-%d %H:%M:%S UTC'),
            'shipment': {
                'id': conv.shipment_id,
                'unique_id': conv.shipment.unique_id if conv.shipment else None,
                'cargo_type': conv.shipment.cargo_type if conv.shipment else None,
                'pickup_address': conv.shipment.pickup_address if conv.shipment else None,
                'delivery_address': conv.shipment.delivery_address if conv.shipment else None,
                'status': conv.shipment.status if conv.shipment else None,
            } if conv.shipment else None,
            'trip': {
                'id': conv.trip_id,
                'status': conv.trip.status if conv.trip else None,
            } if conv.trip else None,
            'shipper': {
                'id': conv.shipper_id,
                'email': conv.shipper.email if conv.shipper else '',
                'name': _user_display_name(conv.shipper),
            } if conv.shipper else None,
            'transporter': {
                'id': conv.transporter_id,
                'email': conv.transporter.email if conv.transporter else '',
                'name': _user_display_name(conv.transporter),
                'company_name': transporter_company,
            } if conv.transporter else None,
            'assigned_driver': {
                'id': conv.trip.assigned_driver_id,
                'email': conv.trip.assigned_driver.email,
                'name': _user_display_name(conv.trip.assigned_driver),
            } if (conv.trip and conv.trip.assigned_driver_id) else None,
            'total_messages': len(msg_list),
            'messages': msg_list,
        }
        resp = JsonResponse(payload, json_dumps_params={'indent': 2})
        resp['Content-Disposition'] = f'attachment; filename="dispute_transcript_conv_{conv.id}.json"'
        return resp

    # Text transcript format
    shipment_unique = conv.shipment.unique_id if (conv.shipment and conv.shipment.unique_id) else f'#{conv.shipment_id}'
    pickup = conv.shipment.pickup_address if conv.shipment else '—'
    delivery = conv.shipment.delivery_address if conv.shipment else '—'
    cargo = conv.shipment.cargo_type if conv.shipment else '—'
    weight = conv.shipment.weight if conv.shipment else '—'
    trip_status_lbl = conv.trip.get_status_display() if conv.trip else 'Pre-Trip / Bidding'
    shipper_lbl = f"{_user_display_name(conv.shipper)} ({conv.shipper.email})" if conv.shipper else '—'
    tp = getattr(conv.transporter, 'transporter_profile', None) if conv.transporter else None
    transporter_comp_str = f" [{tp.company_name}]" if (tp and tp.company_name) else ""
    transporter_lbl = f"{_user_display_name(conv.transporter)}{transporter_comp_str} ({conv.transporter.email})" if conv.transporter else '—'
    driver_lbl = f"{_user_display_name(conv.trip.assigned_driver)} ({conv.trip.assigned_driver.email})" if (conv.trip and conv.trip.assigned_driver) else 'None assigned'

    lines = [
        "=" * 80,
        "TRUCK ME - DISPUTE COMMUNICATION AUDIT LOG & TRANSCRIPT",
        "=" * 80,
        f"Case / Thread ID : #{conv.id}",
        f"Shipment         : {shipment_unique} (Cargo: {cargo}, Weight: {weight})",
        f"Route            : {pickup} -> {delivery}",
        f"Trip Lifecycle   : {trip_status_lbl} (Trip ID: #{conv.trip_id or 'N/A'})",
        f"Shipper          : {shipper_lbl}",
        f"Transporter      : {transporter_lbl}",
        f"Assigned Driver  : {driver_lbl}",
        f"Export Date      : {generated_at_str}",
        f"Total Messages   : {len(msg_list)}",
        "=" * 80,
        "CHRONOLOGICAL MESSAGES TIMELINE",
        "=" * 80,
    ]

    for item in msg_list:
        lines.append(f"[{item['index']}] {item['timestamp']} | SENDER: {item['sender_name']} [{item['sender_role']}] <{item['sender_email']}>")
        lines.append(f"    TYPE: {item['type']}")
        if item['type'] == 'VOICE':
            lines.append(f"    AUDIO RECORDING: {item['voice_url'] or 'Voice note attached'}")
        if item['text']:
            lines.append(f"    MESSAGE: {item['text']}")
        lines.append("-" * 80)

    lines.append("=" * 80)
    lines.append("END OF TRANSCRIPT - OFFICIAL AUDIT RECORD")
    lines.append("=" * 80)

    content = "\n".join(lines)
    resp = HttpResponse(content, content_type='text/plain; charset=utf-8')
    resp['Content-Disposition'] = f'attachment; filename="dispute_transcript_conv_{conv.id}.txt"'
    return resp

