import base64
import uuid
try:
    import imghdr
except ImportError:
    imghdr = None
from decimal import Decimal
from django.db import transaction
from django.db.models import Q

from django.core.files.base import ContentFile
from django.contrib.auth import get_user_model
from rest_framework import serializers
from accounts.models import (
    UserRole,
    ShipperProfile,
    TransporterProfile,
    TransporterDriverLink,
    KYCDocument,
    Vehicle,
    VehicleDocument,
    PlatformSettings,
)
from core.load_type import validate_load_type_xor
from core.models import (
    Shipment,
    Bid,
    Trip,
    TripStatusRecord,
    TripLocation,
    Conversation,
    Message,
    ProofOfDelivery,
    PODPhoto,
    TripReview,
    TripShipperReview,
    SavedAddress,
    FavoriteTransporter,
    FreightRoute,
    RateRequest,
)
from core.countries import (
    country_name_for_code,
    is_valid_country_code,
    normalize_country_code,
)
from core.currencies import currency_code_for_country, currency_for_shipment
from billing.models import WalletAccount, LedgerEntry, Payment, Invoice, WithdrawalRequest
from billing.services import trip_payment_settlement_for_transporter
from .media_urls import build_absolute_media_url
from .traccar import create_traccar_device, generate_unique_tc_uid

User = get_user_model()


class Base64FileField(serializers.Field):
    """
    Accepts a base64-encoded file string (optionally with a data URI prefix
    such as `data:application/pdf;base64,<data>`) and converts it to a
    Django ContentFile ready for saving to a FileField.

    Payload format (either accepted):
        "file": "<pure base64 string>"
        "file": "data:image/jpeg;base64,<base64 string>"

    A companion field `file_name` may optionally be sent; otherwise a UUID
    filename is generated with an extension inferred from the content.
    """

    def to_internal_value(self, data):
        if not isinstance(data, str):
            raise serializers.ValidationError('A base64-encoded string is required.')

        # Strip optional data URI prefix: data:<mime>;base64,<data>
        if ';base64,' in data:
            header, data = data.split(';base64,', 1)
            mime = header.replace('data:', '').strip()
        else:
            mime = None

        try:
            decoded = base64.b64decode(data)
        except Exception:
            raise serializers.ValidationError('Invalid base64 encoding.')

        # Infer extension
        ext = _ext_from_mime(mime) or _ext_from_bytes(decoded) or 'bin'
        filename = f'{uuid.uuid4().hex}.{ext}'
        return ContentFile(decoded, name=filename)

    def to_representation(self, value):
        return value.url if value else None


def _ext_from_mime(mime):
    _map = {
        'image/jpeg': 'jpg',
        'image/jpg': 'jpg',
        'image/png': 'png',
        'image/gif': 'gif',
        'image/webp': 'webp',
        'application/pdf': 'pdf',
    }
    return _map.get(mime)


def _ext_from_bytes(data):
    if not data:
        return None
    if imghdr is not None:
        try:
            kind = imghdr.what(None, h=data)
            if kind:
                return 'jpg' if kind == 'jpeg' else kind
        except Exception:
            pass
    if data[:3] == b'\xff\xd8\xff':
        return 'jpg'
    if data[:8] == b'\x89PNG\r\n\x1a\n':
        return 'png'
    if data[:6] in (b'GIF87a', b'GIF89a'):
        return 'gif'
    if len(data) >= 12 and data[:4] == b'RIFF' and data[8:12] == b'WEBP':
        return 'webp'
    if data[:4] == b'%PDF':
        return 'pdf'
    return None


class Base64ImageField(Base64FileField):
    """Base64 image payload (optionally as a data URI) decoded to a ContentFile."""

    def to_internal_value(self, data):
        if data in (None, ''):
            return None
        content_file = super().to_internal_value(data)
        ext = _ext_from_bytes(content_file.read()) or ''
        content_file.seek(0)
        if ext not in ('jpg', 'jpeg', 'png', 'gif', 'webp'):
            raise serializers.ValidationError('Must be a valid image file (JPEG, PNG, GIF, or WebP).')
        return content_file


def save_luggage_image_and_get_url(content_file, request):
    from django.core.files.storage import default_storage
    from django.utils import timezone

    from api.storage_utils import ensure_upload_dir

    date_path = timezone.now().strftime('%Y/%m/%d')
    rel_dir = f'shipment/luggage/{date_path}'
    ensure_upload_dir(rel_dir)
    saved_path = default_storage.save(f'{rel_dir}/{content_file.name}', content_file)
    return build_absolute_media_url(request, default_storage.url(saved_path))


def save_avatar_and_get_url(content_file, request, *, kind='user'):
    """Decode-ready ContentFile → media file; return absolute URL for DB storage."""
    from django.core.files.storage import default_storage
    from django.utils import timezone

    from api.storage_utils import ensure_upload_dir

    if kind not in ('user', 'vehicle'):
        kind = 'user'
    date_path = timezone.now().strftime('%Y/%m/%d')
    rel_dir = f'avatars/{kind}/{date_path}'
    ensure_upload_dir(rel_dir)
    saved_path = default_storage.save(f'{rel_dir}/{content_file.name}', content_file)
    return build_absolute_media_url(request, default_storage.url(saved_path))


# ---- Shipper 1: Registration & KYC ----
class ShipperRegisterSerializer(serializers.Serializer):
    email = serializers.EmailField()
    password = serializers.CharField(write_only=True, min_length=8)
    first_name = serializers.CharField(required=False, allow_blank=True)
    last_name = serializers.CharField(required=False, allow_blank=True)
    phone = serializers.CharField(required=False, allow_blank=True)
    account_type = serializers.ChoiceField(choices=ShipperProfile.AccountType.choices)
    company_name = serializers.CharField(required=False, allow_blank=True)
    business_name = serializers.CharField(
        required=False,
        allow_blank=True,
        max_length=255,
        help_text='Optional business / trading name.',
    )
    tax_id = serializers.CharField(
        required=False,
        allow_blank=True,
        max_length=64,
        help_text='Optional tax / VAT ID.',
    )
    national_id_number = serializers.CharField(
        required=False,
        allow_blank=True,
        max_length=64,
        help_text='Optional national ID number.',
    )
    national_id_expiry_date = serializers.DateField(
        required=False,
        allow_null=True,
        help_text='Optional national ID expiry date.',
    )
    national_id_file = Base64FileField(
        required=False,
        write_only=True,
        help_text=(
            'Optional base64 national ID document (data URI or raw). '
            'When provided, creates an ID KYC document during signup.'
        ),
    )
    passport_number = serializers.CharField(
        required=False,
        allow_blank=True,
        max_length=64,
        help_text='Optional passport number.',
    )
    passport_expiry_date = serializers.DateField(
        required=False,
        allow_null=True,
        help_text='Optional passport expiry date.',
    )
    passport_file = Base64FileField(
        required=False,
        write_only=True,
        help_text=(
            'Optional base64 passport document (data URI or raw). '
            'When provided, creates a PASSPORT_COPY KYC document during signup '
            '(no separate /shipper/kyc-documents/ call needed for passport).'
        ),
    )

    def validate_national_id_number(self, value):
        return (value or '').strip()

    def validate_passport_number(self, value):
        return normalize_passport_number(value) or ''

    def validate(self, data):
        passport_number = data.get('passport_number') or ''
        if passport_number:
            validate_unique_passport_number(passport_number)
        has_national = bool(
            (data.get('national_id_number') or '').strip()
            or data.get('national_id_file')
        )
        has_passport = bool(passport_number or data.get('passport_file'))
        if not has_national and not has_passport:
            raise serializers.ValidationError(
                'Provide national ID or passport (number or file). At least one is required.',
            )
        return data

    def create(self, validated_data):
        from django.utils import timezone

        from api.storage_utils import ensure_upload_dir

        email = validated_data['email'].strip().lower()
        if User.objects.filter(username=email).exists():
            raise serializers.ValidationError({'email': 'A user with this email already exists.'})

        passport_file = validated_data.pop('passport_file', None)
        national_id_file = validated_data.pop('national_id_file', None)
        passport_number = validated_data.get('passport_number') or ''
        passport_expiry = validated_data.get('passport_expiry_date')
        national_id_number = validated_data.get('national_id_number') or ''
        national_id_expiry = validated_data.get('national_id_expiry_date')

        with transaction.atomic():
            user = User.objects.create_user(
                username=email,
                email=email,
                password=validated_data['password'],
                first_name=validated_data.get('first_name') or '',
                last_name=validated_data.get('last_name') or '',
            )
            UserRole.objects.create(
                user=user,
                role=UserRole.Role.SHIPPER,
                phone=validated_data.get('phone') or '',
            )
            profile = ShipperProfile.objects.create(
                user=user,
                account_type=validated_data['account_type'],
                company_name=validated_data.get('company_name') or '',
                business_name=(validated_data.get('business_name') or '').strip(),
                tax_id=(validated_data.get('tax_id') or '').strip(),
                national_id_number=national_id_number,
                national_id_expiry_date=national_id_expiry,
                passport_number=passport_number,
                passport_expiry_date=passport_expiry,
            )
            kyc_created = False
            if national_id_file or passport_file:
                ensure_upload_dir(f"kyc/{timezone.now().strftime('%Y/%m/%d')}")
            if national_id_file:
                KYCDocument.objects.create(
                    user=user,
                    document_type=KYCDocument.DocumentType.ID,
                    file=national_id_file,
                    expiry_date=national_id_expiry,
                    review_status=KYCDocument.ReviewStatus.PENDING,
                )
                kyc_created = True
            if passport_file:
                KYCDocument.objects.create(
                    user=user,
                    document_type=KYCDocument.DocumentType.PASSPORT_COPY,
                    file=passport_file,
                    passport_number=passport_number or None,
                    expiry_date=passport_expiry,
                    review_status=KYCDocument.ReviewStatus.PENDING,
                )
                kyc_created = True
            if kyc_created and not profile.kyc_submitted_at:
                profile.kyc_submitted_at = timezone.now()
                profile.save(update_fields=['kyc_submitted_at', 'updated_at'])
        return user


# ---- Transporter: Registration & Documents ----
class TransporterRegisterSerializer(serializers.Serializer):
    email = serializers.EmailField()
    password = serializers.CharField(write_only=True, min_length=8)
    first_name = serializers.CharField(required=False, allow_blank=True)
    last_name = serializers.CharField(required=False, allow_blank=True)
    phone = serializers.CharField(required=False, allow_blank=True)
    account_type = serializers.ChoiceField(
        choices=[
            (TransporterProfile.AccountType.DRIVER, 'Individual driver'),
            (TransporterProfile.AccountType.FLEET_OWNER, 'Fleet owner'),
        ],
    )
    company_name = serializers.CharField(
        required=False,
        allow_blank=True,
        help_text='Required for FLEET_OWNER; optional for individual DRIVER.',
    )
    office_number = serializers.CharField(required=False, allow_blank=True)
    mobile_number = serializers.CharField(required=False, allow_blank=True)
    company_location = serializers.CharField(required=False, allow_blank=True)
    language = serializers.CharField(required=False, default='en', allow_blank=True)
    country_to_country = serializers.BooleanField()
    local = serializers.BooleanField()
    avatar = Base64ImageField(required=False, write_only=True, allow_null=True)

    def validate(self, data):
        validate_load_type_xor(data['country_to_country'], data['local'])
        if data.get('account_type') == TransporterProfile.AccountType.FLEET_OWNER:
            company_name = (data.get('company_name') or '').strip()
            if not company_name:
                raise serializers.ValidationError({
                    'company_name': 'Company name is required for fleet owner signup.',
                })
            data['company_name'] = company_name
            data['office_number'] = (data.get('office_number') or '').strip()
            data['mobile_number'] = (data.get('mobile_number') or '').strip()
            data['company_location'] = (data.get('company_location') or '').strip()
        else:
            # Individual driver: company_name is optional.
            data['company_name'] = (data.get('company_name') or '').strip()
        return data

    def create(self, validated_data):
        email = validated_data['email'].strip().lower()
        if User.objects.filter(username=email).exists():
            raise serializers.ValidationError({'email': 'A user with this email already exists.'})

        avatar_file = validated_data.pop('avatar', None)

        with transaction.atomic():
            user = User.objects.create_user(
                username=email,
                email=email,
                password=validated_data['password'],
                first_name=validated_data.get('first_name') or '',
                last_name=validated_data.get('last_name') or '',
            )
            UserRole.objects.create(
                user=user,
                role=UserRole.Role.TRANSPORTER,
                phone=validated_data.get('phone') or '',
                language=validated_data.get('language') or 'en',
            )

            profile_data = {
                'user': user,
                'account_type': validated_data['account_type'],
                'company_name': validated_data.get('company_name') or '',
                'country_to_country': validated_data['country_to_country'],
                'local': validated_data['local'],
            }
            if validated_data['account_type'] == TransporterProfile.AccountType.FLEET_OWNER:
                profile_data['office_number'] = validated_data.get('office_number') or ''
                profile_data['mobile_number'] = validated_data.get('mobile_number') or ''
                profile_data['company_location'] = validated_data.get('company_location') or ''

            if validated_data['account_type'] == TransporterProfile.AccountType.DRIVER:
                tc_u_id = generate_unique_tc_uid()
                full_name = f"{validated_data.get('first_name') or ''} {validated_data.get('last_name') or ''}".strip()
                try:
                    device = create_traccar_device(tc_u_id, device_name=full_name)
                except ValueError as exc:
                    raise serializers.ValidationError({'traccar': str(exc)})
                profile_data['tc_u_id'] = tc_u_id
                profile_data['tc_id'] = str(device.get('id'))

            if avatar_file:
                profile_data['avatar_url'] = save_avatar_and_get_url(
                    avatar_file,
                    self.context.get('request'),
                    kind='user',
                )

            TransporterProfile.objects.create(**profile_data)

        return user


