"""Operator portal endpoints. Every rule mirrors testing_app.views:
unverified operators can't add buses/trips, edited buses go back to PENDING,
approved buses are locked, trip cancel refunds every passenger, etc."""
from datetime import datetime

from django.db.models import Count, Sum
from django.utils import timezone
from rest_framework.decorators import api_view

from testing_app.models import Booking, Bus, City, Operator, Route, Schedule
from testing_app.views import ACTIVE_BOOKING_STATES, _refund_booking

from .auth import body, err, jwt_required, ok
from .serializers import (booking_dict, bus_dict, bus_document_dict, city_dict,
                          route_dict, schedule_dict, seats_taken)


def _operator_of(request):
    return Operator.objects.filter(user=request.api_user).first()


def _parse_dt(value):
    """Accept 'YYYY-MM-DDTHH:MM' (datetime-local) or 'YYYY-MM-DD HH:MM'."""
    if not value:
        return None
    for fmt in ('%Y-%m-%dT%H:%M', '%Y-%m-%d %H:%M', '%Y-%m-%dT%H:%M:%S', '%Y-%m-%d %H:%M:%S'):
        try:
            naive = datetime.strptime(str(value), fmt)
            return timezone.make_aware(naive, timezone.get_current_timezone())
        except ValueError:
            continue
    return None


# ---------------------------------------------------------------------------
# Summary
# ---------------------------------------------------------------------------

@api_view(['GET'])
@jwt_required('OPERATOR')
def summary(request):
    op = _operator_of(request)
    if op is None:
        return err('No operator profile found for your account.', status=403)

    today = timezone.localdate()
    my_schedules = Schedule.objects.filter(bus__operator=op)

    today_qs = my_schedules.filter(departure_time__date=today).select_related(
        'bus', 'route__source_city', 'route__destination_city').order_by('departure_time')

    paid = Booking.objects.filter(schedule__bus__operator=op).exclude(
        booking_status__in=['CANCELLED', 'REFUNDED', 'PENDING_PAYMENT', 'EXPIRED'])
    agg = paid.aggregate(total=Count('id'), gross=Sum('total_amount'),
                         net=Sum('operator_earning'), commission=Sum('commission_amount'))

    active_trips = my_schedules.filter(status='ACTIVE', departure_time__gte=timezone.now())
    total_seats = sum(s.bus.total_seats for s in active_trips.select_related('bus'))
    booked_seats = Booking.objects.filter(
        schedule__in=active_trips, booking_status__in=ACTIVE_BOOKING_STATES
    ).aggregate(n=Sum('seats_count'))['n'] or 0

    buses = Bus.objects.filter(operator=op)
    recent_bookings = paid.select_related(
        'user', 'schedule__bus__operator', 'schedule__route__source_city',
        'schedule__route__destination_city').order_by('-booked_at')[:6]

    return ok({
        'company': op.company_name,
        'verified': op.verified,
        'today_trips': [schedule_dict(s) for s in today_qs],
        'today_trip_count': today_qs.count(),
        'total_bookings': agg['total'] or 0,
        'gross': float(agg['gross'] or 0),
        'net': float(agg['net'] or 0),
        'commission': float(agg['commission'] or 0),
        'bus_count': buses.count(),
        'approved_bus_count': buses.filter(approval_status='APPROVED').count(),
        'pending_bus_count': buses.filter(approval_status='PENDING').count(),
        'active_trip_count': active_trips.count(),
        'total_seats': total_seats,
        'booked_seats': booked_seats,
        'occupancy': round(booked_seats / total_seats * 100, 1) if total_seats else 0,
        'recent_bookings': [booking_dict(b) for b in recent_bookings],
    })


# ---------------------------------------------------------------------------
# Buses
# ---------------------------------------------------------------------------

