"""Passenger seat-map endpoints (FR-10/FR-11, BR-1, BR-3).

Session-authenticated (same as the rest of the passenger website - these are
NOT part of the JWT-based client API). Concurrency safety uses
`select_for_update()` on the BusSeat row itself as the serialization point:
two simultaneous requests for the same physical seat will genuinely block
each other on MySQL/InnoDB (the production database), one at a time, so only
one can ever win the lock/booking. SQLite (used for automated sandbox tests)
does not provide real row-level locking, so the *business-rule* correctness
here is fully tested, but true concurrent-safety should get one manual check
against a real MySQL instance before relying on it under production load.
"""
import json
from datetime import timedelta

from django.conf import settings
from django.db import transaction
from django.http import JsonResponse
from django.utils import timezone

from .models import BookingSeat, BusSeat, Schedule, SeatLock
from .realtime import broadcast_seat_update
from .views import ACTIVE_BOOKING_STATES, current_user, role_required


def _body(request):
    try:
        return json.loads(request.body.decode('utf-8') or '{}')
    except (ValueError, UnicodeDecodeError):
        return {}


def _err(message, status=400):
    return JsonResponse({'ok': False, 'error': message}, status=status)


def _ok(payload=None, **extra):
    data = {'ok': True}
    if payload is not None:
        data['data'] = payload
    data.update(extra)
    return JsonResponse(data)


def _booked_seat_ids(schedule):
    return set(
        BookingSeat.objects.filter(
            schedule=schedule, booking__booking_status__in=ACTIVE_BOOKING_STATES
        ).values_list('seat_id', flat=True)
    )


@role_required('PASSENGER')
def seat_map_status(request, schedule_id):
    """Live grid: every seat's current status for this schedule."""
    schedule = Schedule.objects.select_related('bus').filter(id=schedule_id).first()
    if schedule is None:
        return _err('Trip not found.', status=404)
    bus = schedule.bus
    if not bus.has_seat_map:
        return _err('This trip does not use seat-level selection.', status=409)

    user = current_user(request)
    now = timezone.now()

    # Garbage-collect expired locks for this schedule (cheap, keeps status fresh)
    SeatLock.objects.filter(schedule=schedule, expires_at__lte=now).delete()

    booked_ids = _booked_seat_ids(schedule)
    locks = {l.seat_id: l for l in SeatLock.objects.filter(schedule=schedule)}

    seats = []
    for s in bus.seats.all():
        if not s.is_active:
            status = 'BLOCKED'
        elif s.id in booked_ids:
            status = 'BOOKED'
        elif s.id in locks:
            status = 'LOCKED_BY_ME' if locks[s.id].locked_by_id == user.id else 'LOCKED'
        else:
            status = 'AVAILABLE'
        seats.append({
            'seat_number': s.seat_number, 'deck': s.deck, 'row': s.row_number,
            'col': s.col_number, 'seat_type': s.seat_type, 'status': status,
        })

    return _ok({
        'rows': bus.seat_rows, 'cols': bus.seat_cols, 'deck_count': bus.deck_count,
        'fare': float(schedule.fare), 'seats': seats,
        'lock_minutes': settings.SEAT_LOCK_MINUTES,
    })


@role_required('PASSENGER')
def seat_map_lock(request, schedule_id):
    """Declarative: the request's seat_numbers list becomes this user's
    entire held selection for this schedule (extras released, new ones locked)."""
    if request.method != 'POST':
        return _err('Method not allowed.', status=405)

    schedule = Schedule.objects.select_related('bus').filter(id=schedule_id, status='ACTIVE').first()
    if schedule is None:
        return _err('Trip not available.', status=404)
    if schedule.departure_time <= timezone.now():
        return _err('This trip has already departed.', status=409)

    bus = schedule.bus
    if not bus.has_seat_map:
        return _err('This trip does not use seat-level selection.', status=409)

    wanted = _body(request).get('seat_numbers') or []
    wanted = [str(x) for x in wanted][:10]  # BR-4 style sane upper bound
    user = current_user(request)
    now = timezone.now()
    expires_at = now + timedelta(minutes=settings.SEAT_LOCK_MINUTES)

    # Release any of my current locks on this schedule that aren't in the new selection
    dropped_qs = SeatLock.objects.filter(schedule=schedule, locked_by=user).exclude(seat__seat_number__in=wanted)
    dropped = list(dropped_qs.values_list('seat__seat_number', flat=True))
    dropped_qs.delete()
    if dropped:
        broadcast_seat_update(schedule.id, dropped, 'AVAILABLE')

    granted, rejected = [], []
    with transaction.atomic():
        for seat_number in wanted:
            seat = BusSeat.objects.select_for_update().filter(bus=bus, seat_number=seat_number).first()
            if seat is None or not seat.is_active:
                rejected.append({'seat_number': seat_number, 'reason': 'Not a valid seat.'})
                continue

            SeatLock.objects.filter(schedule=schedule, seat=seat, expires_at__lte=now).delete()

            already_booked = BookingSeat.objects.filter(
                schedule=schedule, seat=seat, booking__booking_status__in=ACTIVE_BOOKING_STATES
            ).exists()
            if already_booked:
                rejected.append({'seat_number': seat_number, 'reason': 'Already booked.'})
                continue

            existing_lock = SeatLock.objects.filter(schedule=schedule, seat=seat).first()
            if existing_lock and existing_lock.locked_by_id != user.id:
                rejected.append({'seat_number': seat_number, 'reason': 'Currently held by another passenger.'})
                continue

            SeatLock.objects.update_or_create(
                schedule=schedule, seat=seat,
                defaults={'locked_by': user, 'expires_at': expires_at},
            )
            granted.append(seat_number)

    if granted:
        broadcast_seat_update(schedule.id, granted, 'LOCKED')

    return _ok({'granted': granted, 'rejected': rejected,
               'expires_at': expires_at.strftime('%Y-%m-%d %H:%M:%S')})


@role_required('PASSENGER')
def seat_map_release(request, schedule_id):
    if request.method != 'POST':
        return _err('Method not allowed.', status=405)
    user = current_user(request)
    mine = SeatLock.objects.filter(schedule_id=schedule_id, locked_by=user)
    released_numbers = list(mine.values_list('seat__seat_number', flat=True))
    n, _ = mine.delete()
    if released_numbers:
        broadcast_seat_update(schedule_id, released_numbers, 'AVAILABLE')
    return _ok({'released': n})