class TransporterProfileSerializer(serializers.ModelSerializer):
    user_id = serializers.IntegerField(source='user.id', read_only=True)
    email = serializers.EmailField(source='user.email', read_only=True)
    first_name = serializers.CharField(source='user.first_name')
    last_name = serializers.CharField(source='user.last_name')
    phone = serializers.CharField(source='user.role.phone', default='')
    language = serializers.CharField(source='user.role.language', default='en')
    documents_verified = serializers.ReadOnlyField()
    tc_id = serializers.ReadOnlyField()
    tc_u_id = serializers.ReadOnlyField()
    avatar = Base64ImageField(required=False, write_only=True, allow_null=True)
    vehicle = serializers.SerializerMethodField()
    vehicles = serializers.SerializerMethodField()
    gcc_id = serializers.SerializerMethodField()

    class Meta:
        model = TransporterProfile
        fields = [
            'user_id', 'email', 'first_name', 'last_name', 'phone', 'language',
            'account_type', 'company_name',
            'office_number', 'mobile_number', 'company_location',
            'avatar', 'avatar_url',
            'country_to_country', 'local',
            'documents_verified', 'tc_id', 'tc_u_id', 'gcc_id',
            'vehicle', 'vehicles',
            'created_at', 'updated_at',
        ]
        read_only_fields = [
            'user_id', 'email', 'avatar_url', 'documents_verified', 'tc_id', 'tc_u_id',
            'gcc_id', 'vehicle', 'vehicles', 'created_at', 'updated_at',
        ]

    def _vehicles_for_user(self, user):
        cached = getattr(user, '_profile_vehicles_cache', None)
        if cached is not None:
            return cached
        qs = (
            Vehicle.objects.filter(Q(owner=user) | Q(assigned_driver=user))
            .select_related('owner', 'assigned_driver')
            .order_by('-updated_at', '-id')
        )
        user._profile_vehicles_cache = list(qs)
        return user._profile_vehicles_cache

    def get_vehicles(self, obj):
        return VehicleSerializer(self._vehicles_for_user(obj.user), many=True).data

    def get_vehicle(self, obj):
        vehicles = self._vehicles_for_user(obj.user)
        primary = next(
            (v for v in vehicles if v.is_active and v.is_verified),
            vehicles[0] if vehicles else None,
        )
        return VehicleSerializer(primary).data if primary else None

    def get_gcc_id(self, obj):
        """GCC country code from the user's COUNTRY_GCC KYC document, if any."""
        cached = getattr(obj.user, '_profile_gcc_id_cache', None)
        if cached is not None:
            return cached
        doc = (
            KYCDocument.objects.filter(
                user=obj.user,
                document_type=KYCDocument.DocumentType.COUNTRY_GCC,
            )
            .exclude(gcc_country='')
            .order_by('-submitted_at')
            .only('gcc_country')
            .first()
        )
        value = doc.gcc_country if doc else ''
        obj.user._profile_gcc_id_cache = value
        return value

    def validate(self, data):
        if 'country_to_country' in data or 'local' in data:
            if 'country_to_country' not in data or 'local' not in data:
                raise serializers.ValidationError(
                    'Both country_to_country and local are required when updating load type.'
                )
            validate_load_type_xor(data['country_to_country'], data['local'])
        return data

    def update(self, instance, validated_data):
        user_data = validated_data.pop('user', {})
        role_data = user_data.pop('role', {})
        avatar_file = validated_data.pop('avatar', None)

        user = instance.user
        for attr, value in user_data.items():
            setattr(user, attr, value)
        user.save()

        if role_data:
            try:
                role = user.role
                if 'phone' in role_data:
                    role.phone = role_data['phone']
                if 'language' in role_data:
                    role.language = role_data.get('language', 'en')
                role.save()
            except Exception:
                pass

        for attr, value in validated_data.items():
            setattr(instance, attr, value)
        if avatar_file:
            instance.avatar_url = save_avatar_and_get_url(
                avatar_file,
                self.context.get('request'),
                kind='user',
            )
        instance.save()

        return instance


class ShipperProfileSerializer(serializers.ModelSerializer):
    # User fields
    user_id = serializers.IntegerField(source='user.id', read_only=True)
    email = serializers.EmailField(source='user.email', read_only=True)
    first_name = serializers.CharField(source='user.first_name')
    last_name = serializers.CharField(source='user.last_name')
    phone = serializers.CharField(source='user.role.phone', default='')

    # Profile fields (read-only)
    kyc_verified = serializers.ReadOnlyField()
    kyc_submitted_at = serializers.ReadOnlyField()

    credit_approved = serializers.ReadOnlyField()
    shipment_preferences = serializers.JSONField(required=False)

    class Meta:
        model = ShipperProfile
        fields = [
            'user_id', 'email', 'first_name', 'last_name', 'phone',
            'account_type', 'company_name', 'business_name', 'tax_id',
            'national_id_number', 'national_id_expiry_date',
            'passport_number', 'passport_expiry_date',
            'kyc_verified', 'kyc_submitted_at',
            'credit_approved', 'shipment_preferences',
            'created_at', 'updated_at',
        ]
        read_only_fields = ['user_id', 'email', 'kyc_verified', 'kyc_submitted_at', 'credit_approved', 'created_at', 'updated_at']

    def validate_national_id_number(self, value):
        number = (value or '').strip()
        if not number:
            raise serializers.ValidationError('National ID number cannot be blank.')
        return number

    def validate_passport_number(self, value):
        return normalize_passport_number(value) or ''

    def validate(self, data):
        passport_number = data.get('passport_number', getattr(self.instance, 'passport_number', '') or '')
        passport_expiry = data.get(
            'passport_expiry_date',
            getattr(self.instance, 'passport_expiry_date', None),
        )
        if 'passport_number' in data or 'passport_expiry_date' in data:
            if passport_expiry and not passport_number:
                raise serializers.ValidationError({
                    'passport_number': 'Passport number is required when passport expiry date is provided.',
                })
        if 'passport_number' in data and passport_number:
            validate_unique_passport_number(
                passport_number,
                exclude_shipper_id=self.instance.pk if self.instance else None,
            )
        return data

    def update(self, instance, validated_data):
        user_data = validated_data.pop('user', {})
        role_data = user_data.pop('role', {})

        # Update User fields
        user = instance.user
        for attr, value in user_data.items():
            setattr(user, attr, value)
        user.save()

        # Update UserRole phone
        if 'phone' in role_data:
            try:
                user.role.phone = role_data['phone']
                user.role.save(update_fields=['phone'])
            except Exception:
                pass

        # Update ShipperProfile fields
        for attr, value in validated_data.items():
            setattr(instance, attr, value)
        instance.save()

        return instance


class KYCDocumentSerializer(serializers.ModelSerializer):
    file_url = serializers.SerializerMethodField()
    file_back_url = serializers.SerializerMethodField()
    gcc_country_name = serializers.SerializerMethodField()

    class Meta:
        model = KYCDocument
        fields = [
            'id',
            'document_type',
            'gcc_country',
            'gcc_country_name',
            'passport_number',
            'file',
            'file_url',
            'file_back',
            'file_back_url',
            'verified',
            'review_status',
            'review_notes',
            'expiry_date',
            'submitted_at',
            'reviewed_at',
        ]
        read_only_fields = [
            'verified', 'review_status', 'review_notes',
            'submitted_at', 'reviewed_at', 'expiry_date',
        ]

    def get_file_url(self, obj):
        if obj.file:
            return build_absolute_media_url(self.context.get('request'), obj.file.url)
        return None

    def get_file_back_url(self, obj):
        if obj.file_back:
            return build_absolute_media_url(self.context.get('request'), obj.file_back.url)
        return None

    def get_gcc_country_name(self, obj):
        from core.countries import country_name_for_code
        return country_name_for_code(obj.gcc_country) or ''


# Transporter document types (post-signup KYC upload)
TRANSPORTER_DRIVER_DOC_TYPES = list(KYCDocument.DRIVER_DOC_TYPES)
TRANSPORTER_FLEET_OWNER_DOC_TYPES = list(KYCDocument.FLEET_OWNER_DOC_TYPES)
TRANSPORTER_DOC_TYPES = list(KYCDocument.TRANSPORTER_DOC_TYPES)
TRANSPORTER_TWO_SIDED_DOC_TYPES = list(KYCDocument.TRANSPORTER_TWO_SIDED_TYPES)


def transporter_doc_types_for_account(account_type):
    if account_type == TransporterProfile.AccountType.FLEET_OWNER:
        return TRANSPORTER_FLEET_OWNER_DOC_TYPES
    return TRANSPORTER_DRIVER_DOC_TYPES


def normalize_passport_number(value):
    """Normalize passport number for storage and uniqueness checks."""
    return (value or '').strip().upper() or None


def validate_unique_passport_number(passport_number, *, exclude_doc_id=None, exclude_shipper_id=None):
    if not passport_number:
        return
    qs = KYCDocument.objects.filter(passport_number__iexact=passport_number)
    if exclude_doc_id:
        qs = qs.exclude(pk=exclude_doc_id)
    if qs.exists():
        raise serializers.ValidationError({
            'passport_number': 'This passport number is already registered.',
        })
    profile_qs = ShipperProfile.objects.filter(passport_number__iexact=passport_number)
    if exclude_shipper_id:
        profile_qs = profile_qs.exclude(pk=exclude_shipper_id)
    if profile_qs.exists():
        raise serializers.ValidationError({
            'passport_number': 'This passport number is already registered.',
        })