@api_view(['GET', 'POST'])
@jwt_required('OPERATOR')
def buses(request):
    op = _operator_of(request)
    if op is None:
        return err('No operator profile found for your account.', status=403)

    if request.method == 'GET':
        rows = Bus.objects.filter(operator=op).select_related('operator').order_by('-id')
        return ok([bus_dict(b) for b in rows])

    if request.method == 'POST':
        if not op.verified:
            return err('Your operator account is not approved by the admin yet, so you cannot add buses.', status=403)
        data = body(request)
        bus_name = str(data.get('bus_name', '')).strip()
        registration_no = str(data.get('registration_no', '')).strip()
        bus_type = data.get('bus_type', 'NORMAL')
        total_seats = str(data.get('total_seats', '')).strip()
        if not bus_name or not registration_no or not total_seats:
            return err('Please fill in all required fields.')
        if not total_seats.isdigit() or int(total_seats) < 1:
            return err('Total seats must be a positive number.')
        if bus_type not in dict(Bus.BUS_TYPE_CHOICES):
            return err('Invalid bus type.')
        if Bus.objects.filter(registration_no=registration_no).exists():
            return err('A bus with this registration number already exists.')
        b = Bus.objects.create(operator=op, bus_name=bus_name, registration_no=registration_no,
                               bus_type=bus_type, total_seats=int(total_seats),
                               status='ACTIVE', approval_status='PENDING')
        from testing_app.views import log_activity
        log_activity(request.api_user, 'OPERATOR', 'BUS_CREATED',
                    f'{bus_name} ({registration_no}) - {total_seats} seats')
        return ok(bus_dict(b), message='Bus added! Click "Documents" to upload its bluebook, insurance and '
                                       'driver\'s license - it becomes bookable once the admin verifies all 4.')

    return err('Method not allowed.', status=405)


@api_view(['GET', 'PUT'])
@jwt_required('OPERATOR')
def bus_detail(request, bus_id):
    op = _operator_of(request)
    b = Bus.objects.select_related('operator').filter(id=bus_id, operator=op).first()
    if b is None:
        return err('Bus not found, or it does not belong to your account.', status=404)

    if request.method == 'PUT':
        if b.approval_status == 'APPROVED':
            return err('This bus is already approved and cannot be edited. Contact the admin.', status=403)
        data = body(request)
        bus_name = str(data.get('bus_name', '')).strip()
        registration_no = str(data.get('registration_no', '')).strip()
        bus_type = data.get('bus_type', b.bus_type)
        total_seats = str(data.get('total_seats', '')).strip()
        if not bus_name or not registration_no or not total_seats:
            return err('Please fill in all required fields.')
        if not total_seats.isdigit() or int(total_seats) < 1:
            return err('Total seats must be a positive number.')
        if Bus.objects.filter(registration_no=registration_no).exclude(id=b.id).exists():
            return err('Another bus with this registration number already exists.')
        b.bus_name, b.registration_no = bus_name, registration_no
        b.bus_type, b.total_seats = bus_type, int(total_seats)
        b.approval_status, b.review_note = 'PENDING', None
        b.reviewed_at, b.reviewed_by = None, None
        b.save()
        return ok(bus_dict(b), message='Bus updated and resubmitted for admin review.')

    if request.method == 'GET':
        return ok(bus_dict(b))
    return err('Method not allowed.', status=405)


# ---------------------------------------------------------------------------
# Cities + routes
# ---------------------------------------------------------------------------

@api_view(['GET'])
@jwt_required('OPERATOR')
def cities(request):
    return ok([city_dict(c) for c in City.objects.order_by('province', 'city_name')])


@api_view(['GET', 'POST'])
@jwt_required('OPERATOR')
def routes(request):
    if request.method == 'GET':
        rows = Route.objects.select_related('source_city', 'destination_city').order_by('-id')
        return ok([route_dict(r) for r in rows])

    if request.method == 'POST':
        data = body(request)
        src, dst = data.get('source_city'), data.get('destination_city')
        distance = str(data.get('distance_km', '') or '').strip()
        duration = str(data.get('duration_minutes', '') or '').strip()
        if not src or not dst:
            return err('Please choose both source and destination cities.')
        if str(src) == str(dst):
            return err('Source and destination cannot be the same city.')
        if not City.objects.filter(id=src).exists() or not City.objects.filter(id=dst).exists():
            return err('Pick cities from the platform list.')
        if Route.objects.filter(source_city_id=src, destination_city_id=dst).exists():
            return err('That route already exists.')
        r = Route.objects.create(
            source_city_id=src, destination_city_id=dst,
            distance_km=distance or None,
            estimated_duration_minutes=int(duration) if duration.isdigit() else None,
            is_active=True, created_by=request.api_user)
        return ok(route_dict(r), message='Route created.')
    return err('Method not allowed.', status=405)