class TransporterDocumentUploadSerializer(serializers.Serializer):
    document_type = serializers.CharField()
    file = Base64FileField(required=False)
    file_back = Base64FileField(required=False)
    gcc_country = serializers.CharField(required=False, allow_blank=True)
    passport_number = serializers.CharField(required=False, allow_blank=True, max_length=64)
    expiry_date = serializers.DateField(required=False, allow_null=True)

    def validate_document_type(self, value):
        value = (value or '').strip().upper()
        force_account_type = self.context.get('force_account_type')
        if force_account_type is not None:
            account_type = force_account_type
        else:
            user = self.context.get('user')
            account_type = TransporterProfile.AccountType.DRIVER
            if user is not None:
                try:
                    account_type = user.transporter_profile.account_type
                except TransporterProfile.DoesNotExist:
                    pass
        allowed = set(transporter_doc_types_for_account(account_type))
        if value not in allowed:
            raise serializers.ValidationError(
                f'Invalid document_type for this account. Allowed: {", ".join(sorted(allowed))}.'
            )
        return value

    def validate_gcc_country(self, value):
        from core.countries import is_gcc_country_code, normalize_country_code

        value = (value or '').strip().upper()
        if not value:
            return ''
        code = normalize_country_code(value)
        if not code or not is_gcc_country_code(code):
            raise serializers.ValidationError(
                'gcc_country must be a GCC country code: AE, SA, OM, QA, BH, or KW.'
            )
        return code

    def validate_passport_number(self, value):
        return normalize_passport_number(value)

    def validate(self, data):
        doc_type = data['document_type']
        user = self.context.get('user')
        existing = None
        if user:
            existing = KYCDocument.objects.filter(user=user, document_type=doc_type).first()

        file_front = data.get('file')
        file_back = data.get('file_back')
        two_sided = doc_type in KYCDocument.TRANSPORTER_TWO_SIDED_TYPES
        gcc_country = data.get('gcc_country', '')
        passport_number = data.get('passport_number')
        passport_provided = 'passport_number' in self.initial_data

        if existing is None:
            if not file_front:
                raise serializers.ValidationError({'file': 'Front file is required.'})
            if two_sided and not file_back:
                raise serializers.ValidationError({'file_back': 'Back file is required.'})
            if doc_type == KYCDocument.DocumentType.COUNTRY_GCC and not gcc_country:
                raise serializers.ValidationError({
                    'gcc_country': 'GCC country is required for GCC ID uploads.',
                })
            if doc_type == KYCDocument.DocumentType.PASSPORT_COPY and not passport_number:
                raise serializers.ValidationError({
                    'passport_number': 'Passport number is required for passport uploads.',
                })
        elif (
            not file_front
            and not file_back
            and 'expiry_date' not in data
            and 'gcc_country' not in data
            and not passport_provided
        ):
            raise serializers.ValidationError(
                'Provide at least one of file, file_back, gcc_country, passport_number, or expiry_date to update the document.'
            )

        if doc_type != KYCDocument.DocumentType.COUNTRY_GCC:
            data['gcc_country'] = ''
        elif existing is not None and 'gcc_country' not in self.initial_data:
            data.pop('gcc_country', None)

        if doc_type == KYCDocument.DocumentType.PASSPORT_COPY:
            if passport_provided:
                number = data.get('passport_number')
            elif existing is not None:
                data.pop('passport_number', None)
                number = existing.passport_number
            else:
                number = data.get('passport_number')
            if existing is None or passport_provided:
                if not number:
                    raise serializers.ValidationError({
                        'passport_number': 'Passport number is required for passport uploads.',
                    })
                validate_unique_passport_number(
                    number,
                    exclude_doc_id=existing.pk if existing else None,
                )
                data['passport_number'] = number
        else:
            data['passport_number'] = None

        return data

    def create(self, validated_data):
        from accounts.kyc_review import reset_kyc_document_for_resubmit, sync_user_kyc_verified

        user = validated_data.pop('user', None)
        if user is None:
            raise serializers.ValidationError('User is required.')

        document_type = validated_data.pop('document_type')
        doc = KYCDocument.objects.filter(user=user, document_type=document_type).first()
        if doc is None:
            doc = KYCDocument.objects.create(
                user=user,
                document_type=document_type,
                review_status=KYCDocument.ReviewStatus.PENDING,
                **validated_data,
            )
            sync_user_kyc_verified(user)
            return doc

        file_changed = 'file' in validated_data or 'file_back' in validated_data
        update_fields = []
        for field in ('file', 'file_back', 'expiry_date', 'gcc_country', 'passport_number'):
            if field not in validated_data:
                continue
            setattr(doc, field, validated_data[field])
            update_fields.append(field)
        if file_changed:
            reset_kyc_document_for_resubmit(doc, file_changed=True)
            update_fields.extend([
                'review_status', 'review_notes', 'verified', 'reviewed_at', 'reviewed_by',
            ])
        if update_fields:
            doc.save(update_fields=list(dict.fromkeys(update_fields)))
            sync_user_kyc_verified(user)
        return doc


def upsert_driver_kyc_documents(user, documents, request=None):
    """Upsert individual-driver KYC docs (same types/expiry rules) for a user."""
    from django.utils import timezone

    from api.storage_utils import ensure_upload_dir

    if not documents:
        return []
    ensure_upload_dir(f"kyc/{timezone.now().strftime('%Y/%m/%d')}")
    ready = []
    errors = []
    for index, raw in enumerate(documents):
        ser = TransporterDocumentUploadSerializer(
            data=raw,
            context={
                'request': request,
                'user': user,
                'force_account_type': TransporterProfile.AccountType.DRIVER,
            },
        )
        if not ser.is_valid():
            errors.append({index: ser.errors})
        else:
            ready.append(ser)
    if errors:
        raise serializers.ValidationError({'documents': errors})
    return [ser.save(user=user) for ser in ready]


class VehicleSerializer(serializers.ModelSerializer):
    owner_id = serializers.IntegerField(source='owner.id', read_only=True)
    assigned_driver_id = serializers.IntegerField(source='assigned_driver.id', read_only=True)
    assigned_driver_email = serializers.EmailField(source='assigned_driver.email', read_only=True)
    avatar = Base64ImageField(required=False, write_only=True, allow_null=True)
    vehicle_types = serializers.ListField(
        child=serializers.CharField(max_length=100, allow_blank=False),
        required=False,
        allow_empty=False,
        help_text='One or more vehicle types this unit can serve.',
    )
    noc_file = Base64FileField(
        required=False,
        write_only=True,
        allow_null=True,
        help_text='Optional NOC document (base64). Not required to create a vehicle.',
    )
    noc_expiry_date = serializers.DateField(
        required=False,
        allow_null=True,
        write_only=True,
        help_text='Optional expiry for NOC when noc_file is provided.',
    )

    class Meta:
        model = Vehicle
        fields = [
            'id',
            'owner_id',
            'vehicle_type',
            'vehicle_types',
            'registration_number',
            'load_capacity',
            'max_length_m',
            'max_width_m',
            'max_height_m',
            'special_features',
            'avatar',
            'avatar_url',
            'noc_file',
            'noc_expiry_date',
            'is_active',
            'is_verified',
            'assigned_driver_id',
            'assigned_driver_email',
            'verified_at',
            'created_at',
            'updated_at',
        ]
        read_only_fields = [
            'id',
            'owner_id',
            'avatar_url',
            'is_verified',
            'verified_at',
            'created_at',
            'updated_at',
            'assigned_driver_id',
            'assigned_driver_email',
        ]
        extra_kwargs = {
            'vehicle_type': {'required': False, 'allow_blank': True},
        }

    def _normalize_vehicle_types(self, raw_types) -> list[str]:
        from core.load_discovery import normalize_vehicle_type

        if not isinstance(raw_types, list):
            raise serializers.ValidationError({'vehicle_types': 'Must be a list of strings.'})
        cleaned = []
        seen = set()
        for item in raw_types:
            text = str(item or '').strip()
            if not text:
                continue
            canonical = normalize_vehicle_type(text) or text
            key = canonical.casefold()
            if key in seen:
                continue
            seen.add(key)
            cleaned.append(canonical)
        if not cleaned:
            raise serializers.ValidationError({
                'vehicle_types': 'Provide at least one non-empty vehicle type.',
            })
        return cleaned

    def validate(self, data):
        noc_file = data.get('noc_file')
        has_expiry = 'noc_expiry_date' in self.initial_data
        if has_expiry and self.initial_data.get('noc_expiry_date') not in (None, ''):
            if not noc_file and self.instance is None:
                raise serializers.ValidationError({
                    'noc_file': 'NOC file is required when noc_expiry_date is provided.',
                })
            if (
                self.instance is not None
                and not noc_file
                and not VehicleDocument.objects.filter(
                    vehicle=self.instance,
                    document_type=VehicleDocument.DocumentType.NOC,
                ).exists()
            ):
                raise serializers.ValidationError({
                    'noc_file': 'NOC file is required when noc_expiry_date is provided.',
                })

        types_in_payload = 'vehicle_types' in self.initial_data
        type_in_payload = 'vehicle_type' in self.initial_data
        if types_in_payload:
            data['vehicle_types'] = self._normalize_vehicle_types(self.initial_data.get('vehicle_types'))
            data['vehicle_type'] = data['vehicle_types'][0]
        elif type_in_payload:
            primary = (data.get('vehicle_type') or self.initial_data.get('vehicle_type') or '').strip()
            if not primary and self.instance is None:
                raise serializers.ValidationError({
                    'vehicle_type': 'This field may not be blank.',
                })
            if primary:
                data['vehicle_types'] = self._normalize_vehicle_types([primary])
                data['vehicle_type'] = data['vehicle_types'][0]
        elif self.instance is None:
            raise serializers.ValidationError({
                'vehicle_types': 'Provide vehicle_types (preferred) or vehicle_type.',
            })
        return data

    def to_representation(self, instance):
        data = super().to_representation(instance)
        types = instance.type_list() if hasattr(instance, 'type_list') else []
        data['vehicle_types'] = types
        if types:
            data['vehicle_type'] = types[0]
        return data

    def validate_registration_number(self, value):
        registration = (value or '').strip()
        if not registration:
            raise serializers.ValidationError('This field may not be blank.')

        request = self.context.get('request')
        owner = None
        if self.instance is not None:
            owner = self.instance.owner
        elif request is not None and getattr(request, 'user', None) is not None:
            owner = request.user

        if owner is not None:
            qs = Vehicle.objects.filter(owner=owner, registration_number=registration)
            if self.instance is not None:
                qs = qs.exclude(pk=self.instance.pk)
            if qs.exists():
                raise serializers.ValidationError(
                    'You already have a vehicle with this registration number.'
                )
        return registration

    def _apply_avatar(self, vehicle, content_file):
        if not content_file:
            return vehicle
        vehicle.avatar_url = save_avatar_and_get_url(
            content_file,
            self.context.get('request'),
            kind='vehicle',
        )
        vehicle.save(update_fields=['avatar_url', 'updated_at'])
        return vehicle

    def _apply_optional_noc(self, vehicle, noc_file, noc_expiry_date):
        if not noc_file:
            return
        from django.utils import timezone

        from api.storage_utils import ensure_upload_dir

        ensure_upload_dir(f"vehicle-docs/{timezone.now().strftime('%Y/%m/%d')}")
        VehicleDocument.objects.update_or_create(
            vehicle=vehicle,
            document_type=VehicleDocument.DocumentType.NOC,
            defaults={
                'file': noc_file,
                'expiry_date': noc_expiry_date,
                'review_status': VehicleDocument.ReviewStatus.PENDING,
                'review_notes': '',
                'reviewed_at': None,
                'reviewed_by': None,
            },
        )

    def create(self, validated_data):
        avatar_file = validated_data.pop('avatar', None)
        noc_file = validated_data.pop('noc_file', None)
        noc_expiry_date = validated_data.pop('noc_expiry_date', None)
        vehicle = super().create(validated_data)
        self._apply_avatar(vehicle, avatar_file)
        self._apply_optional_noc(vehicle, noc_file, noc_expiry_date)
        return vehicle

    def update(self, instance, validated_data):
        avatar_file = validated_data.pop('avatar', None)
        noc_file = validated_data.pop('noc_file', None)
        noc_expiry_date = validated_data.pop('noc_expiry_date', None)
        vehicle = super().update(instance, validated_data)
        self._apply_avatar(vehicle, avatar_file)
        # PATCH: only write NOC when a file is sent (expiry alone does not create).
        if noc_file:
            self._apply_optional_noc(vehicle, noc_file, noc_expiry_date)
        elif 'noc_expiry_date' in self.initial_data and noc_file is None:
            existing = VehicleDocument.objects.filter(
                vehicle=vehicle,
                document_type=VehicleDocument.DocumentType.NOC,
            ).first()
            if existing is not None:
                existing.expiry_date = noc_expiry_date
                existing.save(update_fields=['expiry_date'])
        return vehicle


class VehicleDriverAssignSerializer(serializers.Serializer):
    driver_id = serializers.IntegerField(required=False, allow_null=True, default=None)


class PlatformSettingsSerializer(serializers.ModelSerializer):
    class Meta:
        model = PlatformSettings
        fields = [
            'load_visibility_radius_km',
            'load_visibility_max_radius_km',
            'default_wallet_currency',
            'document_reminder_days',
            'analytics_transporter_bid_window_days',
            'on_time_delivery_grace_hours',
            'nearby_load_notify_radius_km',
            'nearby_publish_notify_max',
            'navigation_naive_speed_kph',
            'pod_max_delivery_distance_km',
            'admin_stale_trip_hours',
            'bidding_rate_distribution_count',
        ]

    def validate_default_wallet_currency(self, value):
        code = (value or '').strip().upper()
        if len(code) != 3 or not code.isalpha():
            raise serializers.ValidationError('Enter a 3-letter ISO currency code (e.g. USD).')
        return code


class DriverTripStatusSerializer(serializers.Serializer):
    status = serializers.CharField()
    lat = serializers.FloatField(required=False, allow_null=True)
    lon = serializers.FloatField(required=False, allow_null=True)
    recorded_at = serializers.DateTimeField(required=False, allow_null=True)