@api_view(['PUT'])
@jwt_required('OPERATOR')
def route_detail(request, route_id):
    r = Route.objects.select_related('source_city', 'destination_city').filter(id=route_id).first()
    if r is None:
        return err('Route not found.', status=404)

    if request.method == 'PUT':
        data = body(request)
        src, dst = data.get('source_city'), data.get('destination_city')
        distance = str(data.get('distance_km', '') or '').strip()
        duration = str(data.get('duration_minutes', '') or '').strip()
        if not src or not dst:
            return err('Please choose both cities.')
        if str(src) == str(dst):
            return err('Source and destination cannot be the same city.')
        r.source_city_id, r.destination_city_id = src, dst
        r.distance_km = distance or None
        r.estimated_duration_minutes = int(duration) if duration.isdigit() else None
        r.save()
        r = Route.objects.select_related('source_city', 'destination_city').get(id=r.id)
        return ok(route_dict(r), message='Route updated.')
    return err('Method not allowed.', status=405)


@api_view(['POST'])
@jwt_required('OPERATOR')
def route_toggle(request, route_id):
    r = Route.objects.select_related('source_city', 'destination_city').filter(id=route_id).first()
    if r is None:
        return err('Route not found.', status=404)
    r.is_active = not r.is_active
    r.save(update_fields=['is_active'])
    return ok(route_dict(r), message=f"Route {'activated' if r.is_active else 'deactivated'}.")


# ---------------------------------------------------------------------------
# Schedules (trips)
# ---------------------------------------------------------------------------

@api_view(['GET', 'POST'])
@jwt_required('OPERATOR')
def schedules(request):
    op = _operator_of(request)
    if op is None:
        return err('No operator profile found for your account.', status=403)

    if request.method == 'GET':
        rows = Schedule.objects.select_related(
            'bus', 'route__source_city', 'route__destination_city'
        ).filter(bus__operator=op).order_by('-departure_time')[:200]
        return ok([schedule_dict(s) for s in rows])

    if request.method == 'POST':
        if not op.verified:
            return err('Your operator account must be approved before creating trips.', status=403)
        data = body(request)
        bus_id, route_id = data.get('bus'), data.get('route')
        departure = _parse_dt(data.get('departure_time'))
        arrival = _parse_dt(data.get('arrival_time'))
        fare = str(data.get('fare', '') or '').strip()
        if not bus_id or not route_id or not departure or not arrival or not fare:
            return err('Please fill in all fields.')
        if arrival <= departure:
            return err('Arrival must be after departure.')
        bus_obj = Bus.objects.filter(id=bus_id, operator=op, approval_status='APPROVED', status='ACTIVE').first()
        if bus_obj is None:
            return err('Pick one of your approved buses.')
        if not bus_obj.is_fully_verified():
            return err(f'"{bus_obj.bus_name}" still needs all 4 documents verified by the admin '
                      'before you can schedule trips on it. Check My Buses → Documents.', status=403)
        if not Route.objects.filter(id=route_id, is_active=True).exists():
            return err('Pick an active route.')
        s = Schedule.objects.create(bus_id=bus_id, route_id=route_id,
                                    departure_time=departure, arrival_time=arrival,
                                    fare=fare, status='ACTIVE')
        s = Schedule.objects.select_related('bus', 'route__source_city', 'route__destination_city').get(id=s.id)
        from testing_app.views import log_activity
        log_activity(request.api_user, 'OPERATOR', 'TRIP_CREATED',
                    f'{s.route.source_city.city_name} → {s.route.destination_city.city_name} on '
                    f'{s.bus.bus_name} - departs {departure:%Y-%m-%d %H:%M}, fare Rs.{fare}')
        return ok(schedule_dict(s), message='Trip scheduled. Passengers can now book it.')
    return err('Method not allowed.', status=405)


@api_view(['GET', 'PUT'])
@jwt_required('OPERATOR')
def schedule_detail(request, schedule_id):
    op = _operator_of(request)
    s = Schedule.objects.select_related(
        'bus', 'route__source_city', 'route__destination_city'
    ).filter(id=schedule_id, bus__operator=op).first()
    if s is None:
        return err('Trip not found.', status=404)

    if request.method == 'GET':
        return ok(schedule_dict(s))

    if request.method == 'PUT':
        if s.status != 'ACTIVE':
            return err('Only active trips can be edited.', status=403)
        data = body(request)
        bus_id = data.get('bus', s.bus_id)
        route_id = data.get('route', s.route_id)
        departure = _parse_dt(data.get('departure_time')) or s.departure_time
        arrival = _parse_dt(data.get('arrival_time')) or s.arrival_time
        fare = str(data.get('fare', s.fare))
        if arrival <= departure:
            return err('Arrival must be after departure.')
        edit_bus = Bus.objects.filter(id=bus_id, operator=op, approval_status='APPROVED').first()
        if edit_bus is None:
            return err('Pick one of your approved buses.')
        if not edit_bus.is_fully_verified():
            return err(f'"{edit_bus.bus_name}" still needs all 4 documents verified before it can run trips.', status=403)
        s.bus_id, s.route_id = bus_id, route_id
        s.departure_time, s.arrival_time, s.fare = departure, arrival, fare
        s.save()
        s = Schedule.objects.select_related('bus', 'route__source_city', 'route__destination_city').get(id=s.id)
        return ok(schedule_dict(s), message='Trip updated.')
    return err('Method not allowed.', status=405)