class TripAssignDriverSerializer(serializers.Serializer):
    driver_id = serializers.IntegerField(required=False, allow_null=True, default=None)


class VehicleDocumentSerializer(serializers.ModelSerializer):
    file_url = serializers.SerializerMethodField()
    reviewed_by_id = serializers.IntegerField(source='reviewed_by.id', read_only=True)

    class Meta:
        model = VehicleDocument
        fields = [
            'id',
            'vehicle',
            'document_type',
            'file',
            'file_url',
            'review_status',
            'review_notes',
            'expiry_date',
            'submitted_at',
            'reviewed_at',
            'reviewed_by_id',
        ]
        read_only_fields = [
            'review_status',
            'review_notes',
            'expiry_date',
            'submitted_at',
            'reviewed_at',
            'reviewed_by_id',
        ]

    def get_file_url(self, obj):
        if obj.file:
            return build_absolute_media_url(self.context.get('request'), obj.file.url)
        return None


class VehicleDocumentUploadSerializer(serializers.Serializer):
    document_type = serializers.ChoiceField(choices=VehicleDocument.DocumentType.choices)
    file = Base64FileField()
    expiry_date = serializers.DateField(required=False, allow_null=True)

    def create(self, validated_data, **kwargs):
        vehicle = kwargs.pop('vehicle', None)
        if vehicle is not None:
            validated_data['vehicle'] = vehicle
        return VehicleDocument.objects.create(**validated_data)


class VehicleDocumentReviewSerializer(serializers.Serializer):
    review_status = serializers.ChoiceField(
        choices=[
            VehicleDocument.ReviewStatus.APPROVED,
            VehicleDocument.ReviewStatus.REJECTED,
        ]
    )
    review_notes = serializers.CharField(required=False, allow_blank=True)
    expiry_date = serializers.DateField(required=False, allow_null=True)


class VehicleVerificationSerializer(serializers.Serializer):
    is_verified = serializers.BooleanField()
    is_active = serializers.BooleanField(required=False)


class KYCDocumentUploadSerializer(serializers.Serializer):
    document_type = serializers.ChoiceField(choices=KYCDocument.DocumentType.choices)
    file = Base64FileField()
    passport_number = serializers.CharField(required=False, allow_blank=True, max_length=64)
    expiry_date = serializers.DateField(required=False, allow_null=True)

    def validate_document_type(self, value):
        value = (value or '').strip().upper()
        user = self.context.get('user')
        if user is None:
            request = self.context.get('request')
            user = getattr(request, 'user', None) if request is not None else None
        if user is not None:
            try:
                user.shipper_profile
            except ShipperProfile.DoesNotExist:
                return value
            allowed = set(KYCDocument.SHIPPER_DOC_TYPES)
            if value not in allowed:
                raise serializers.ValidationError(
                    f'Invalid document_type for shipper. Allowed: {", ".join(sorted(allowed))}.'
                )
        return value

    def validate_passport_number(self, value):
        return normalize_passport_number(value)

    def validate(self, data):
        doc_type = data.get('document_type')
        if doc_type == KYCDocument.DocumentType.PASSPORT_COPY:
            number = data.get('passport_number')
            if not number:
                raise serializers.ValidationError({
                    'passport_number': 'Passport number is required for passport uploads.',
                })
            user = self.context.get('user')
            existing = None
            if user:
                existing = KYCDocument.objects.filter(
                    user=user, document_type=doc_type,
                ).first()
            validate_unique_passport_number(
                number,
                exclude_doc_id=existing.pk if existing else None,
            )
            data['passport_number'] = number
        else:
            data['passport_number'] = None
        return data

    def create(self, validated_data, **kwargs):
        from accounts.kyc_review import reset_kyc_document_for_resubmit, sync_user_kyc_verified

        user = kwargs.pop('user', None)
        if user is not None:
            validated_data['user'] = user
        user = validated_data.get('user')
        document_type = validated_data.get('document_type')
        existing = None
        if user and document_type:
            existing = KYCDocument.objects.filter(user=user, document_type=document_type).first()
        if existing is None:
            validated_data.setdefault('review_status', KYCDocument.ReviewStatus.PENDING)
            doc = KYCDocument.objects.create(**validated_data)
            if user:
                sync_user_kyc_verified(user)
            return doc

        existing.file = validated_data['file']
        update_fields = [
            'file', 'review_status', 'review_notes',
            'verified', 'reviewed_at', 'reviewed_by',
        ]
        if 'expiry_date' in validated_data:
            existing.expiry_date = validated_data['expiry_date']
            update_fields.append('expiry_date')
        if 'passport_number' in validated_data:
            existing.passport_number = validated_data['passport_number']
            update_fields.append('passport_number')
        reset_kyc_document_for_resubmit(existing, file_changed=True)
        existing.save(update_fields=update_fields)
        sync_user_kyc_verified(user)
        return existing


# ---- Shipper 2: Shipments ----
class ShipmentSerializer(serializers.ModelSerializer):
    currency = serializers.SerializerMethodField()
    pod = serializers.SerializerMethodField()
    pod_url = serializers.SerializerMethodField()
    pod_urls = serializers.SerializerMethodField()

    class Meta:
        model = Shipment
        fields = [
            'id', 'unique_id', 'pickup_address', 'pickup_company', 'pickup_lat', 'pickup_lon',
            'pickup_country_code',
            'delivery_address', 'dropoff_company', 'delivery_lat', 'delivery_lon',
            'delivery_country_code',
            'cargo_type', 'weight', 'dimensions', 'vehicle_type_required',
            'special_instructions', 'luggage_image_url', 'distance_km', 'suggested_price',
            'currency',
            'price_breakdown',
            'is_return', 'country_to_country', 'local',
            'status', 'pod', 'pod_url', 'pod_urls', 'pickup_scheduled_at', 'created_at', 'updated_at'
        ]
        read_only_fields = [
            'unique_id', 'status', 'created_at', 'updated_at', 'luggage_image_url',
            'currency',
            'price_breakdown',
            'pod', 'pod_url', 'pod_urls',
        ]

    def get_currency(self, obj):
        return currency_for_shipment(obj)

    def _get_pod_obj(self, obj):
        try:
            trip = getattr(obj, 'trip', None)
            if trip:
                return getattr(trip, 'pod', None)
        except Exception:
            pass
        return None

    def get_pod(self, obj):
        pod = self._get_pod_obj(obj)
        if pod:
            return serialize_pod_summary(pod, request=self.context.get('request'))
        return None

    def get_pod_url(self, obj):
        pod = self._get_pod_obj(obj)
        if not pod:
            return None
        photos = getattr(pod, 'photos', None)
        first = photos.all().first() if photos else None
        if first and first.image:
            return build_absolute_media_url(self.context.get('request'), first.image.url)
        return None

    def get_pod_urls(self, obj):
        pod = self._get_pod_obj(obj)
        if not pod:
            return []
        photos = getattr(pod, 'photos', None)
        if not photos:
            return []
        urls = []
        for p in photos.all():
            if p.image:
                url = build_absolute_media_url(self.context.get('request'), p.image.url)
                if url:
                    urls.append(url)
        return urls



def shipper_public_summary(shipper):
    if not shipper:
        return None
    role = getattr(shipper, 'role', None)
    profile = getattr(shipper, 'shipper_profile', None)
    return {
        'id': shipper.id,
        'email': shipper.email,
        'first_name': shipper.first_name,
        'last_name': shipper.last_name,
        'phone': getattr(role, 'phone', '') or '',
        'account_type': getattr(profile, 'account_type', None),
        'company_name': getattr(profile, 'company_name', '') or '',
    }


def transporter_party_summary(user, request=None):
    """Public transporter/driver card for shipper-facing trip payloads."""
    if not user:
        return None
    role = getattr(user, 'role', None)
    profile = getattr(user, 'transporter_profile', None)
    fav_ids = get_shipper_favorite_transporter_ids(request) if request is not None else set()
    is_fav = user.id in fav_ids
    return {
        'id': user.id,
        'email': user.email,
        'first_name': user.first_name,
        'last_name': user.last_name,
        'phone': getattr(role, 'phone', '') or '',
        'account_type': getattr(profile, 'account_type', None),
        'company_name': getattr(profile, 'company_name', '') or '',
        'is_favourite': is_fav,
        'is_favorite': is_fav,
    }


def trip_pickup_summary(shipment):
    if not shipment:
        return None
    return {
        'address': shipment.pickup_address or '',
        'company': shipment.pickup_company or '',
        'lat': shipment.pickup_lat,
        'lon': shipment.pickup_lon,
        'country_code': shipment.pickup_country_code or '',
        'scheduled_at': shipment.pickup_scheduled_at,
    }


def trip_dropoff_summary(shipment):
    if not shipment:
        return None
    return {
        'address': shipment.delivery_address or '',
        'company': shipment.dropoff_company or '',
        'lat': shipment.delivery_lat,
        'lon': shipment.delivery_lon,
        'country_code': shipment.delivery_country_code or '',
    }


class TransporterAvailableShipmentSerializer(ShipmentSerializer):
    shipper = serializers.SerializerMethodField()

    class Meta(ShipmentSerializer.Meta):
        fields = [*ShipmentSerializer.Meta.fields, 'shipper']

    def get_shipper(self, obj):
        return shipper_public_summary(obj.shipper)


class AdminShipmentSerializer(ShipmentSerializer):
    """Shipper id included for admin list/detail."""

    class Meta(ShipmentSerializer.Meta):
        fields = ['shipper', *ShipmentSerializer.Meta.fields]


class RateRequestSerializer(serializers.ModelSerializer):
    shipment = ShipmentSerializer(read_only=True)
    driver_email = serializers.EmailField(source='driver.email', read_only=True)
    driver_name = serializers.SerializerMethodField()

    class Meta:
        model = RateRequest
        fields = [
            'id',
            'shipment_id',
            'shipment',
            'driver_id',
            'driver_email',
            'driver_name',
            'batch_number',
            'status',
            'distance_km',
            'offered_price',
            'currency',
            'rejection_reason',
            'created_at',
            'responded_at',
        ]
        read_only_fields = [
            'id',
            'shipment_id',
            'shipment',
            'driver_id',
            'driver_email',
            'driver_name',
            'batch_number',
            'created_at',
            'responded_at',
        ]

    def get_driver_name(self, obj):
        return obj.driver.get_full_name() or obj.driver.email


class RateRequestRejectSerializer(serializers.Serializer):
    reason = serializers.CharField(required=False, allow_blank=True, max_length=255)


class ShipperDriverLocationsSerializer(serializers.Serializer):
    shipment_id = serializers.IntegerField(min_value=1)
    country_code = serializers.CharField(max_length=2)
    lat = serializers.DecimalField(max_digits=10, decimal_places=7)
    lng = serializers.DecimalField(max_digits=10, decimal_places=7)

    def validate_country_code(self, value):
        from core.countries import is_valid_country_code, normalize_country_code

        code = normalize_country_code(value)
        if not code or not is_valid_country_code(code):
            raise serializers.ValidationError('Must be a valid ISO 3166-1 alpha-2 country code.')
        return code

    def validate_lat(self, value):
        lat = float(value)
        if lat < -90 or lat > 90:
            raise serializers.ValidationError('Latitude must be between -90 and 90.')
        return value

    def validate_lng(self, value):
        lng = float(value)
        if lng < -180 or lng > 180:
            raise serializers.ValidationError('Longitude must be between -180 and 180.')
        return value


class ShipmentPriceEstimateSerializer(serializers.Serializer):
    """Preview suggested price without creating a shipment."""

    pickup_lat = serializers.DecimalField(max_digits=10, decimal_places=7)
    pickup_lon = serializers.DecimalField(max_digits=10, decimal_places=7)
    delivery_lat = serializers.DecimalField(max_digits=10, decimal_places=7)
    delivery_lon = serializers.DecimalField(max_digits=10, decimal_places=7)
    route_coords = serializers.JSONField(
        required=False,
        help_text='Optional driven polyline [[lng, lat], ...]. If omitted, a 2-point pickup→delivery line is used.',
    )

    def validate_pickup_lat(self, value):
        return self._validate_lat(value)

    def validate_delivery_lat(self, value):
        return self._validate_lat(value)

    def validate_pickup_lon(self, value):
        return self._validate_lng(value)

    def validate_delivery_lon(self, value):
        return self._validate_lng(value)

    def _validate_lat(self, value):
        lat = float(value)
        if lat < -90 or lat > 90:
            raise serializers.ValidationError('Latitude must be between -90 and 90.')
        return value

    def _validate_lng(self, value):
        lng = float(value)
        if lng < -180 or lng > 180:
            raise serializers.ValidationError('Longitude must be between -180 and 180.')
        return value

    def validate_route_coords(self, value):
        if value is None:
            return None
        return self._normalize_route_coords(value)

    def _normalize_route_coords(self, raw):
        if not isinstance(raw, list):
            raise serializers.ValidationError('Must be a list of [lng, lat] points.')
        points = []
        for idx, item in enumerate(raw):
            try:
                if isinstance(item, (list, tuple)) and len(item) >= 2:
                    lng, lat = float(item[0]), float(item[1])
                elif isinstance(item, dict):
                    lng = float(item.get('lng', item.get('lon', item.get('longitude'))))
                    lat = float(item.get('lat', item.get('latitude')))
                else:
                    raise TypeError
            except (TypeError, ValueError, KeyError):
                raise serializers.ValidationError(
                    f'Invalid coordinate at index {idx}; expected [lng, lat].'
                )
            if not (-180 <= lng <= 180 and -90 <= lat <= 90):
                raise serializers.ValidationError(f'Coordinate out of range at index {idx}.')
            points.append((lng, lat))
        if len(points) < 2:
            raise serializers.ValidationError('Provide at least two coordinate points.')
        return points

    def estimate(self):
        from core.pricing.service import estimate_suggested_price

        data = self.validated_data
        route_coords = data.get('route_coords')
        if not route_coords:
            route_coords = [
                (float(data['pickup_lon']), float(data['pickup_lat'])),
                (float(data['delivery_lon']), float(data['delivery_lat'])),
            ]
        return estimate_suggested_price(
            data['pickup_lat'],
            data['pickup_lon'],
            data['delivery_lat'],
            data['delivery_lon'],
            route_coords,
        )


class ShipmentCreateUpdateSerializer(serializers.ModelSerializer):
    luggage_image = Base64ImageField(required=False, write_only=True, allow_null=True)
    # Full driven polyline from the client routing provider: [(lng, lat), ...].
    # Server has no directions API; if omitted, a 2-point pickup→delivery line is used.
    route_coords = serializers.JSONField(required=False, write_only=True)

    class Meta:
        model = Shipment
        fields = [
            'pickup_address', 'pickup_company', 'pickup_lat', 'pickup_lon',
            'pickup_country_code',
            'delivery_address', 'dropoff_company', 'delivery_lat', 'delivery_lon',
            'delivery_country_code',
            'cargo_type', 'weight', 'dimensions', 'vehicle_type_required',
            'special_instructions', 'luggage_image', 'distance_km', 'suggested_price',
            'pickup_scheduled_at', 'is_return',
            'country_to_country', 'local',
            'route_coords',
        ]

    def validate(self, data):
        if self.instance is not None:
            if 'country_to_country' not in data or 'local' not in data:
                raise serializers.ValidationError(
                    'Both country_to_country and local are required when updating a shipment.'
                )
        validate_load_type_xor(data['country_to_country'], data['local'])
        if 'route_coords' in data and data['route_coords'] is not None:
            data['route_coords'] = self._normalize_route_coords(data['route_coords'])
        return data

    def _normalize_route_coords(self, raw):
        if not isinstance(raw, list):
            raise serializers.ValidationError({'route_coords': 'Must be a list of [lng, lat] points.'})
        points = []
        for idx, item in enumerate(raw):
            try:
                if isinstance(item, (list, tuple)) and len(item) >= 2:
                    lng, lat = float(item[0]), float(item[1])
                elif isinstance(item, dict):
                    lng = float(item.get('lng', item.get('lon', item.get('longitude'))))
                    lat = float(item.get('lat', item.get('latitude')))
                else:
                    raise TypeError
            except (TypeError, ValueError, KeyError):
                raise serializers.ValidationError(
                    {'route_coords': f'Invalid coordinate at index {idx}; expected [lng, lat].'}
                )
            if not (-180 <= lng <= 180 and -90 <= lat <= 90):
                raise serializers.ValidationError(
                    {'route_coords': f'Coordinate out of range at index {idx}.'}
                )
            points.append((lng, lat))
        if len(points) < 2:
            raise serializers.ValidationError(
                {'route_coords': 'Provide at least two coordinate points.'}
            )
        return points

    def _apply_luggage_image(self, shipment, content_file):
        if not content_file:
            return shipment
        shipment.luggage_image_url = save_luggage_image_and_get_url(
            content_file,
            self.context.get('request'),
        )
        shipment.save(update_fields=['luggage_image_url', 'updated_at'])
        return shipment

    def validate_pickup_country_code(self, value):
        return self._validate_country_code(value, operational=True)

    def validate_delivery_country_code(self, value):
        return self._validate_country_code(value, operational=True)

    def _validate_country_code(self, value, *, operational=False):
        from core.countries import is_valid_country_code, normalize_country_code, operational_region_error_for_country

        if value in (None, ''):
            return value
        code = normalize_country_code(value)
        if not code or not is_valid_country_code(code):
            raise serializers.ValidationError('Must be a valid ISO 3166-1 alpha-2 country code.')
        if operational:
            region_error = operational_region_error_for_country(code)
            if region_error:
                raise serializers.ValidationError(region_error)
        return code

    def _fill_country_codes_from_coords(self, shipment):
        from core.geocoding import maybe_fill_country_code_from_coords

        updated_fields = []
        for field, lat_field, lon_field in (
            ('pickup_country_code', 'pickup_lat', 'pickup_lon'),
            ('delivery_country_code', 'delivery_lat', 'delivery_lon'),
        ):
            if maybe_fill_country_code_from_coords(
                shipment,
                field=field,
                lat_field=lat_field,
                lon_field=lon_field,
            ):
                updated_fields.append(field)
        if updated_fields:
            shipment.save(update_fields=[*updated_fields, 'updated_at'])
        return shipment

    def _apply_pickup_currency(self, shipment):
        from core.currencies import currency_code_for_country
        from core.pricing.service import country_code_for_point

        pickup = shipment.pickup_country_code
        if not pickup and shipment.pickup_lat is not None and shipment.pickup_lon is not None:
            pickup = country_code_for_point(shipment.pickup_lat, shipment.pickup_lon)
        shipment.currency = currency_code_for_country(pickup)
        shipment.save(update_fields=['currency', 'updated_at'])
        return shipment

    def _ensure_operational_country_codes(self, shipment):
        from core.countries import operational_region_error_for_country

        for field in ('pickup_country_code', 'delivery_country_code'):
            message = operational_region_error_for_country(getattr(shipment, field))
            if message:
                raise serializers.ValidationError(message)

    def _ensure_return_trip_allowed(self, shipment):
        if not shipment.is_return:
            return
        from core.countries import return_trip_error_for_pickup_country

        message = return_trip_error_for_pickup_country(shipment.pickup_country_code)
        if message:
            raise serializers.ValidationError(message)

    def _route_coords_for_pricing(self, shipment, route_coords):
        """Prefer client polyline; else 2-point pickup→delivery (lng, lat)."""
        if route_coords and len(route_coords) >= 2:
            return route_coords
        if (
            shipment.pickup_lat is None
            or shipment.pickup_lon is None
            or shipment.delivery_lat is None
            or shipment.delivery_lon is None
        ):
            return None
        return [
            (float(shipment.pickup_lon), float(shipment.pickup_lat)),
            (float(shipment.delivery_lon), float(shipment.delivery_lat)),
        ]

    def create(self, validated_data):
        from django.db import transaction

        luggage_file = validated_data.pop('luggage_image', None)
        validated_data.pop('route_coords', None)
        validated_data.pop('currency', None)

        with transaction.atomic():
            shipment = super().create(validated_data)
            shipment = self._fill_country_codes_from_coords(shipment)
            self._ensure_operational_country_codes(shipment)
            self._ensure_return_trip_allowed(shipment)
            shipment = self._apply_pickup_currency(shipment)
            shipment = self._apply_luggage_image(shipment, luggage_file)
            return shipment

    def update(self, instance, validated_data):
        from django.db import transaction

        luggage_file = validated_data.pop('luggage_image', None)
        validated_data.pop('route_coords', None)
        validated_data.pop('currency', None)

        with transaction.atomic():
            shipment = super().update(instance, validated_data)
            shipment = self._fill_country_codes_from_coords(shipment)
            self._ensure_operational_country_codes(shipment)
            self._ensure_return_trip_allowed(shipment)
            shipment = self._apply_pickup_currency(shipment)
            shipment = self._apply_luggage_image(shipment, luggage_file)
            return shipment


def get_shipper_favorite_transporter_ids(request):
    """Cached set of favorited transporter user IDs for the current request's shipper."""
    if not request:
        return set()
    user = getattr(request, 'user', None)
    if not user or not getattr(user, 'is_authenticated', False):
        return set()
    if not hasattr(request, '_favorite_transporter_ids_cache'):
        request._favorite_transporter_ids_cache = set(
            FavoriteTransporter.objects.filter(shipper=user).values_list('transporter_id', flat=True)
        )
    return request._favorite_transporter_ids_cache


# ---- Shipper 3: Bids ----
class BidListSerializer(serializers.ModelSerializer):
    transporter_email = serializers.EmailField(source='transporter.email', read_only=True)
    transporter_name = serializers.SerializerMethodField()
    currency = serializers.SerializerMethodField()
    is_favourite = serializers.SerializerMethodField()
    is_favorite = serializers.SerializerMethodField()

    class Meta:
        model = Bid
        fields = [
            'id', 'transporter', 'transporter_email', 'transporter_name', 'is_favourite', 'is_favorite',
            'amount', 'currency', 'status', 'counter_amount', 'message', 'created_at',
        ]

    def get_transporter_name(self, obj):
        return f'{obj.transporter.first_name} {obj.transporter.last_name}'.strip() or obj.transporter.email

    def get_currency(self, obj):
        return currency_for_shipment(getattr(obj, 'shipment', None))

    def get_is_favourite(self, obj):
        fav_ids = get_shipper_favorite_transporter_ids(self.context.get('request'))
        if not fav_ids:
            return False
        return bool(getattr(obj, 'transporter_id', None) in fav_ids)

    def get_is_favorite(self, obj):
        return self.get_is_favourite(obj)


class BidDetailSerializer(BidListSerializer):
    class Meta(BidListSerializer.Meta):
        fields = BidListSerializer.Meta.fields


class BidAcceptSerializer(serializers.Serializer):
    pass


class BidCounterSerializer(serializers.Serializer):
    counter_amount = serializers.DecimalField(max_digits=12, decimal_places=2)
    message = serializers.CharField(required=False, allow_blank=True)


class TransporterBidSubmitSerializer(serializers.Serializer):
    ACTION_ACCEPT = 'ACCEPT'
    ACTION_COUNTER = 'COUNTER'
    ACTION_CHOICES = [
        (ACTION_ACCEPT, 'Accept shipper proposed rate'),
        (ACTION_COUNTER, 'Submit counter-offer'),
    ]

    action = serializers.ChoiceField(choices=ACTION_CHOICES)
    amount = serializers.DecimalField(max_digits=12, decimal_places=2, required=False)
    message = serializers.CharField(required=False, allow_blank=True)

    def validate(self, attrs):
        action = attrs.get('action')
        amount = attrs.get('amount')
        if action == self.ACTION_COUNTER and amount is None:
            raise serializers.ValidationError({'amount': 'amount is required for counter-offer.'})
        return attrs


# ---- Shipper 4: Trips ----
class TripLocationSerializer(serializers.ModelSerializer):
    class Meta:
        model = TripLocation
        fields = ['id', 'lat', 'lon', 'recorded_at']


class TripStatusRecordSerializer(serializers.ModelSerializer):
    class Meta:
        model = TripStatusRecord
        fields = ['status', 'recorded_at']


# ---- Shipper 4: Trips ----
class TripUniqueIdMixin:
    unique_id = serializers.SerializerMethodField()

    def get_unique_id(self, obj):
        shipment = getattr(obj, 'shipment', None)
        if shipment is not None:
            return shipment.unique_id
        if obj.shipment_id:
            return (
                Shipment.objects.filter(pk=obj.shipment_id)
                .values_list('unique_id', flat=True)
                .first()
            )
        return None