@api_view(['POST'])
@jwt_required('OPERATOR')
def schedule_cancel(request, schedule_id):
    op = _operator_of(request)
    s = Schedule.objects.filter(id=schedule_id, bus__operator=op).first()
    if s is None:
        return err('Trip not found.', status=404)
    if s.status == 'CANCELLED':
        return err('This trip is already cancelled.')
    s.status = 'CANCELLED'
    s.save(update_fields=['status'])
    affected = Booking.objects.filter(schedule=s, booking_status__in=['PENDING', 'CONFIRMED', 'CHECKED_IN'])
    count = affected.count()
    for b in affected:
        _refund_booking(b, 'Trip cancelled by operator - full refund')
    from testing_app.views import log_activity
    log_activity(request.api_user, 'OPERATOR', 'TRIP_CANCELLED',
                f'Schedule #{s.id} - {count} passenger(s) fully refunded')
    return ok({'refunded': count}, message=f'Trip cancelled. {count} passenger booking(s) fully refunded.')


# ---------------------------------------------------------------------------
# Bookings + manifest
# ---------------------------------------------------------------------------

@api_view(['GET'])
@jwt_required('OPERATOR')
def bookings(request):
    op = _operator_of(request)
    if op is None:
        return err('No operator profile found for your account.', status=403)
    rows = Booking.objects.select_related(
        'user', 'schedule__bus__operator', 'schedule__route__source_city',
        'schedule__route__destination_city'
    ).filter(schedule__bus__operator=op).order_by('-booked_at')[:200]
    return ok([booking_dict(b) for b in rows])


@api_view(['GET'])
@jwt_required('OPERATOR')
def manifest(request, schedule_id):
    op = _operator_of(request)
    s = Schedule.objects.select_related(
        'bus', 'route__source_city', 'route__destination_city'
    ).filter(id=schedule_id, bus__operator=op).first()
    if s is None:
        return err('Trip not found.', status=404)
    rows = Booking.objects.select_related(
        'user', 'schedule__bus__operator', 'schedule__route__source_city',
        'schedule__route__destination_city'
    ).filter(schedule=s, booking_status__in=ACTIVE_BOOKING_STATES).order_by('booked_at')
    return ok({'trip': schedule_dict(s), 'passengers': [booking_dict(b) for b in rows],
               'seats_booked': seats_taken(s)})

# ---------------------------------------------------------------------------
# Operator profile (name / phone / avatar colour / password)
# ---------------------------------------------------------------------------

def _profile_dict(request, user, op):
    return {
        'full_name': user.full_name,
        'email': user.email,
        'phone': user.phone or '',
        'avatar_color': user.avatar_color or '',
        'avatar_url': request.build_absolute_uri(user.avatar.url) if user.avatar else '',
        'has_password': not (user.password_hash or '').startswith('!'),
        'company': op.company_name if op else None,
        'verified': op.verified if op else False,
    }


@api_view(['GET', 'PUT'])
@jwt_required('OPERATOR')
def profile(request):
    u = request.api_user
    op = _operator_of(request)

    if request.method == 'GET':
        return ok(_profile_dict(request, u, op))

    if request.method == 'PUT':
        data = body(request)
        full_name = str(data.get('full_name', '')).strip()
        phone = str(data.get('phone', '') or '').strip()
        avatar_color = str(data.get('avatar_color', '') or '').strip()
        if not full_name:
            return err('Your name cannot be empty.')
        if avatar_color and (len(avatar_color) > 7 or not avatar_color.startswith('#')):
            return err('Invalid avatar colour.')
        u.full_name = full_name
        u.phone = phone or None
        u.avatar_color = avatar_color or None
        u.save(update_fields=['full_name', 'phone', 'avatar_color'])
        # Company name is intentionally NOT accepted here - it's locked after registration.
        return ok(_profile_dict(request, u, op), message='Profile updated.')

    return err('Method not allowed.', status=405)