class TripListSerializer(TripUniqueIdMixin, serializers.ModelSerializer):
    shipment_id = serializers.PrimaryKeyRelatedField(source='shipment', read_only=True)
    pickup_company = serializers.CharField(source='shipment.pickup_company', read_only=True)
    dropoff_company = serializers.CharField(source='shipment.dropoff_company', read_only=True)
    currency = serializers.SerializerMethodField()
    is_return = serializers.BooleanField(source='shipment.is_return', read_only=True)
    status = serializers.CharField(read_only=True)
    status_timeline = TripStatusRecordSerializer(source='status_records', many=True, read_only=True)
    assigned_driver_id = serializers.IntegerField(source='assigned_driver.id', read_only=True)
    shipper_rating = serializers.SerializerMethodField()
    pod = serializers.SerializerMethodField()
    pod_url = serializers.SerializerMethodField()
    pod_urls = serializers.SerializerMethodField()
    is_favourite = serializers.SerializerMethodField()
    is_favorite = serializers.SerializerMethodField()

    class Meta:
        model = Trip
        fields = [
            'id', 'unique_id', 'shipment_id', 'pickup_company', 'dropoff_company',
            'currency', 'agreed_price', 'is_return', 'status', 'status_timeline',
            'transporter', 'assigned_driver_id', 'is_favourite', 'is_favorite', 'shipper_rating',
            'pod', 'pod_url', 'pod_urls',
            'current_lat', 'current_lon', 'eta', 'created_at', 'updated_at',
        ]

    def get_is_favourite(self, obj):
        fav_ids = get_shipper_favorite_transporter_ids(self.context.get('request'))
        if not fav_ids:
            return False
        return bool(
            (getattr(obj, 'transporter_id', None) in fav_ids)
            or (getattr(obj, 'assigned_driver_id', None) in fav_ids)
        )

    def get_is_favorite(self, obj):
        return self.get_is_favourite(obj)

    def get_shipper_rating(self, obj):
        return hasattr(obj, 'shipper_review')

    def get_currency(self, obj):
        return currency_for_shipment(getattr(obj, 'shipment', None))

    def _get_pod_obj(self, obj):
        try:
            return getattr(obj, 'pod', None)
        except Exception:
            return None

    def get_pod(self, obj):
        pod = self._get_pod_obj(obj)
        if pod:
            return serialize_pod_summary(pod, request=self.context.get('request'))
        return None

    def get_pod_url(self, obj):
        pod = self._get_pod_obj(obj)
        if not pod:
            return None
        photos = getattr(pod, 'photos', None)
        first = photos.all().first() if photos else None
        if first and first.image:
            return build_absolute_media_url(self.context.get('request'), first.image.url)
        return None

    def get_pod_urls(self, obj):
        pod = self._get_pod_obj(obj)
        if not pod:
            return []
        photos = getattr(pod, 'photos', None)
        if not photos:
            return []
        urls = []
        for p in photos.all():
            if p.image:
                url = build_absolute_media_url(self.context.get('request'), p.image.url)
                if url:
                    urls.append(url)
        return urls



class ShipperTripListSerializer(TripListSerializer):
    """Shipper trip list with pickup/dropoff, transporter, and driver details."""

    transporter = serializers.SerializerMethodField()
    assigned_driver = serializers.SerializerMethodField()
    individual_driver = serializers.SerializerMethodField()
    assigned_vehicle = serializers.SerializerMethodField()
    pickup = serializers.SerializerMethodField()
    dropoff = serializers.SerializerMethodField()
    pickup_scheduled_at = serializers.DateTimeField(
        source='shipment.pickup_scheduled_at', read_only=True, allow_null=True,
    )
    cargo_type = serializers.CharField(source='shipment.cargo_type', read_only=True)
    weight = serializers.CharField(source='shipment.weight', read_only=True)
    vehicle_type_required = serializers.CharField(
        source='shipment.vehicle_type_required', read_only=True,
    )
    distance_km = serializers.DecimalField(
        source='shipment.distance_km', max_digits=12, decimal_places=2,
        read_only=True, allow_null=True,
    )

    class Meta(TripListSerializer.Meta):
        fields = list(TripListSerializer.Meta.fields) + [
            'pickup', 'dropoff', 'pickup_scheduled_at',
            'cargo_type', 'weight', 'vehicle_type_required', 'distance_km',
            'assigned_driver', 'individual_driver', 'assigned_vehicle',
        ]

    def get_shipper_rating(self, obj):
        return hasattr(obj, 'review')

    def get_transporter(self, obj):
        return transporter_party_summary(
            getattr(obj, 'transporter', None),
            self.context.get('request'),
        )

    def get_assigned_driver(self, obj):
        return transporter_party_summary(
            getattr(obj, 'assigned_driver', None),
            self.context.get('request'),
        )

    def get_individual_driver(self, obj):
        """Executing driver: assigned_driver when set, else individual transporter."""
        assigned = getattr(obj, 'assigned_driver', None)
        if assigned:
            return transporter_party_summary(assigned, self.context.get('request'))
        transporter = getattr(obj, 'transporter', None)
        profile = getattr(transporter, 'transporter_profile', None) if transporter else None
        account_type = getattr(profile, 'account_type', None)
        if account_type in (
            TransporterProfile.AccountType.DRIVER,
            TransporterProfile.AccountType.TRANSPORTER_DRIVER,
        ):
            return transporter_party_summary(transporter, self.context.get('request'))
        return None

    def get_assigned_vehicle(self, obj):
        cache = self.context.get('assigned_vehicles_by_trip_id')
        if isinstance(cache, dict):
            vehicle = cache.get(obj.id)
        else:
            vehicle = resolve_trip_assigned_vehicle(obj)
        return vehicle_public_summary(vehicle)

    def get_pickup(self, obj):
        return trip_pickup_summary(getattr(obj, 'shipment', None))

    def get_dropoff(self, obj):
        return trip_dropoff_summary(getattr(obj, 'shipment', None))


def resolve_trips_assigned_vehicles(trips) -> dict:
    """Map trip.id -> Vehicle with a single query (avoids N+1 on trip lists)."""
    trip_list = list(trips)
    if not trip_list:
        return {}

    owner_ids = {t.transporter_id for t in trip_list if t.transporter_id}
    if not owner_ids:
        return {}

    vehicles = list(
        Vehicle.objects.filter(owner_id__in=owner_ids)
        .only(
            'id',
            'owner_id',
            'assigned_driver_id',
            'vehicle_type',
            'vehicle_types',
            'registration_number',
            'load_capacity',
            'avatar_url',
            'is_verified',
            'is_active',
            'updated_at',
        )
        .order_by('-updated_at')
    )
    by_owner_driver = {}
    by_owner_unassigned = {}
    for vehicle in vehicles:
        key = (vehicle.owner_id, vehicle.assigned_driver_id)
        if key not in by_owner_driver:
            by_owner_driver[key] = vehicle
        if vehicle.assigned_driver_id is None and vehicle.owner_id not in by_owner_unassigned:
            by_owner_unassigned[vehicle.owner_id] = vehicle

    result = {}
    for trip in trip_list:
        driver = getattr(trip, 'assigned_driver', None) or getattr(trip, 'transporter', None)
        transporter_id = getattr(trip, 'transporter_id', None)
        if not driver or not transporter_id:
            continue
        vehicle = by_owner_driver.get((transporter_id, driver.id))
        if vehicle is None and driver.id == transporter_id:
            vehicle = by_owner_unassigned.get(transporter_id)
        if vehicle is not None:
            result[trip.id] = vehicle
    return result


def resolve_trip_assigned_vehicle(trip):
    """Vehicle assigned to the trip's executing driver (fleet or individual)."""
    return resolve_trips_assigned_vehicles([trip]).get(getattr(trip, 'id', None))


def vehicle_public_summary(vehicle):
    """Compact vehicle card for trip list/detail (avoids heavy VehicleSerializer)."""
    if not vehicle:
        return None
    types = vehicle.type_list() if hasattr(vehicle, 'type_list') else []
    return {
        'id': vehicle.id,
        'registration_number': vehicle.registration_number,
        'vehicle_type': vehicle.vehicle_type or (types[0] if types else ''),
        'vehicle_types': types,
        'avatar_url': (vehicle.avatar_url or '').strip(),
        'load_capacity': vehicle.load_capacity,
        'is_verified': vehicle.is_verified,
        'is_active': vehicle.is_active,
        'assigned_driver_id': vehicle.assigned_driver_id,
    }

class TripDetailSerializer(TripUniqueIdMixin, serializers.ModelSerializer):
    shipment = ShipmentSerializer(read_only=True)
    is_return = serializers.BooleanField(source='shipment.is_return', read_only=True)
    currency = serializers.SerializerMethodField()
    accepted_bid = BidListSerializer(read_only=True)
    status_timeline = TripStatusRecordSerializer(source='status_records', many=True, read_only=True)
    shipper_rating = serializers.SerializerMethodField()
    pod = serializers.SerializerMethodField()
    pod_url = serializers.SerializerMethodField()
    pod_urls = serializers.SerializerMethodField()
    is_favourite = serializers.SerializerMethodField()
    is_favorite = serializers.SerializerMethodField()

    class Meta:
        model = Trip
        fields = [
            'id', 'unique_id', 'shipment', 'is_return', 'currency', 'agreed_price', 'accepted_bid', 'transporter', 'is_favourite', 'is_favorite', 'status', 'status_timeline',
            'shipper_rating', 'pod', 'pod_url', 'pod_urls', 'current_lat', 'current_lon', 'eta', 'created_at', 'updated_at',
        ]

    def get_is_favourite(self, obj):
        fav_ids = get_shipper_favorite_transporter_ids(self.context.get('request'))
        if not fav_ids:
            return False
        return bool(
            (getattr(obj, 'transporter_id', None) in fav_ids)
            or (getattr(obj, 'assigned_driver_id', None) in fav_ids)
        )

    def get_is_favorite(self, obj):
        return self.get_is_favourite(obj)

    def get_shipper_rating(self, obj):
        return hasattr(obj, 'shipper_review')

    def get_currency(self, obj):
        return currency_for_shipment(getattr(obj, 'shipment', None))

    def _get_pod_obj(self, obj):
        try:
            return getattr(obj, 'pod', None)
        except Exception:
            return None

    def get_pod(self, obj):
        pod = self._get_pod_obj(obj)
        if pod:
            return serialize_pod_summary(pod, request=self.context.get('request'))
        return None

    def get_pod_url(self, obj):
        pod = self._get_pod_obj(obj)
        if not pod:
            return None
        photos = getattr(pod, 'photos', None)
        first = photos.all().first() if photos else None
        if first and first.image:
            return build_absolute_media_url(self.context.get('request'), first.image.url)
        return None

    def get_pod_urls(self, obj):
        pod = self._get_pod_obj(obj)
        if not pod:
            return []
        photos = getattr(pod, 'photos', None)
        if not photos:
            return []
        urls = []
        for p in photos.all():
            if p.image:
                url = build_absolute_media_url(self.context.get('request'), p.image.url)
                if url:
                    urls.append(url)
        return urls



class ShipperTripDetailSerializer(TripDetailSerializer):
    """Shipper trip detail: shipper_rating = whether the shipper submitted a trip review."""
    transporter = serializers.SerializerMethodField()
    assigned_driver = serializers.SerializerMethodField()
    individual_driver = serializers.SerializerMethodField()
    assigned_vehicle = serializers.SerializerMethodField()
    pickup = serializers.SerializerMethodField()
    dropoff = serializers.SerializerMethodField()
    pickup_scheduled_at = serializers.DateTimeField(
        source='shipment.pickup_scheduled_at', read_only=True, allow_null=True,
    )

    class Meta(TripDetailSerializer.Meta):
        fields = list(TripDetailSerializer.Meta.fields) + [
            'pickup', 'dropoff', 'pickup_scheduled_at',
            'assigned_driver', 'individual_driver', 'assigned_vehicle',
        ]

    def get_shipper_rating(self, obj):
        return hasattr(obj, 'review')

    def get_transporter(self, obj):
        return transporter_party_summary(
            getattr(obj, 'transporter', None),
            self.context.get('request'),
        )

    def get_assigned_driver(self, obj):
        return transporter_party_summary(
            getattr(obj, 'assigned_driver', None),
            self.context.get('request'),
        )

    def get_individual_driver(self, obj):
        assigned = getattr(obj, 'assigned_driver', None)
        if assigned:
            return transporter_party_summary(assigned, self.context.get('request'))
        transporter = getattr(obj, 'transporter', None)
        profile = getattr(transporter, 'transporter_profile', None) if transporter else None
        account_type = getattr(profile, 'account_type', None)
        if account_type in (
            TransporterProfile.AccountType.DRIVER,
            TransporterProfile.AccountType.TRANSPORTER_DRIVER,
        ):
            return transporter_party_summary(transporter, self.context.get('request'))
        return None

    def get_assigned_vehicle(self, obj):
        cache = self.context.get('assigned_vehicles_by_trip_id')
        if isinstance(cache, dict):
            vehicle = cache.get(obj.id)
        else:
            vehicle = resolve_trip_assigned_vehicle(obj)
        return vehicle_public_summary(vehicle)

    def get_pickup(self, obj):
        return trip_pickup_summary(getattr(obj, 'shipment', None))

    def get_dropoff(self, obj):
        return trip_dropoff_summary(getattr(obj, 'shipment', None))