@api_view(['POST'])
@jwt_required('OPERATOR')
def delete_account(request):
    """Full account deletion - permanently removes this operator's login,
    company profile, buses, trips, documents, and every booking/payment/
    rating tied to those trips. This cannot be undone - there is no
    recovery once confirmed. (If you'd rather keep the financial/booking
    history and just stop the account from logging in, use the admin's
    suspend/deactivate tools instead - this endpoint is a true delete.)"""
    u = request.api_user
    data = body(request)
    password = str(data.get('password', ''))
    if not password:
        return err('Enter your password to confirm account deletion.')
    if (u.password_hash or '').startswith('!'):
        return err('This is a Google-only account with no password set - '
                  'ask the admin to deactivate your account instead.', status=403)
    from testing_app.views import verify_password
    if not verify_password(u, password):
        return err('Incorrect password.', status=403)

    from testing_app.views import log_activity
    log_activity(u, 'OPERATOR', 'ACCOUNT_DELETED',
                f'{u.full_name} ({u.email}) permanently deleted their account and all related data')

    op = _operator_of(request)
    if op is not None:
        for doc in op.documents.all():
            if doc.file:
                doc.file.delete(save=False)
        # Cascades: buses -> seat maps/schedules -> bookings -> booking seats/
        # payments/ratings, plus the operator's documents.
        op.delete()

    if u.avatar:
        u.avatar.delete(save=False)
    u.delete()  # removes the login itself; ActivityLog/LoginLog rows survive with user set to NULL

    return ok({'deleted': True}, message='Your account and all related data have been permanently deleted.')


@api_view(['POST'])
@jwt_required('OPERATOR')
def profile_password(request):
    """Set a password (first time, for Google accounts) or change it."""
    from django.contrib.auth.hashers import make_password
    from testing_app.views import verify_password

    u = request.api_user
    data = body(request)
    new_password = str(data.get('new_password', ''))
    if len(new_password) < 6:
        return err('New password must be at least 6 characters.')

    has_password = not (u.password_hash or '').startswith('!')
    if has_password:
        current = str(data.get('current_password', ''))
        if not current:
            return err('Enter your current password.')
        if not verify_password(u, current):
            return err('Current password is wrong.', status=403)
        msg = 'Password changed.'
    else:
        msg = 'Password set! You can now also login with email + password + code.'

    u.password_hash = make_password(new_password)
    u.password_changed_at = timezone.now()
    u.failed_login_attempts = 0
    u.account_locked_until = None
    u.save(update_fields=['password_hash', 'password_changed_at',
                          'failed_login_attempts', 'account_locked_until'])
    return ok({'has_password': True}, message=msg)


ALLOWED_AVATAR_EXT = {'.jpg', '.jpeg', '.png', '.webp', '.gif'}
MAX_AVATAR_BYTES = 2 * 1024 * 1024  # 2 MB


@api_view(['POST', 'DELETE'])
@jwt_required('OPERATOR')
def profile_avatar(request):
    """Upload (POST multipart, field 'avatar') or remove (DELETE) the photo."""
    import os
    u = request.api_user

    if request.method == 'DELETE':
        if u.avatar:
            u.avatar.delete(save=False)
            u.avatar = None
            u.save(update_fields=['avatar'])
        return ok({'avatar_url': ''}, message='Photo removed.')

    if request.method == 'POST':
        f = request.FILES.get('avatar')
        if f is None:
            return err('Choose an image file first.')
        ext = os.path.splitext(f.name)[1].lower()
        if ext not in ALLOWED_AVATAR_EXT:
            return err('Use a JPG, PNG, WEBP or GIF image.')
        if f.size > MAX_AVATAR_BYTES:
            return err('Image is too big - maximum 2 MB.')
        if u.avatar:
            u.avatar.delete(save=False)
        u.avatar.save(f'user{u.id}{ext}', f, save=True)
        return ok({'avatar_url': request.build_absolute_uri(u.avatar.url)},
                  message='Photo updated!')

    return err('Method not allowed.', status=405)

# ---------------------------------------------------------------------------
# Seat map configuration (FR-20) + deactivate/delete
# ---------------------------------------------------------------------------