class TransporterTripDetailSerializer(TripDetailSerializer):
    shipper = serializers.SerializerMethodField()
    assigned_driver = serializers.SerializerMethodField()
    assigned_vehicle = serializers.SerializerMethodField()
    tc_id = serializers.SerializerMethodField()
    tc_u_id = serializers.SerializerMethodField()

    class Meta(TripDetailSerializer.Meta):
        fields = list(TripDetailSerializer.Meta.fields) + [
            'shipper', 'assigned_driver', 'assigned_vehicle', 'tc_id', 'tc_u_id',
        ]

    def get_shipper(self, obj):
        shipper = getattr(getattr(obj, 'shipment', None), 'shipper', None)
        return shipper_public_summary(shipper)

    def get_assigned_driver(self, obj):
        d = getattr(obj, 'assigned_driver', None)
        if not d:
            return None
        role = getattr(d, 'role', None)
        profile = getattr(d, 'transporter_profile', None)
        fav_ids = get_shipper_favorite_transporter_ids(self.context.get('request'))
        return {
            'id': d.id,
            'email': d.email,
            'first_name': d.first_name,
            'last_name': d.last_name,
            'phone': getattr(role, 'phone', '') or '',
            'account_type': getattr(profile, 'account_type', None),
            'company_name': getattr(profile, 'company_name', '') or '',
            'is_favourite': d.id in fav_ids,
            'is_favorite': d.id in fav_ids,
        }

    def get_assigned_vehicle(self, obj):
        vehicle = resolve_trip_assigned_vehicle(obj)
        if not vehicle:
            return None
        return VehicleSerializer(vehicle, context=self.context).data

    def get_tc_id(self, obj):
        try:
            v = obj.transporter.transporter_profile.tc_id
            return str(v) if v else None
        except Exception:
            return None

    def get_tc_u_id(self, obj):
        try:
            v = obj.transporter.transporter_profile.tc_u_id
            return v or None
        except Exception:
            return None


class TransporterCompletedTripSerializer(TransporterTripDetailSerializer):
    """Completed-trip list: includes settlement summary for payment UI."""

    payment = serializers.SerializerMethodField()

    class Meta(TransporterTripDetailSerializer.Meta):
        fields = list(TransporterTripDetailSerializer.Meta.fields) + ['payment']

    def get_payment(self, obj):
        return trip_payment_settlement_for_transporter(obj)


# ---- Shipper 5: Messages ----
class MessageSerializer(serializers.ModelSerializer):
    sender_email = serializers.EmailField(source='sender.email', read_only=True)
    sender_name = serializers.SerializerMethodField()
    sender_account_type = serializers.SerializerMethodField()
    voice_url = serializers.SerializerMethodField()
    conversation_id = serializers.IntegerField(source='conversation.id', read_only=True)
    shipment_id = serializers.IntegerField(source='conversation.shipment_id', read_only=True)
    trip_id = serializers.IntegerField(source='conversation.trip_id', read_only=True)

    class Meta:
        model = Message
        fields = [
            'id',
            'conversation_id',
            'shipment_id',
            'trip_id',
            'sender',
            'sender_email',
            'sender_name',
            'sender_account_type',
            'text',
            'message_type',
            'voice_file',
            'voice_url',
            'created_at',
        ]
        read_only_fields = ['sender', 'created_at']

    def get_sender_name(self, obj):
        sender = obj.sender
        if not sender:
            return ''
        name = f'{sender.first_name} {sender.last_name}'.strip()
        return name or (sender.email or '')

    def get_sender_account_type(self, obj):
        sender = obj.sender
        if not sender:
            return None
        shipper_profile = getattr(sender, 'shipper_profile', None)
        if shipper_profile is not None:
            return shipper_profile.account_type
        transporter_profile = getattr(sender, 'transporter_profile', None)
        if transporter_profile is not None:
            return transporter_profile.account_type
        return None

    def get_voice_url(self, obj):
        if obj.voice_file:
            return build_absolute_media_url(self.context.get('request'), obj.voice_file.url)
        return None


class MessageCreateSerializer(serializers.ModelSerializer):
    class Meta:
        model = Message
        fields = ['text', 'message_type', 'voice_file']

    def validate(self, attrs):
        msg_type = attrs.get('message_type') or Message.MessageType.TEXT
        text = (attrs.get('text') or '').strip()
        voice = attrs.get('voice_file')

        if msg_type == Message.MessageType.VOICE:
            if not voice:
                raise serializers.ValidationError(
                    {'voice_file': 'Voice file is required for VOICE messages.'}
                )
            attrs['text'] = text
        else:
            if not text:
                raise serializers.ValidationError(
                    {'text': 'Text is required for TEXT messages.'}
                )
            if voice:
                raise serializers.ValidationError(
                    {'voice_file': 'Voice file is not allowed for TEXT messages.'}
                )
            attrs['text'] = text
        return attrs


# ---- Admin: Conversation Mirroring Serializers ----
class AdminConversationListSerializer(serializers.ModelSerializer):
    shipment_unique_id = serializers.CharField(source='shipment.unique_id', read_only=True, default=None)
    pickup_address = serializers.CharField(source='shipment.pickup_address', read_only=True, default='')
    delivery_address = serializers.CharField(source='shipment.delivery_address', read_only=True, default='')
    cargo_type = serializers.CharField(source='shipment.cargo_type', read_only=True, default='')
    trip_status = serializers.CharField(source='trip.status', read_only=True, default=None)
    shipper_email = serializers.EmailField(source='shipper.email', read_only=True, default='')
    shipper_name = serializers.SerializerMethodField()
    transporter_email = serializers.EmailField(source='transporter.email', read_only=True, default='')
    transporter_name = serializers.SerializerMethodField()
    assigned_driver = serializers.SerializerMethodField()
    messages_count = serializers.SerializerMethodField()
    latest_message = serializers.SerializerMethodField()

    class Meta:
        model = Conversation
        fields = [
            'id',
            'shipment',
            'shipment_unique_id',
            'pickup_address',
            'delivery_address',
            'cargo_type',
            'trip',
            'trip_status',
            'shipper',
            'shipper_email',
            'shipper_name',
            'transporter',
            'transporter_email',
            'transporter_name',
            'assigned_driver',
            'messages_count',
            'latest_message',
            'created_at',
        ]

    def get_shipper_name(self, obj):
        if not obj.shipper:
            return ''
        name = f'{obj.shipper.first_name} {obj.shipper.last_name}'.strip()
        return name or obj.shipper.email

    def get_transporter_name(self, obj):
        if not obj.transporter:
            return ''
        tp = getattr(obj.transporter, 'transporter_profile', None)
        if tp and tp.company_name:
            return tp.company_name
        name = f'{obj.transporter.first_name} {obj.transporter.last_name}'.strip()
        return name or obj.transporter.email

    def get_assigned_driver(self, obj):
        if not obj.trip or not obj.trip.assigned_driver:
            return None
        d = obj.trip.assigned_driver
        name = f'{d.first_name} {d.last_name}'.strip() or d.email
        return {'id': d.id, 'email': d.email, 'name': name}

    def get_messages_count(self, obj):
        return obj.messages.count()

    def get_latest_message(self, obj):
        msg = obj.messages.order_by('-created_at').first()
        if not msg:
            return None
        return {
            'id': msg.id,
            'sender_id': msg.sender_id,
            'message_type': msg.message_type,
            'text': msg.text,
            'created_at': msg.created_at,
        }


class AdminConversationDetailSerializer(AdminConversationListSerializer):
    messages = MessageSerializer(many=True, read_only=True)

    class Meta(AdminConversationListSerializer.Meta):
        fields = AdminConversationListSerializer.Meta.fields + ['messages']


# ---- Shipper 6: POD ----
class PODPhotoSerializer(serializers.ModelSerializer):
    image_url = serializers.SerializerMethodField()

    class Meta:
        model = PODPhoto
        fields = ['id', 'image', 'image_url', 'created_at']

    def get_image_url(self, obj):
        if obj.image:
            return build_absolute_media_url(self.context.get('request'), obj.image.url)
        return None


class PODSerializer(serializers.ModelSerializer):
    photos = PODPhotoSerializer(many=True, read_only=True)
    signature_url = serializers.SerializerMethodField()

    class Meta:
        model = ProofOfDelivery
        fields = ['id', 'receiver_name', 'receiver_signature', 'signature_url', 'delivery_lat', 'delivery_lon', 'delivered_at', 'photos', 'created_at']

    def get_signature_url(self, obj):
        if obj.receiver_signature:
            return build_absolute_media_url(self.context.get('request'), obj.receiver_signature.url)
        return None


def serialize_pod_summary(pod, request=None):
    """Serialize POD with photos and absolute media URLs."""
    if not pod:
        return None
    photos_list = []
    try:
        photos = pod.photos.all()
    except Exception:
        photos = []
    for p in photos:
        url = build_absolute_media_url(request, p.image.url) if p.image else None
        photos_list.append({
            'id': p.id,
            'image': p.image.url if p.image else None,
            'image_url': url,
            'url': url,
            'created_at': p.created_at,
        })
    sig_url = build_absolute_media_url(request, pod.receiver_signature.url) if pod.receiver_signature else None
    first_photo_url = photos_list[0]['image_url'] if photos_list else None
    return {
        'id': pod.id,
        'receiver_name': pod.receiver_name,
        'receiver_signature': pod.receiver_signature.url if pod.receiver_signature else None,
        'signature_url': sig_url,
        'delivery_lat': pod.delivery_lat,
        'delivery_lon': pod.delivery_lon,
        'delivered_at': pod.delivered_at,
        'photos': photos_list,
        'pod_url': first_photo_url,
        'pod_urls': [p['image_url'] for p in photos_list if p['image_url']],
        'created_at': pod.created_at,
    }



# ---- Billing & engagement (shipper 7–10) ----
class WalletSerializer(serializers.ModelSerializer):
    class Meta:
        model = WalletAccount
        fields = ['balance', 'currency', 'updated_at']
        read_only_fields = fields


class PaymentSerializer(serializers.ModelSerializer):
    class Meta:
        model = Payment
        fields = [
            'id', 'trip', 'payer', 'payee', 'method', 'amount', 'currency', 'status',
            'provider_ref', 'metadata', 'created_at', 'updated_at',
        ]
        read_only_fields = fields


class InvoiceSerializer(serializers.ModelSerializer):
    currency = serializers.SerializerMethodField()

    class Meta:
        model = Invoice
        fields = [
            'id', 'number', 'trip', 'shipper', 'line_items', 'tax', 'total', 'currency', 'status',
            'payment', 'is_archived', 'archived_at', 'due_date', 'issued_at', 'created_at',
        ]
        read_only_fields = fields

    def get_currency(self, obj):
        payment = getattr(obj, 'payment', None)
        if payment is not None:
            return payment.currency
        trip = getattr(obj, 'trip', None)
        shipment = getattr(trip, 'shipment', None) if trip is not None else None
        return currency_for_shipment(shipment)


class LedgerEntrySerializer(serializers.ModelSerializer):
    class Meta:
        model = LedgerEntry
        fields = [
            'id', 'amount', 'entry_type', 'trip', 'payment', 'note', 'idempotency_key', 'created_at',
        ]
        read_only_fields = fields


class WithdrawalRequestSerializer(serializers.ModelSerializer):
    class Meta:
        model = WithdrawalRequest
        fields = ['id', 'amount', 'currency', 'status', 'note', 'created_at', 'updated_at']
        read_only_fields = fields


class TripPaySerializer(serializers.Serializer):
    """Wallet, card (driver confirms), COD, or credit terms."""
    method = serializers.ChoiceField(
        choices=[
            (Payment.Method.WALLET, 'Wallet'),
            (Payment.Method.CARD, 'Card'),
            (Payment.Method.CASH, 'Cash'),
            (Payment.Method.CREDIT, 'Credit terms'),
        ]
    )