@api_view(['GET', 'POST'])
@jwt_required('OPERATOR')
def bus_seat_map(request, bus_id):
    from testing_app.seatmap import (bus_has_active_bookings, default_grid,
                                     generate_seat_layout)

    op = _operator_of(request)
    b = Bus.objects.filter(id=bus_id, operator=op).first()
    if b is None:
        return err('Bus not found, or it does not belong to your account.', status=404)

    if request.method == 'GET':
        seats = list(b.seats.all())
        if not seats:
            rows, cols = default_grid(b.total_seats or 30)
        else:
            rows, cols = b.seat_rows, b.seat_cols
        return ok({
            'has_seat_map': b.has_seat_map,
            'deck_count': b.deck_count,
            'rows': rows, 'cols': cols,
            'locked': bus_has_active_bookings(b),
            'seats': [{'seat_number': s.seat_number, 'deck': s.deck,
                       'row': s.row_number, 'col': s.col_number,
                       'seat_type': s.seat_type} for s in seats],
        })

    # POST: (re)configure
    if bus_has_active_bookings(b):
        return err('This bus has active bookings against it - the seat map cannot be changed. '
                   'Ask the admin if you truly need to reconfigure it.', status=403)
    data = body(request)
    try:
        rows = int(data.get('rows'))
        cols = int(data.get('cols'))
        deck_count = int(data.get('deck_count', 1) or 1)
    except (TypeError, ValueError):
        return err('Rows, columns and deck count must be numbers.')
    if not (1 <= rows <= 30) or not (1 <= cols <= 10) or deck_count not in (1, 2):
        return err('Rows must be 1-30, columns 1-10, decks 1 or 2.')
    seat_types = data.get('seat_types') or {}
    if not isinstance(seat_types, dict):
        seat_types = {}

    generate_seat_layout(b, rows, cols, deck_count, seat_types)
    return ok({'total_seats': b.total_seats}, message=f'Seat map saved - {b.total_seats} seats configured.')


@api_view(['POST'])
@jwt_required('OPERATOR')
def bus_deactivate_toggle(request, bus_id):
    op = _operator_of(request)
    b = Bus.objects.filter(id=bus_id, operator=op).first()
    if b is None:
        return err('Bus not found, or it does not belong to your account.', status=404)
    b.status = 'INACTIVE' if b.status == 'ACTIVE' else 'ACTIVE'
    b.save(update_fields=['status'])
    return ok(bus_dict(b), message=f"Bus {'deactivated' if b.status == 'INACTIVE' else 'reactivated'}.")


@api_view(['DELETE'])
@jwt_required('OPERATOR')
def bus_delete(request, bus_id):
    op = _operator_of(request)
    b = Bus.objects.filter(id=bus_id, operator=op).first()
    if b is None:
        return err('Bus not found, or it does not belong to your account.', status=404)
    if Schedule.objects.filter(bus=b).exists():
        return err('This bus has scheduled trips (past or future) and cannot be deleted, '
                   'to preserve booking history. Deactivate it instead.', status=403)
    name = b.bus_name or b.registration_no
    b.delete()
    return ok(message=f'{name} deleted.')


# ---------------------------------------------------------------------------
# FR-23: cancel ONE booking (operator-attributable reason -> always full refund)
# ---------------------------------------------------------------------------

OPERATOR_CANCEL_REASONS = ['SAFETY_ISSUE', 'VEHICLE_FAILURE', 'OPERATIONAL_ISSUE', 'WEATHER', 'OTHER']


@api_view(['GET'])
@jwt_required('OPERATOR')
def booking_cancel_reasons(request):
    return ok(OPERATOR_CANCEL_REASONS)


@api_view(['POST'])
@jwt_required('OPERATOR')
def booking_cancel(request, booking_id):
    op = _operator_of(request)
    b = Booking.objects.select_related('schedule__bus', 'user').filter(
        id=booking_id, schedule__bus__operator=op).first()
    if b is None:
        return err('Booking not found on your trips.', status=404)
    if b.booking_status not in ('PENDING', 'PENDING_PAYMENT', 'CONFIRMED', 'CHECKED_IN'):
        return err(f'This booking is {b.booking_status} and cannot be cancelled.')
    data = body(request)
    reason_code = str(data.get('reason_code', '')).strip()
    note = str(data.get('note', '') or '').strip()
    if reason_code not in OPERATOR_CANCEL_REASONS:
        return err('Pick a valid reason code.')
    _refund_booking(b, note or f'Cancelled by operator ({reason_code})',
                    cancelled_by='OPERATOR', reason_code=reason_code)
    from testing_app.views import log_activity
    log_activity(request.api_user, 'OPERATOR', 'BOOKING_CANCELLED',
                f'{b.booking_code} - {reason_code} - passenger fully refunded')
    return ok(booking_dict(b), message=f'Booking {b.booking_code} cancelled - passenger fully refunded.')