class TripReviewCreateSerializer(serializers.Serializer):
    transporter_rating = serializers.IntegerField(min_value=1, max_value=5)
    driver_rating = serializers.IntegerField(required=False, allow_null=True, min_value=1, max_value=5)
    driver_id = serializers.IntegerField(required=False, allow_null=True)
    comment = serializers.CharField(required=False, allow_blank=True)


class TripReviewSerializer(serializers.ModelSerializer):
    class Meta:
        model = TripReview
        fields = [
            'id', 'trip', 'shipper', 'transporter', 'driver',
            'transporter_rating', 'driver_rating', 'comment', 'created_at',
        ]
        read_only_fields = fields


class IndividualDriverRatingSerializer(serializers.ModelSerializer):
    """Shipper review as seen by the rated individual driver."""
    rating = serializers.SerializerMethodField()
    shipper_name = serializers.SerializerMethodField()
    shipment_unique_id = serializers.CharField(
        source='trip.shipment.unique_id',
        read_only=True,
        allow_null=True,
    )

    class Meta:
        model = TripReview
        fields = [
            'id',
            'trip',
            'shipper',
            'transporter',
            'driver',
            'rating',
            'transporter_rating',
            'driver_rating',
            'comment',
            'shipper_name',
            'shipment_unique_id',
            'created_at',
        ]
        read_only_fields = fields

    def get_rating(self, obj):
        if obj.driver_rating is not None:
            return obj.driver_rating
        return obj.transporter_rating

    def get_shipper_name(self, obj):
        shipper = obj.shipper
        name = f'{shipper.first_name} {shipper.last_name}'.strip()
        return name or shipper.email


class TripShipperReviewCreateSerializer(serializers.Serializer):
    shipper_rating = serializers.IntegerField(min_value=1, max_value=5)
    comment = serializers.CharField(required=False, allow_blank=True)


class TripShipperReviewSerializer(serializers.ModelSerializer):
    class Meta:
        model = TripShipperReview
        fields = [
            'id', 'trip', 'shipper', 'transporter', 'reviewer',
            'shipper_rating', 'comment', 'created_at',
        ]
        read_only_fields = fields


class SavedAddressSerializer(serializers.ModelSerializer):
    class Meta:
        model = SavedAddress
        fields = [
            'id', 'label', 'address_line', 'lat', 'lon', 'country_code', 'is_default',
            'created_at', 'updated_at',
        ]
        read_only_fields = ['id', 'created_at', 'updated_at']


class FavoriteTransporterSerializer(serializers.ModelSerializer):
    first_name = serializers.CharField(source='transporter.first_name', read_only=True)
    last_name = serializers.CharField(source='transporter.last_name', read_only=True)

    class Meta:
        model = FavoriteTransporter
        fields = ['id', 'transporter', 'first_name', 'last_name', 'created_at']
        read_only_fields = ['id', 'created_at']


class FcmTokenSerializer(serializers.Serializer):
    fcm_token = serializers.CharField(max_length=4096)


class ShipmentPreferencesSerializer(serializers.Serializer):
    """Arbitrary JSON preferences stored on ShipperProfile.shipment_preferences."""
    shipment_preferences = serializers.JSONField()


class WithdrawalCreateSerializer(serializers.Serializer):
    amount = serializers.DecimalField(max_digits=14, decimal_places=2, min_value=Decimal('0.01'))
    note = serializers.CharField(required=False, allow_blank=True, default='')


# ---- Fleet Owner: Driver CRUD ----
def _fleet_owner_load_type_kwargs(fleet_owner):
    """Fleet drivers inherit load-type flags from their fleet owner."""
    owner_profile = fleet_owner.transporter_profile
    return {
        'country_to_country': owner_profile.country_to_country,
        'local': owner_profile.local,
    }


class FleetDriverCreateSerializer(serializers.Serializer):
    email = serializers.EmailField()
    password = serializers.CharField(write_only=True, min_length=8)
    first_name = serializers.CharField(required=False, allow_blank=True)
    last_name = serializers.CharField(required=False, allow_blank=True)
    phone = serializers.CharField(required=False, allow_blank=True)
    language = serializers.CharField(required=False, default='en', allow_blank=True)
    avatar = Base64ImageField(required=False, write_only=True, allow_null=True)
    documents = serializers.ListField(
        child=serializers.DictField(),
        required=False,
        allow_empty=True,
        help_text=(
            'Optional KYC docs (same as individual driver): DRIVER_LICENSE, '
            'PASSPORT_COPY, PERMIT, COUNTRY_GCC, optional NOC; '
            'PASSPORT_COPY requires passport_number; each may include expiry_date.'
        ),
    )

    def _avatar_url(self, avatar_file):
        if not avatar_file:
            return None
        return save_avatar_and_get_url(
            avatar_file,
            self.context.get('request'),
            kind='user',
        )

    def create(self, validated_data):
        email = validated_data['email'].strip().lower()
        fleet_owner = self.context['fleet_owner']
        avatar_file = validated_data.pop('avatar', None)
        documents = validated_data.pop('documents', None) or []
        avatar_url = self._avatar_url(avatar_file)
        request = self.context.get('request')

        if User.objects.filter(username=email).exists():
            user = User.objects.get(username=email)
            if not hasattr(user, 'role') or user.role.role != UserRole.Role.TRANSPORTER:
                raise serializers.ValidationError({'email': 'This email belongs to a non-transporter account.'})
            link, created = TransporterDriverLink.objects.get_or_create(
                transporter=fleet_owner, driver=user,
                defaults={'is_active': True},
            )
            if not created:
                if link.is_active:
                    raise serializers.ValidationError({'email': 'This driver is already linked to your fleet.'})
                link.is_active = True
                link.save(update_fields=['is_active', 'updated_at'])
            try:
                profile = user.transporter_profile
                update_fields = []
                if profile.account_type != TransporterProfile.AccountType.FLEET_OWNER:
                    profile.account_type = TransporterProfile.AccountType.TRANSPORTER_DRIVER
                    update_fields.append('account_type')
                    if profile.country_to_country == profile.local:
                        load_type = _fleet_owner_load_type_kwargs(fleet_owner)
                        profile.country_to_country = load_type['country_to_country']
                        profile.local = load_type['local']
                        update_fields.extend(['country_to_country', 'local'])
                if avatar_url is not None:
                    profile.avatar_url = avatar_url
                    update_fields.append('avatar_url')
                if update_fields:
                    update_fields.append('updated_at')
                    profile.save(update_fields=update_fields)
            except TransporterProfile.DoesNotExist:
                pass
            if documents:
                upsert_driver_kyc_documents(user, documents, request=request)
            return user

        with transaction.atomic():
            user = User.objects.create_user(
                username=email,
                email=email,
                password=validated_data['password'],
                first_name=validated_data.get('first_name') or '',
                last_name=validated_data.get('last_name') or '',
            )
            UserRole.objects.create(
                user=user,
                role=UserRole.Role.TRANSPORTER,
                phone=validated_data.get('phone') or '',
                language=validated_data.get('language') or 'en',
            )
            profile_data = {
                'user': user,
                'account_type': TransporterProfile.AccountType.TRANSPORTER_DRIVER,
                **_fleet_owner_load_type_kwargs(fleet_owner),
            }
            if avatar_url is not None:
                profile_data['avatar_url'] = avatar_url
            tc_u_id = generate_unique_tc_uid()
            full_name = f"{validated_data.get('first_name') or ''} {validated_data.get('last_name') or ''}".strip()
            try:
                device = create_traccar_device(tc_u_id, device_name=full_name)
            except ValueError as exc:
                raise serializers.ValidationError({'traccar': str(exc)})
            profile_data['tc_u_id'] = tc_u_id
            profile_data['tc_id'] = str(device.get('id'))
            TransporterProfile.objects.create(**profile_data)
            TransporterDriverLink.objects.create(
                transporter=fleet_owner, driver=user, is_active=True,
            )
            if documents:
                upsert_driver_kyc_documents(user, documents, request=request)
        return user


class FleetDriverUpdateSerializer(serializers.Serializer):
    email = serializers.EmailField(required=False)
    password = serializers.CharField(write_only=True, required=False, min_length=8)
    first_name = serializers.CharField(required=False, allow_blank=True)
    last_name = serializers.CharField(required=False, allow_blank=True)
    phone = serializers.CharField(required=False, allow_blank=True)
    language = serializers.CharField(required=False, allow_blank=True)
    avatar = Base64ImageField(required=False, write_only=True, allow_null=True)
    documents = serializers.ListField(
        child=serializers.DictField(),
        required=False,
        allow_empty=True,
    )


class FleetDriverSerializer(serializers.Serializer):
    id = serializers.IntegerField(source='driver.id')
    email = serializers.EmailField(source='driver.email')
    first_name = serializers.CharField(source='driver.first_name')
    last_name = serializers.CharField(source='driver.last_name')
    account_type = serializers.SerializerMethodField()
    phone = serializers.SerializerMethodField()
    language = serializers.SerializerMethodField()
    documents_verified = serializers.SerializerMethodField()
    tc_id = serializers.SerializerMethodField()
    avatar_url = serializers.SerializerMethodField()
    documents = serializers.SerializerMethodField()
    is_active = serializers.BooleanField()
    linked_at = serializers.DateTimeField(source='created_at')

    def get_account_type(self, obj):
        try:
            return obj.driver.transporter_profile.account_type
        except Exception:
            return None

    def get_phone(self, obj):
        try:
            return obj.driver.role.phone or ''
        except Exception:
            return ''

    def get_language(self, obj):
        try:
            return obj.driver.role.language or 'en'
        except Exception:
            return 'en'

    def get_documents_verified(self, obj):
        try:
            return obj.driver.transporter_profile.documents_verified
        except Exception:
            return False

    def get_tc_id(self, obj):
        try:
            return obj.driver.transporter_profile.tc_id or ''
        except Exception:
            return ''

    def get_avatar_url(self, obj):
        try:
            return obj.driver.transporter_profile.avatar_url or ''
        except Exception:
            return ''

    def get_documents(self, obj):
        qs = KYCDocument.objects.filter(
            user=obj.driver,
            document_type__in=TRANSPORTER_DRIVER_DOC_TYPES,
        ).order_by('-submitted_at')
        return KYCDocumentSerializer(qs, many=True, context=self.context).data


class FreightRouteSerializer(serializers.ModelSerializer):
    origin_country_name = serializers.SerializerMethodField()
    destination_country_name = serializers.SerializerMethodField()

    class Meta:
        model = FreightRoute
        fields = (
            'id',
            'origin_country_code',
            'destination_country_code',
            'origin_country_name',
            'destination_country_name',
            'min_freight',
            'currency',
            'created_at',
            'updated_at',
        )
        read_only_fields = ('created_at', 'updated_at')
        # Unique pair is validated in validate() with a clear 400 message.
        validators = []

    def get_origin_country_name(self, obj):
        return country_name_for_code(obj.origin_country_code)

    def get_destination_country_name(self, obj):
        return country_name_for_code(obj.destination_country_code)

    def validate_origin_country_code(self, value):
        code = normalize_country_code(value)
        if not code or not is_valid_country_code(code):
            raise serializers.ValidationError('Invalid origin country code.')
        return code

    def validate_destination_country_code(self, value):
        code = normalize_country_code(value)
        if not code or not is_valid_country_code(code):
            raise serializers.ValidationError('Invalid destination country code.')
        return code

    def validate_currency(self, value):
        if value in (None, ''):
            return value
        code = str(value).strip().upper()
        if len(code) != 3 or not code.isalpha():
            raise serializers.ValidationError('Currency must be a 3-letter ISO 4217 code.')
        return code

    def validate_min_freight(self, value):
        if value is None:
            raise serializers.ValidationError('min_freight is required.')
        if value < 0:
            raise serializers.ValidationError('min_freight cannot be negative.')
        return value

    def validate(self, attrs):
        origin = attrs.get('origin_country_code')
        if origin is None and self.instance is not None:
            origin = self.instance.origin_country_code
        destination = attrs.get('destination_country_code')
        if destination is None and self.instance is not None:
            destination = self.instance.destination_country_code
        if origin and destination and origin == destination:
            raise serializers.ValidationError(
                'Origin and destination must be different countries.',
            )
        if origin and destination:
            qs = FreightRoute.objects.filter(
                origin_country_code=origin,
                destination_country_code=destination,
            )
            if self.instance is not None:
                qs = qs.exclude(pk=self.instance.pk)
            if qs.exists():
                raise serializers.ValidationError(
                    'A freight route for this origin and destination already exists.',
                )
        if origin:
            attrs['currency'] = currency_code_for_country(origin)
        return attrs