# ---------------------------------------------------------------------------
# Per-bus documents (bluebook, insurance, driver's license) - exactly 4
# required slots per bus. A bus only becomes bookable once all 4 are VERIFIED.
# ---------------------------------------------------------------------------

ALLOWED_DOC_EXT = {'.pdf', '.jpg', '.jpeg', '.png', '.webp'}
MAX_DOC_BYTES = 5 * 1024 * 1024


@api_view(['GET'])
@jwt_required('OPERATOR')
def bus_documents(request, bus_id):
    from testing_app.models import BusDocument
    op = _operator_of(request)
    b = Bus.objects.filter(id=bus_id, operator=op).first()
    if b is None:
        return err('Bus not found, or it does not belong to your account.', status=404)

    existing = {d.doc_type: d for d in b.documents.all()}
    slots = []
    for doc_type, label in BusDocument.DOC_TYPE_CHOICES:
        d = existing.get(doc_type)
        if d:
            slots.append({**bus_document_dict(request, d), 'doc_type': doc_type, 'label': label})
        else:
            slots.append({'doc_type': doc_type, 'label': label, 'status': 'MISSING',
                         'file_url': '', 'file_name': '', 'review_note': '', 'can_replace': True,
                         'uploaded_at': None, 'updated_at': None})
    return ok({'bus_id': b.id, 'bus_name': b.bus_name, 'is_fully_verified': b.is_fully_verified(),
              'is_bookable': b.is_bookable(), 'slots': slots})


@api_view(['POST'])
@jwt_required('OPERATOR')
def bus_document_upload(request, bus_id, doc_type):
    import os
    from testing_app.models import BusDocument
    op = _operator_of(request)
    b = Bus.objects.filter(id=bus_id, operator=op).first()
    if b is None:
        return err('Bus not found, or it does not belong to your account.', status=404)
    if doc_type not in BusDocument.REQUIRED_DOC_TYPES:
        return err('Invalid document type.')

    f = request.FILES.get('file')
    if f is None:
        return err('Choose a file first.')
    ext = os.path.splitext(f.name)[1].lower()
    if ext not in ALLOWED_DOC_EXT:
        return err('Use a PDF, JPG, PNG or WEBP file.')
    if f.size > MAX_DOC_BYTES:
        return err('File too big - maximum 5 MB.')

    existing = BusDocument.objects.filter(bus=b, doc_type=doc_type).first()
    if existing and not existing.can_be_replaced_by_operator():
        return err('This document is already verified and locked. '
                  'Contact the admin if it genuinely needs to change.', status=403)

    if existing:
        if existing.file:
            existing.file.delete(save=False)
        existing.file = f
        existing.status = 'PENDING'
        existing.review_note = None
        existing.reviewed_at = None
        existing.reviewed_by = None
        existing.save()
        d = existing
        verb = 're-uploaded'
    else:
        d = BusDocument.objects.create(bus=b, doc_type=doc_type, file=f)
        verb = 'uploaded'

    from testing_app.realtime import notify_admins
    notify_admins('bus_document_uploaded', {
        'company': op.company_name, 'bus_name': b.bus_name, 'bus_id': b.id, 'doc_type': doc_type,
    })
    from testing_app.views import log_activity
    log_activity(request.api_user, 'OPERATOR', 'BUS_DOCUMENT_UPLOADED',
                f'{b.bus_name} - {dict(BusDocument.DOC_TYPE_CHOICES)[doc_type]} {verb}')
    return ok(bus_document_dict(request, d), message=f'Document {verb} - the admin will review it.')


# ---------------------------------------------------------------------------
# BR-11: mark a trip DELAYED (+ email every confirmed passenger)
# ---------------------------------------------------------------------------

@api_view(['POST'])
@jwt_required('OPERATOR')
def schedule_delay(request, schedule_id):
    from django.core.mail import send_mail
    op = _operator_of(request)
    s = Schedule.objects.select_related(
        'route__source_city', 'route__destination_city').filter(id=schedule_id, bus__operator=op).first()
    if s is None:
        return err('Trip not found.', status=404)
    if s.status not in ('ACTIVE', 'DELAYED'):
        return err(f'A {s.status} trip cannot be delayed.')
    data = body(request)
    new_dep = _parse_dt(data.get('new_departure_time'))
    note = str(data.get('note', '') or '').strip()
    if new_dep is None:
        return err('Give the new departure time.')
    if new_dep <= s.departure_time:
        return err('The new departure must be later than the current one.')
    if s.original_departure_time is None:
        s.original_departure_time = s.departure_time
    s.departure_time = new_dep
    s.delay_note = note or None
    s.status = 'DELAYED'
    s.save(update_fields=['departure_time', 'original_departure_time', 'delay_note', 'status'])

    passengers = Booking.objects.filter(
        schedule=s, booking_status__in=['CONFIRMED', 'CHECKED_IN']).select_related('user')
    trip = f"{s.route.source_city.city_name} → {s.route.destination_city.city_name}"
    for b in passengers:
        try:
            send_mail(
                subject=f'Trip delayed - {trip}',
                message=(f"Hi {b.user.full_name.split(' ')[0]},\n\n"
                         f"Your trip {trip} (booking {b.booking_code}) has been delayed.\n"
                         f"New departure: {timezone.localtime(s.departure_time):%a, %d %b %Y · %I:%M %p}\n"
                         + (f"Operator note: {note}\n" if note else "")
                         + "\nSorry for the inconvenience.\n- Yatra Booking"),
                from_email=None, recipient_list=[b.user.email], fail_silently=True)
        except Exception:
            pass
    from testing_app.realtime import notify_admins
    notify_admins('trip_delayed', {'trip': trip, 'schedule_id': s.id})
    from testing_app.views import log_activity
    log_activity(request.api_user, 'OPERATOR', 'TRIP_DELAYED',
                f'{trip} - new departure {new_dep:%Y-%m-%d %H:%M} - {passengers.count()} passenger(s) notified')
    return ok(schedule_dict(s), message=f'Trip marked DELAYED - {passengers.count()} passenger(s) emailed.')


# ---------------------------------------------------------------------------
# FR-17 (operator view) + FR-24 CSV export
# ---------------------------------------------------------------------------

@api_view(['GET'])
@jwt_required('OPERATOR')
def ratings(request):
    from django.db.models import Avg
    from testing_app.models import Rating
    op = _operator_of(request)
    qs = Rating.objects.filter(operator=op).select_related('user', 'booking')
    avg = qs.aggregate(a=Avg('stars'))['a']
    return ok({
        'average': round(avg, 2) if avg else None,
        'count': qs.count(),
        'ratings': [{'stars': r.stars, 'review': r.review, 'issue_flag': r.issue_flag,
                     'by': r.user.full_name, 'booking_code': r.booking.booking_code,
                     'created_at': timezone.localtime(r.created_at).strftime('%Y-%m-%d')} for r in qs[:100]],
    })


@api_view(['GET'])
@jwt_required('OPERATOR')
def export_bookings_csv(request):
    import csv
    from django.http import HttpResponse
    op = _operator_of(request)
    rows = Booking.objects.select_related(
        'user', 'schedule__route__source_city', 'schedule__route__destination_city', 'schedule__bus'
    ).filter(schedule__bus__operator=op).order_by('-booked_at')[:2000]
    resp = HttpResponse(content_type='text/csv')
    resp['Content-Disposition'] = 'attachment; filename="my-bookings.csv"'
    w = csv.writer(resp)
    w.writerow(['Booking', 'Passenger', 'Trip', 'Bus', 'Departure', 'Seats', 'Total (Rs)',
                'Commission (Rs)', 'My earning (Rs)', 'Status', 'Booked at'])
    for b in rows:
        w.writerow([b.booking_code, b.user.full_name,
                    f"{b.schedule.route.source_city.city_name}-{b.schedule.route.destination_city.city_name}",
                    b.schedule.bus.bus_name or b.schedule.bus.registration_no,
                    timezone.localtime(b.schedule.departure_time).strftime('%Y-%m-%d %H:%M'),
                    b.seats_count, b.total_amount, b.commission_amount, b.operator_earning,
                    b.booking_status, timezone.localtime(b.booked_at).strftime('%Y-%m-%d %H:%M')])
    return resp
