import csv
import secrets
from datetime import timedelta
from functools import wraps
from urllib.parse import urlencode

import requests

from django.conf import settings
from django.contrib import messages
from django.contrib.auth.hashers import check_password, identify_hasher, make_password
from django.core.mail import send_mail
from django.http import HttpResponse
from django.shortcuts import redirect, render
from django.utils import timezone
from django.utils.crypto import constant_time_compare
from decimal import Decimal
from django.db.models import Count, Sum
from django.db import transaction as db_transaction

from .models import *

# Matches MAX_LOGIN_ATTEMPTS = 5 in system_settings of yatra_booking.sql
MAX_LOGIN_ATTEMPTS = 5
LOCKOUT_MINUTES = 15
OTP_VALID_MINUTES = 5
OTP_MAX_TRIES = 3


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def get_client_ip(request):
    """Best-effort client IP (works behind a proxy too)."""
    forwarded = request.META.get('HTTP_X_FORWARDED_FOR')
    if forwarded:
        return forwarded.split(',')[0].strip()
    return request.META.get('REMOTE_ADDR')


def log_login(request, status, email='', user=None, message='', role_name=None):
    """Write one row into the login_logs table."""
    LoginLog.objects.create(
        user=user,
        email_attempted=email or (user.email if user else ''),
        role_name=role_name or (user.role.name if user else None),
        status=status,
        ip_address=get_client_ip(request),
        user_agent=request.META.get('HTTP_USER_AGENT', '')[:255],
        message=message,
    )
    
def log_activity(user, role, action, detail, amount=None):
    """Write one row into the passenger/operator activity audit trail
    (kept separate from AdminAudit, which is admin moderation actions)."""
    ActivityLog.objects.create(user=user, role=role, action=action, detail=detail[:500], amount=amount)    


def verify_password(user, raw_password):
    """
    Check the password against password_hash. Plain-text values typed into
    Django admin are accepted once and upgraded to a real hash.
    """
    stored = user.password_hash or ''
    try:
        identify_hasher(stored)          # raises ValueError if not a known hash
        return check_password(raw_password, stored)
    except ValueError:
        if constant_time_compare(stored, raw_password):
            user.password_hash = make_password(raw_password)
            user.save(update_fields=['password_hash'])
            return True
        return False


def _finish_login(request, user, log_message, active_role=None):
    """Common final step: set session, update last_login, write the log."""
    active_role = active_role or user.role.name

    user.last_login_at = timezone.now()
    user.save(update_fields=['last_login_at'])

    request.session.cycle_key()  # new session id (prevents session fixation)
    request.session['user_id'] = user.id
    request.session['user_name'] = user.full_name
    request.session['user_role'] = active_role
    request.session.pop('otp_user_id', None)

    log_login(request, 'SUCCESS', user=user, message=log_message, role_name=active_role)
    messages.success(request, f'Welcome {user.full_name}! You are logged in as {active_role}.')
    if active_role == 'ADMIN':
        return redirect('admin_dashboard')
    
    if active_role == 'OPERATOR':
        return redirect('operator_dashboard')   
    
    if active_role == 'PASSENGER':
        return redirect('book_ticket')
    return redirect('dashboard')  # fallback for any other role
    
def grant_role(user, role_name):
    """Let this user also login as `role_name` (creates the operator profile if needed)."""
    role_obj, _ = Role.objects.get_or_create(name=role_name)
    if user.role_id != role_obj.id:
        UserRoleAccess.objects.get_or_create(user=user, role=role_obj)
    if role_name == 'OPERATOR' and not Operator.objects.filter(user=user).exists():
        Operator.objects.create(user=user, company_name=f"{user.full_name}'s Company", verified=False)


def _generate_and_send_otp(request, user, purpose='login'):
    """Create a 6-digit OTP, email it (pretty HTML), log it. Returns (ok, error_message)."""
    from .emails import otp_email

    recent = LoginOTP.objects.filter(
        user=user, used=False,
        created_at__gt=timezone.now() - timedelta(seconds=60)
    ).first()
    if recent:
        return False, 'Please wait a minute before requesting a new code.'

    code = f"{secrets.randbelow(1000000):06d}"
    LoginOTP.objects.filter(user=user, used=False).update(used=True)  # kill old codes
    LoginOTP.objects.create(
        user=user, code=code,
        expires_at=timezone.now() + timedelta(minutes=OTP_VALID_MINUTES),
    )
    subject, text, html = otp_email(purpose, user, code, OTP_VALID_MINUTES)
    try:
        send_mail(
            subject=subject,
            message=text,
            from_email=None,  # uses DEFAULT_FROM_EMAIL from settings
            recipient_list=[user.email],
            fail_silently=False,
            html_message=html,
        )
    except Exception as e:
        return False, f'Could not send the OTP email ({e}).'

    log_login(request, 'OTP_SENT', user=user, message=f'OTP emailed ({purpose})')
    return True, ''


# ---------------------------------------------------------------------------
# Access-control decorators (session based)
# ---------------------------------------------------------------------------

def login_required_custom(view_func):
    """Only allow logged-in users (any role)."""
    @wraps(view_func)
    def wrapper(request, *args, **kwargs):
        if not request.session.get('user_id'):
            messages.error(request, 'Please login first to access this page.')
            return redirect('login')
        return view_func(request, *args, **kwargs)
    return wrapper


def role_required(*roles):
    """Only allow logged-in users whose role is in `roles`."""
    def decorator(view_func):
        @wraps(view_func)
        def wrapper(request, *args, **kwargs):
            if not request.session.get('user_id'):
                messages.error(request, 'Please login first to access this page.')
                return redirect('login')
            if request.session.get('user_role') not in roles:
                messages.error(
                    request,
                    f"Access denied. Only {' / '.join(roles)} can open that page. "
                    f"(You are logged in as {request.session.get('user_role')}.)"
                )
                return redirect('dashboard')
            return view_func(request, *args, **kwargs)
        return wrapper
    return decorator


# ---------------------------------------------------------------------------
# Authentication views
# ---------------------------------------------------------------------------

def login_view(request):
    if request.session.get('user_id'):
        return redirect('dashboard')

    if request.method == 'POST':
        email = request.POST.get('email', '').strip()
        password = request.POST.get('password', '')

        if not email or not password:
            messages.error(request, 'Please enter both email and password.')
            return render(request, 'login.html')

        user = User.objects.select_related('role').filter(email__iexact=email).first()

        if user is None:
            log_login(request, 'FAILED', email=email, message='Unknown email')
            messages.error(request, 'Invalid email or password.')
            return render(request, 'login.html')

        now = timezone.now()

        if user.account_locked_until and user.account_locked_until > now:
            remaining = int((user.account_locked_until - now).total_seconds() // 60) + 1
            log_login(request, 'LOCKED', user=user, message='Attempt while account locked')
            messages.error(
                request,
                f'Account locked after too many failed attempts. '
                f'Try again in {remaining} minute(s).'
            )
            return render(request, 'login.html')

        if user.status != 'ACTIVE':
            log_login(request, 'FAILED', user=user, message=f'Account status: {user.status}')
            messages.error(request, f'Your account is {user.status}. Please contact the administrator.')
            return render(request, 'login.html')

        # Google-created accounts have no password - point them to the right button
        if (user.password_hash or '').startswith('!'):
            log_login(request, 'FAILED', user=user, message='Password login on Google-only account')
            messages.error(request, 'This account was created with Google. Please use the "Continue with Google" button.')
            return render(request, 'login.html')

        if verify_password(user, password):
            user.failed_login_attempts = 0
            user.account_locked_until = None
            user.save(update_fields=['failed_login_attempts', 'account_locked_until'])

            # Operators need a second step: email OTP
            if user.role.name in ('PASSENGER', 'OPERATOR'):
                ok, err = _generate_and_send_otp(request, user)
                if not ok:
                    messages.error(request, err)
                    return render(request, 'login.html')
                request.session['otp_user_id'] = user.id
                messages.success(request, f'Password correct! We emailed a 6-digit code to {user.email}.')
                return redirect('verify_otp')

            return _finish_login(request, user, 'Login successful')

        # Wrong password -> count the failure, maybe lock the account
        user.failed_login_attempts += 1
        if user.failed_login_attempts >= MAX_LOGIN_ATTEMPTS:
            user.account_locked_until = now + timedelta(minutes=LOCKOUT_MINUTES)
            user.save(update_fields=['failed_login_attempts', 'account_locked_until'])
            log_login(request, 'LOCKED', user=user,
                      message=f'Account locked after {MAX_LOGIN_ATTEMPTS} failed attempts')
            messages.error(request, f'Too many failed attempts. Account locked for {LOCKOUT_MINUTES} minutes.')
        else:
            user.save(update_fields=['failed_login_attempts'])
            log_login(request, 'FAILED', user=user,
                      message=f'Wrong password (attempt {user.failed_login_attempts}/{MAX_LOGIN_ATTEMPTS})')
            messages.error(
                request,
                f'Invalid email or password. '
                f'Attempt {user.failed_login_attempts} of {MAX_LOGIN_ATTEMPTS}.'
            )

    return render(request, 'login.html')


def register_view(request):
    """Self-registration for users and operators (password + email OTP verification)."""
    if request.session.get('user_id'):
        return redirect('dashboard')

    if request.method == 'POST':
        role_choice = request.POST.get('role', 'PASSENGER')
        if role_choice not in ('PASSENGER', 'OPERATOR'):
            role_choice = 'PASSENGER'

        full_name = request.POST.get('full_name', '').strip()
        company_name = request.POST.get('company_name', '').strip()
        email = request.POST.get('email', '').strip()
        phone = request.POST.get('phone', '').strip() or None
        password = request.POST.get('password', '')
        confirm = request.POST.get('confirm_password', '')

        if not full_name or not email or not password:
            messages.error(request, 'Please fill in all required fields.')
        elif role_choice == 'OPERATOR' and not company_name:
            messages.error(request, 'Company name is required for operator accounts.')
        elif len(password) < 6:
            messages.error(request, 'Password must be at least 6 characters.')
        elif password != confirm:
            messages.error(request, 'Passwords do not match.')
        elif User.objects.filter(email__iexact=email).exists():
            messages.error(request, 'An account with this email already exists. Please login instead.')
        elif phone and User.objects.filter(phone=phone).exists():
            messages.error(request, 'This phone number is already registered.')
        else:
            role, _ = Role.objects.get_or_create(name=role_choice)
            user = User.objects.create(
                role=role,
                full_name=full_name,
                email=email,
                phone=phone,
                password_hash=make_password(password),
                status='ACTIVE',
                email_verified=False,
            )
            if role_choice == 'OPERATOR':
                Operator.objects.create(user=user, company_name=company_name, verified=False)

            ok, err = _generate_and_send_otp(request, user)
            if not ok:
                messages.error(request, f'Account created, but the code email failed ({err}). Just login normally and we will resend it.')
                return redirect('login')

            request.session['otp_user_id'] = user.id
            messages.success(request, f'Account created! Enter the 6-digit code we emailed to {email} to verify your email and login.')
            return redirect('verify_otp')

    return render(request, 'register.html')


def verify_otp(request):
    """Second login step for operators: enter the emailed 6-digit code."""
    uid = request.session.get('otp_user_id')
    if not uid:
        messages.error(request, 'Please login first.')
        return redirect('login')

    user = User.objects.select_related('role').filter(id=uid).first()
    if user is None:
        request.session.pop('otp_user_id', None)
        return redirect('login')

    if request.method == 'POST':
        code = request.POST.get('code', '').strip()
        otp = LoginOTP.objects.filter(user=user, used=False).order_by('-created_at').first()

        if otp is None or otp.expires_at < timezone.now():
            log_login(request, 'FAILED', user=user, message='OTP expired or missing')
            messages.error(request, 'That code has expired. Click "Resend code" to get a new one.')
        elif otp.attempts >= OTP_MAX_TRIES:
            otp.used = True
            otp.save(update_fields=['used'])
            log_login(request, 'FAILED', user=user, message='Too many wrong OTP entries')
            messages.error(request, 'Too many wrong tries. Click "Resend code" to get a new one.')
        elif constant_time_compare(otp.code, code):
            otp.used = True
            otp.save(update_fields=['used'])
            if not user.email_verified:
                user.email_verified = True
                user.save(update_fields=['email_verified'])
            return _finish_login(request, user, 'Operator OTP verified - login complete')
        else:
            otp.attempts += 1
            otp.save(update_fields=['attempts'])
            log_login(request, 'FAILED', user=user,
                      message=f'Wrong OTP code (try {otp.attempts}/{OTP_MAX_TRIES})')
            messages.error(request, f'Wrong code. Try {otp.attempts} of {OTP_MAX_TRIES}.')

    return render(request, 'verify_otp.html', {'email': user.email})


def resend_otp(request):
    uid = request.session.get('otp_user_id')
    if not uid:
        return redirect('login')
    user = User.objects.select_related('role').filter(id=uid).first()
    if user is None:
        return redirect('login')
    ok, err = _generate_and_send_otp(request, user)
    if ok:
        messages.success(request, f'A new code was emailed to {user.email}.')
    else:
        messages.error(request, err)
    return redirect('verify_otp')


# ---------------------------------------------------------------------------
# Google OAuth login (operators only)
# ---------------------------------------------------------------------------

def google_login(request):
    """Step 1: send the user to Google's consent screen."""
    role = request.GET.get('role', 'PASSENGER')
    if role not in ('PASSENGER', 'OPERATOR'):
        role = 'PASSENGER'
    request.session['google_signup_role'] = role

    state = secrets.token_urlsafe(24)
    request.session['google_oauth_state'] = state
    params = {
        'client_id': settings.GOOGLE_CLIENT_ID,
        'redirect_uri': settings.GOOGLE_CALLBACK_URL,
        'response_type': 'code',
        'scope': 'openid email profile',
        'state': state,
        'prompt': 'select_account',
    }
    return redirect('https://accounts.google.com/o/oauth2/v2/auth?' + urlencode(params))


def google_callback(request):
    """Step 2: Google sends the user back here with a code; we log them in."""
    if request.GET.get('error'):
        messages.error(request, f"Google login cancelled ({request.GET['error']}).")
        return redirect('login')

    saved_state = request.session.pop('google_oauth_state', None)
    if not saved_state or saved_state != request.GET.get('state'):
        messages.error(request, 'Google login failed (state mismatch). Please try again.')
        return redirect('login')

    code = request.GET.get('code')
    if not code:
        messages.error(request, 'Google login failed (no code returned).')
        return redirect('login')

    try:
        token_resp = requests.post(
            'https://oauth2.googleapis.com/token',
            data={
                'client_id': settings.GOOGLE_CLIENT_ID,
                'client_secret': settings.GOOGLE_CLIENT_SECRET,
                'code': code,
                'grant_type': 'authorization_code',
                'redirect_uri': settings.GOOGLE_CALLBACK_URL,
            },
            timeout=15,
        )
        access_token = token_resp.json().get('access_token')
        if not access_token:
            messages.error(request, 'Google login failed (could not get token).')
            return redirect('login')

        info = requests.get(
            'https://www.googleapis.com/oauth2/v3/userinfo',
            headers={'Authorization': f'Bearer {access_token}'},
            timeout=15,
        ).json()
    except requests.RequestException:
        messages.error(request, 'Could not reach Google. Check your internet and try again.')
        return redirect('login')

    email = info.get('email')
    if not email:
        messages.error(request, 'Google did not return an email address.')
        return redirect('login')

    chosen_role = request.session.pop('google_signup_role', 'PASSENGER')
    if chosen_role not in ('PASSENGER', 'OPERATOR'):
        chosen_role = 'PASSENGER'

    user = User.objects.select_related('role').filter(email__iexact=email).first()
    created = False

    if user is None:
        # First Google login with this email -> create the account for the chosen role
        role_obj, _ = Role.objects.get_or_create(name=chosen_role)
        user = User.objects.create(
            role=role_obj,
            full_name=info.get('name') or email.split('@')[0],
            email=email,
            password_hash=make_password(None),  # unusable password: Google-only account
            email_verified=True,                # Google already verified this email
            status='ACTIVE',
        )
        created = True

    if user.role.name == 'ADMIN':
        log_login(request, 'FAILED', user=user, message='Google login not allowed for admin accounts')
        messages.error(request, 'Admin accounts must login with email + password.')
        return redirect('login')

    if user.status != 'ACTIVE':
        log_login(request, 'FAILED', user=user, message=f'Google login blocked, account {user.status}')
        messages.error(request, f'Your account is {user.status}.')
        return redirect('login')

    # Same email can act as PASSENGER and/or OPERATOR - grant the role they picked
    grant_role(user, chosen_role)

    if created:
        kind = 'operator' if chosen_role == 'OPERATOR' else 'user'
        messages.success(request, f'New {kind} account created for {email} via Google.')
        return _finish_login(request, user, f'Google login - new {kind} account auto-created',
                             active_role=chosen_role)

    return _finish_login(request, user, f'Google OAuth login as {chosen_role}',
                         active_role=chosen_role)

def logout_view(request):
    user_id = request.session.get('user_id')
    if user_id:
        user = User.objects.select_related('role').filter(id=user_id).first()
        log_login(request, 'LOGOUT', user=user, message='User logged out')
    for key in ('user_id', 'user_name', 'user_role', 'otp_user_id'):
        request.session.pop(key, None)
    messages.success(request, 'You have been logged out.')
    return redirect('login')


# ---------------------------------------------------------------------------
# Login log report (ADMIN only)
# ---------------------------------------------------------------------------

@role_required('ADMIN')
def login_logs(request):
    """Login log report - only the ADMIN role can open this page."""
    logs = LoginLog.objects.select_related('user', 'user__role').all()[:100]
    stats = {
        'total': LoginLog.objects.count(),
        'success': LoginLog.objects.filter(status='SUCCESS').count(),
        'failed': LoginLog.objects.filter(status='FAILED').count(),
        'locked': LoginLog.objects.filter(status='LOCKED').count(),
        'logout': LoginLog.objects.filter(status='LOGOUT').count(),
    }
    return render(request, 'login_logs.html', {'logs': logs, 'stats': stats})


@role_required('ADMIN')
def download_login_logs(request):
    """Download the complete login log as a CSV file (Excel-compatible)."""
    response = HttpResponse(content_type='text/csv')
    filename = f'login_logs_{timezone.localtime():%Y-%m-%d_%H%M}.csv'
    response['Content-Disposition'] = f'attachment; filename="{filename}"'
    response.write('\ufeff')  # BOM so Excel shows the file correctly

    writer = csv.writer(response)
    writer.writerow(['ID', 'Date & Time', 'Email', 'User', 'Role', 'Status', 'IP Address', 'Details', 'Browser / Device'])

    for log in LoginLog.objects.select_related('user').order_by('created_at'):
        writer.writerow([
            log.id,
            timezone.localtime(log.created_at).strftime('%Y-%m-%d %H:%M:%S'),
            log.email_attempted,
            log.user.full_name if log.user else '',
            log.role_name or '',
            log.status,
            log.ip_address or '',
            log.message or '',
            log.user_agent or '',
        ])
    return response


# ---------------------------------------------------------------------------
# Existing test pages (protected by login)
# ---------------------------------------------------------------------------

@login_required_custom
def dashboard(request):
    stats = {
        'total_users': User.objects.count(),
        'total_operators': Operator.objects.count(),
        'total_buses': Bus.objects.count(),
        'total_routes': Route.objects.count(),
        'total_schedules': Schedule.objects.count(),
        'total_bookings': Booking.objects.count(),
        'total_cities': City.objects.count(),
    }
    return render(request, 'dashboard.html', {'stats': stats})


@login_required_custom
def test_users(request):
    users = User.objects.select_related('role').all()[:50]
    return render(request, 'test_users.html', {'users': users})


@login_required_custom
def test_operators(request):
    operators = Operator.objects.select_related('user').all()[:50]
    return render(request, 'test_operators.html', {'operators': operators})


@login_required_custom
def test_buses(request):
    buses = Bus.objects.select_related('operator__user').all()[:50]
    return render(request, 'test_buses.html', {'buses': buses})


@login_required_custom
def test_routes(request):
    routes = Route.objects.select_related('source_city', 'destination_city').all()[:50]
    return render(request, 'test_routes.html', {'routes': routes})


@login_required_custom
def test_schedules(request):
    schedules = Schedule.objects.select_related('bus', 'route__source_city', 'route__destination_city').all()[:50]
    return render(request, 'test_schedules.html', {'schedules': schedules})


@login_required_custom
def test_bookings(request):
    bookings = Booking.objects.select_related('user', 'schedule__bus', 'schedule__route').all()[:50]
    return render(request, 'test_bookings.html', {'bookings': bookings})

# ---------------------------------------------------------------------------
# Bus approval + booking + admin commission
# ---------------------------------------------------------------------------

COMMISSION_RATE = Decimal('10.00')   # admin takes 10% tax/commission per ticket


def current_user(request):
    uid = request.session.get('user_id')
    if not uid:
        return None
    return User.objects.select_related('role').filter(id=uid).first()

def log_audit(request, action, target, reason_code=None, note=None):
    AdminAudit.objects.create(
        admin=current_user(request),
        action=action, target=target,
        reason_code=reason_code, note=note,
    )

REASON_CODES = ['POLICY_VIOLATION', 'FRAUD', 'SPAM', 'USER_REQUEST', 'PAYMENT_ISSUE', 'OTHER']


def _my_operator(request):
    user = current_user(request)
    return Operator.objects.filter(user=user).first() if user else None


# ----- OPERATOR: my buses -----

@role_required('OPERATOR')
def operator_buses(request):
    op = _my_operator(request)
    buses = Bus.objects.filter(operator=op).order_by('-id') if op else []
    return render(request, 'operator_buses.html', {'operator': op, 'buses': buses})


@role_required('OPERATOR')
def add_bus(request):
    op = _my_operator(request)
    if op is None:
        messages.error(request, 'No operator profile found for your account. Please contact the admin.')
        return redirect('dashboard')

    # Admin must give access (verify the operator) before buses can be added
    if not op.verified:
        messages.error(request, 'Your operator account is not approved by the admin yet, so you cannot add buses.')
        return redirect('operator_buses')

    if request.method == 'POST':
        bus_name = request.POST.get('bus_name', '').strip()
        registration_no = request.POST.get('registration_no', '').strip()
        bus_type = request.POST.get('bus_type', 'NORMAL')
        total_seats = request.POST.get('total_seats', '').strip()

        if not bus_name or not registration_no or not total_seats:
            messages.error(request, 'Please fill in all required fields.')
        elif not total_seats.isdigit() or int(total_seats) < 1:
            messages.error(request, 'Total seats must be a positive number.')
        elif Bus.objects.filter(registration_no=registration_no).exists():
            messages.error(request, 'A bus with this registration number already exists.')
        else:
            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',
            )
            messages.success(request, 'Bus submitted! It will be visible once the admin approves it.')
            return redirect('operator_buses')

    return render(request, 'add_bus.html', {'bus_types': Bus.BUS_TYPE_CHOICES})

@role_required('OPERATOR')
def edit_bus(request, bus_id):
    op = _my_operator(request)
    if op is None:
        messages.error(request, 'No operator profile found for your account.')
        return redirect('dashboard')

    bus = Bus.objects.filter(id=bus_id, operator=op).first()
    if bus is None:
        messages.error(request, 'Bus not found, or it does not belong to your account.')
        return redirect('operator_buses')

    # Approved buses are locked (they may already have scheduled trips)
    if bus.approval_status == 'APPROVED':
        messages.error(request, 'This bus is already approved and cannot be edited. Contact the admin to make changes.')
        return redirect('operator_buses')

    if request.method == 'POST':
        bus_name = request.POST.get('bus_name', '').strip()
        registration_no = request.POST.get('registration_no', '').strip()
        bus_type = request.POST.get('bus_type', 'NORMAL')
        total_seats = request.POST.get('total_seats', '').strip()

        if not bus_name or not registration_no or not total_seats:
            messages.error(request, 'Please fill in all required fields.')
        elif not total_seats.isdigit() or int(total_seats) < 1:
            messages.error(request, 'Total seats must be a positive number.')
        elif Bus.objects.filter(registration_no=registration_no).exclude(id=bus.id).exists():
            messages.error(request, 'Another bus with this registration number already exists.')
        else:
            bus.bus_name = bus_name
            bus.registration_no = registration_no
            bus.bus_type = bus_type
            bus.total_seats = int(total_seats)
            # Editing a declined/pending bus sends it back for admin review
            bus.approval_status = 'PENDING'
            bus.review_note = None
            bus.reviewed_at = None
            bus.reviewed_by = None
            bus.save()
            messages.success(request, 'Bus updated and resubmitted for admin review.')
            return redirect('operator_buses')

    return render(request, 'edit_bus.html', {'bus': bus, 'bus_types': Bus.BUS_TYPE_CHOICES})


# ----- ADMIN: give operator access -----

@role_required('ADMIN')
def manage_operators(request):
    if request.method == 'POST':
        op = Operator.objects.select_related('user').filter(id=request.POST.get('operator_id')).first()
        if op:
            op.verified = (request.POST.get('action') == 'verify')
            op.save(update_fields=['verified'])
            state = 'granted access to' if op.verified else 'revoked access from'
            messages.success(request, f'You {state} {op.company_name}.')
        return redirect('manage_operators')

    operators = Operator.objects.select_related('user').order_by('-id')
    return render(request, 'manage_operators.html', {'operators': operators})


# ----- ADMIN: approve / reject buses -----

@role_required('ADMIN')
def manage_buses(request):
    if request.method == 'POST':
        bus = Bus.objects.select_related('operator').filter(id=request.POST.get('bus_id')).first()
        action = request.POST.get('action')
        note = request.POST.get('review_note', '').strip()

        if bus is None:
            messages.error(request, 'Bus not found.')
        elif action == 'approve':
            bus.approval_status = 'APPROVED'
            bus.review_note = note or 'Approved by admin.'
            bus.reviewed_at = timezone.now()
            bus.reviewed_by = current_user(request)
            bus.save()
            messages.success(request, f'{bus.bus_name} approved - it is now bookable.')
        elif action == 'reject':
            if not note:
                messages.error(request, 'Please write a reason before declining a bus.')
                return redirect('manage_buses')
            bus.approval_status = 'REJECTED'
            bus.review_note = note
            bus.reviewed_at = timezone.now()
            bus.reviewed_by = current_user(request)
            bus.save()
            messages.success(request, f'{bus.bus_name} declined. The operator will see your reason.')
        return redirect('manage_buses')

    pending = Bus.objects.select_related('operator').filter(approval_status='PENDING').order_by('id')
    reviewed = Bus.objects.select_related('operator').exclude(approval_status='PENDING').order_by('-reviewed_at')[:20]
    return render(request, 'manage_buses.html', {'pending': pending, 'reviewed': reviewed})


# ----- PASSENGER: book a ticket -----

ACTIVE_BOOKING_STATES = ['PENDING', 'PENDING_PAYMENT', 'CONFIRMED', 'CHECKED_IN', 'COMPLETED']
# States that must never count as revenue (unpaid or refunded)
NON_REVENUE_STATES = ['CANCELLED', 'REFUNDED', 'PENDING_PAYMENT', 'EXPIRED']


@role_required('PASSENGER')
def book_ticket(request):
    from .views_payments import expire_stale_bookings
    expire_stale_bookings()
    user = current_user(request)

    if request.method == 'POST':
        schedule = Schedule.objects.select_related('bus', 'route').filter(
            id=request.POST.get('schedule_id'), status='ACTIVE'
        ).first()

        if schedule is None:
            messages.error(request, 'That trip is not available.')
        elif schedule.bus.approval_status != 'APPROVED':
            messages.error(request, 'That bus is not approved by the admin.')
        elif schedule.bus.has_seat_map:
            # ---- Seat-map flow: book exactly the seats this user currently holds ----
            seat_numbers = [s for s in request.POST.get('seat_numbers', '').split(',') if s]
            if not seat_numbers:
                messages.error(request, 'Please select at least 1 seat.')
                return redirect(f"/book/?schedule={schedule.id}")

            now = timezone.now()
            held_locks = list(SeatLock.objects.select_related('seat').filter(
                schedule=schedule, locked_by=user, expires_at__gt=now,
                seat__seat_number__in=seat_numbers,
            ))
            if len(held_locks) != len(set(seat_numbers)):
                messages.error(request, 'Your seat selection expired - please select your seats again.')
                return redirect(f"/book/?schedule={schedule.id}")

            seats = len(held_locks)
            total = (schedule.fare * seats).quantize(Decimal('0.01'))
            commission = (total * COMMISSION_RATE / 100).quantize(Decimal('0.01'))

            while True:
                code = f"YB{timezone.now():%y%m%d}{secrets.randbelow(1000000):06d}"
                if not Booking.objects.filter(booking_code=code).exists():
                    break

            from .views_payments import PAYMENT_WINDOW_MINUTES
            with db_transaction.atomic():
                booking = Booking.objects.create(
                    booking_code=code, user=user, schedule=schedule, seats_count=seats,
                    total_amount=total, commission_rate=COMMISSION_RATE,
                    commission_amount=commission, operator_earning=total - commission,
                    booking_status='PENDING_PAYMENT',
                    payment_deadline=timezone.now() + timedelta(minutes=PAYMENT_WINDOW_MINUTES),
                )
                BookingSeat.objects.bulk_create([
                    BookingSeat(booking=booking, schedule=schedule, seat=lock.seat)
                    for lock in held_locks
                ])
                SeatLock.objects.filter(id__in=[l.id for l in held_locks]).delete()

            from .realtime import broadcast_seat_update
            broadcast_seat_update(schedule.id, seat_numbers, 'BOOKED')
            log_activity(user, 'PASSENGER', 'BOOKING_CREATED',
                        f'{code} - {seats} seat(s) ({", ".join(sorted(seat_numbers))}) on '
                        f'{schedule.route.source_city.city_name} → {schedule.route.destination_city.city_name}',
                        amount=total)

            messages.success(
                request,
                f'Seats {", ".join(sorted(seat_numbers))} held for {PAYMENT_WINDOW_MINUTES} minutes - '
                f'complete the eSewa payment to confirm booking {code}.'
            )
            return redirect('checkout', booking_id=booking.id)
        else:
            # ---- Legacy flow: quantity only (buses without a configured seat map) ----
            seats_raw = request.POST.get('seats', '1')
            seats = int(seats_raw) if seats_raw.isdigit() and int(seats_raw) > 0 else 0
            if seats < 1:
                messages.error(request, 'Please choose at least 1 seat.')
            else:
                taken = Booking.objects.filter(
                    schedule=schedule, booking_status__in=ACTIVE_BOOKING_STATES
                ).aggregate(n=Sum('seats_count'))['n'] or 0
                available = schedule.bus.total_seats - taken

                if seats > available:
                    messages.error(request, f'Only {available} seat(s) left on this trip.')
                else:
                    total = (schedule.fare * seats).quantize(Decimal('0.01'))
                    commission = (total * COMMISSION_RATE / 100).quantize(Decimal('0.01'))

                    while True:
                        code = f"YB{timezone.now():%y%m%d}{secrets.randbelow(1000000):06d}"
                        if not Booking.objects.filter(booking_code=code).exists():
                            break

                    from .views_payments import PAYMENT_WINDOW_MINUTES
                    booking = Booking.objects.create(
                        booking_code=code,
                        user=user,
                        schedule=schedule,
                        seats_count=seats,
                        total_amount=total,
                        commission_rate=COMMISSION_RATE,
                        commission_amount=commission,
                        operator_earning=total - commission,
                        booking_status='PENDING_PAYMENT',
                        payment_deadline=timezone.now() + timedelta(minutes=PAYMENT_WINDOW_MINUTES),
                    )
                    log_activity(user, 'PASSENGER', 'BOOKING_CREATED',
                                f'{code} - {seats} seat(s) on '
                                f'{schedule.route.source_city.city_name} → {schedule.route.destination_city.city_name}',
                                amount=total)
                    messages.success(
                        request,
                        f'{seats} seat(s) held for {PAYMENT_WINDOW_MINUTES} minutes - '
                        f'complete the eSewa payment to confirm booking {code}.'
                    )
                    return redirect('checkout', booking_id=booking.id)

    from testing_app.models import BusDocument
    from django.db.models import Count, Q as DQ
    required_docs = len(BusDocument.REQUIRED_DOC_TYPES)
    schedules = list(Schedule.objects.select_related(
        'bus', 'bus__operator', 'route__source_city', 'route__destination_city'
    ).filter(
        status='ACTIVE',
        bus__approval_status='APPROVED',
        bus__status='ACTIVE',
        departure_time__gte=timezone.now(),
    ).annotate(
        verified_docs=Count('bus__documents', filter=DQ(bus__documents__status='VERIFIED'), distinct=True)
    ).filter(verified_docs__gte=required_docs).order_by('departure_time')[:50])

    taken_map = {
        row['schedule']: row['n']
        for row in Booking.objects.filter(
            schedule__in=schedules, booking_status__in=ACTIVE_BOOKING_STATES
        ).values('schedule').annotate(n=Sum('seats_count'))
    }
    for s in schedules:
        s.available_seats = s.bus.total_seats - taken_map.get(s.id, 0)

    return render(request, 'book_ticket.html', {'schedules': schedules})


@role_required('PASSENGER')
def my_bookings(request):
    from .views_payments import expire_stale_bookings
    expire_stale_bookings()
    user = current_user(request)
    bookings = Booking.objects.select_related(
        'schedule__bus', 'schedule__route__source_city', 'schedule__route__destination_city'
    ).filter(user=user).order_by('-booked_at')[:50]
    return render(request, 'my_bookings.html', {
        'bookings': bookings, 'now': timezone.now(),
        'confirmed_like': ['CONFIRMED', 'CHECKED_IN'],
    })


# ----- ADMIN: commission report -----

@role_required('ADMIN')
def commission_report(request):
    qs = Booking.objects.exclude(booking_status__in=['CANCELLED', 'REFUNDED', 'PENDING_PAYMENT', 'EXPIRED'])

    totals = qs.aggregate(
        tickets=Count('id'),
        seats=Sum('seats_count'),
        sales=Sum('total_amount'),
        commission=Sum('commission_amount'),
        operator=Sum('operator_earning'),
    )
    per_operator = qs.values(
        'schedule__bus__operator__company_name'
    ).annotate(
        tickets=Count('id'),
        sales=Sum('total_amount'),
        commission=Sum('commission_amount'),
    ).order_by('-commission')

    bookings = qs.select_related(
        'user', 'schedule__bus__operator'
    ).order_by('-booked_at')[:50]

    return render(request, 'commission_report.html', {
        'totals': totals, 'per_operator': per_operator,
        'bookings': bookings, 'rate': COMMISSION_RATE,
    })
    
    # ---------------------------------------------------------------------------
# Dashboards (FR-19 operator, FR-25 admin)
# ---------------------------------------------------------------------------

@role_required('OPERATOR')
def operator_dashboard(request):
    op = _my_operator(request)
    ctx = {'operator': op}

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

        today_trips = 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
        occupancy = round(booked_seats / total_seats * 100, 1) if total_seats else 0

        buses = Bus.objects.filter(operator=op)
        ctx.update({
            'today_trips': today_trips,
            'today_trip_count': today_trips.count(),
            'total_bookings': agg['total'] or 0,
            'gross': agg['gross'] or 0,
            'net': agg['net'] or 0,
            'commission': agg['commission'] or 0,
            'bus_count': buses.count(),
            'approved_bus_count': buses.filter(approval_status='APPROVED').count(),
            'active_trip_count': active_trips.count(),
            'total_seats': total_seats,
            'booked_seats': booked_seats,
            'occupancy': occupancy,
        })

    return render(request, 'operator_dashboard.html', ctx)


@role_required('ADMIN')
def admin_dashboard(request):
    now = timezone.now()
    paid = Booking.objects.exclude(booking_status__in=['CANCELLED', 'REFUNDED', 'PENDING_PAYMENT', 'EXPIRED'])
    money = paid.aggregate(commission=Sum('commission_amount'), gross=Sum('total_amount'))

    ctx = {
        'total_users': User.objects.count(),
        'total_passengers': User.objects.filter(role__name='PASSENGER').count(),
        'total_operators': Operator.objects.count(),
        'verified_operators': Operator.objects.filter(verified=True).count(),
        'pending_operators': Operator.objects.filter(verified=False).count(),
        'total_buses': Bus.objects.count(),
        'pending_buses': Bus.objects.filter(approval_status='PENDING').count(),
        'approved_buses': Bus.objects.filter(approval_status='APPROVED').count(),
        'total_bookings': paid.count(),
        'platform_revenue': money['commission'] or 0,
        'gross_sales': money['gross'] or 0,
        'active_trips': Schedule.objects.filter(status='ACTIVE', departure_time__gte=now).count(),
        'recent_bookings': paid.select_related(
            'user', 'schedule__bus__operator'
        ).order_by('-booked_at')[:10],
    }
    return render(request, 'admin_dashboard.html', ctx)

# ---------------------------------------------------------------------------
# FR-21  Operator Route management  (admin keeps Django-admin access too)
# ---------------------------------------------------------------------------

@role_required('OPERATOR')
def operator_routes(request):
    routes = Route.objects.select_related('source_city', 'destination_city').order_by('-id')
    return render(request, 'operator_routes.html', {'routes': routes})


@role_required('OPERATOR')
def add_route(request):
    cities = City.objects.order_by('city_name')
    if request.method == 'POST':
        src = request.POST.get('source_city')
        dst = request.POST.get('destination_city')
        distance = request.POST.get('distance_km', '').strip()
        duration = request.POST.get('estimated_duration_minutes', '').strip()

        if not src or not dst:
            messages.error(request, 'Please choose both source and destination cities.')
        elif src == dst:
            messages.error(request, 'Source and destination cannot be the same city.')
        elif Route.objects.filter(source_city_id=src, destination_city_id=dst).exists():
            messages.error(request, 'That route already exists.')
        else:
            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=current_user(request),
            )
            messages.success(request, 'Route created.')
            return redirect('operator_routes')

    return render(request, 'route_form.html', {'cities': cities, 'route': None})


@role_required('OPERATOR')
def edit_route(request, route_id):
    route = Route.objects.filter(id=route_id).first()
    if route is None:
        messages.error(request, 'Route not found.')
        return redirect('operator_routes')
    cities = City.objects.order_by('city_name')

    if request.method == 'POST':
        src = request.POST.get('source_city')
        dst = request.POST.get('destination_city')
        distance = request.POST.get('distance_km', '').strip()
        duration = request.POST.get('estimated_duration_minutes', '').strip()

        if not src or not dst:
            messages.error(request, 'Please choose both cities.')
        elif src == dst:
            messages.error(request, 'Source and destination cannot be the same city.')
        else:
            route.source_city_id = src
            route.destination_city_id = dst
            route.distance_km = distance or None
            route.estimated_duration_minutes = int(duration) if duration.isdigit() else None
            route.save()
            messages.success(request, 'Route updated.')
            return redirect('operator_routes')

    return render(request, 'route_form.html', {'cities': cities, 'route': route})


@role_required('OPERATOR')
def toggle_route(request, route_id):
    route = Route.objects.filter(id=route_id).first()
    if route:
        route.is_active = not route.is_active
        route.save(update_fields=['is_active'])
        messages.success(request, f"Route {'activated' if route.is_active else 'deactivated'}.")
    return redirect('operator_routes')

# ---------------------------------------------------------------------------
# FR-22  Operator Schedule management
# ---------------------------------------------------------------------------

@role_required('OPERATOR')
def operator_schedules(request):
    op = _my_operator(request)
    schedules = Schedule.objects.select_related(
        'bus', 'route__source_city', 'route__destination_city'
    ).filter(bus__operator=op).order_by('-departure_time') if op else []
    return render(request, 'operator_schedules.html', {'schedules': schedules})


@role_required('OPERATOR')
def add_schedule(request):
    op = _my_operator(request)
    if op is None or not op.verified:
        messages.error(request, 'Your operator account must be approved before creating trips.')
        return redirect('operator_schedules')

    buses = Bus.objects.filter(operator=op, approval_status='APPROVED', status='ACTIVE')
    routes = Route.objects.filter(is_active=True).select_related('source_city', 'destination_city')

    if request.method == 'POST':
        bus_id = request.POST.get('bus')
        route_id = request.POST.get('route')
        departure = request.POST.get('departure_time', '').strip()
        arrival = request.POST.get('arrival_time', '').strip()
        fare = request.POST.get('fare', '').strip()

        if not bus_id or not route_id or not departure or not arrival or not fare:
            messages.error(request, 'Please fill in all fields.')
        elif not Bus.objects.filter(id=bus_id, operator=op, approval_status='APPROVED').exists():
            messages.error(request, 'Pick one of your approved buses.')
        else:
            Schedule.objects.create(
                bus_id=bus_id,
                route_id=route_id,
                departure_time=departure,
                arrival_time=arrival,
                fare=fare,
                status='ACTIVE',
            )
            messages.success(request, 'Trip scheduled. Passengers can now book it.')
            return redirect('operator_schedules')

    return render(request, 'schedule_form.html', {'buses': buses, 'routes': routes, 'schedule': None})


@role_required('OPERATOR')
def edit_schedule(request, schedule_id):
    op = _my_operator(request)
    schedule = Schedule.objects.filter(id=schedule_id, bus__operator=op).first()
    if schedule is None:
        messages.error(request, 'Trip not found.')
        return redirect('operator_schedules')

    buses = Bus.objects.filter(operator=op, approval_status='APPROVED', status='ACTIVE')
    routes = Route.objects.filter(is_active=True).select_related('source_city', 'destination_city')

    if request.method == 'POST':
        schedule.bus_id = request.POST.get('bus')
        schedule.route_id = request.POST.get('route')
        schedule.departure_time = request.POST.get('departure_time')
        schedule.arrival_time = request.POST.get('arrival_time')
        schedule.fare = request.POST.get('fare')
        schedule.save()
        messages.success(request, 'Trip updated.')
        return redirect('operator_schedules')

    return render(request, 'schedule_form.html', {'buses': buses, 'routes': routes, 'schedule': schedule})

# ---------------------------------------------------------------------------
# FR-22 / FR-23  Cancellation + refund
# ---------------------------------------------------------------------------

def _refund_booking(booking, reason, cancelled_by='OPERATOR', reason_code=None):
    """Operator/admin-initiated cancellation: ALWAYS a full refund (BR-7)."""
    booking.booking_status = 'REFUNDED'
    booking.cancel_reason = reason
    booking.cancelled_by = cancelled_by
    booking.cancel_reason_code = reason_code
    booking.cancellation_fee_amount = Decimal('0.00')
    booking.cancelled_at = timezone.now()
    booking.save(update_fields=['booking_status', 'cancel_reason', 'cancelled_by',
                                'cancel_reason_code', 'cancellation_fee_amount', 'cancelled_at'])
    Payment.objects.filter(booking=booking, status='COMPLETE').update(status='FULL_REFUND')
    seat_numbers = list(booking.booking_seats.values_list('seat__seat_number', flat=True))
    if seat_numbers:
        from .realtime import broadcast_seat_update
        broadcast_seat_update(booking.schedule_id, seat_numbers, 'AVAILABLE')


CANCELLATION_PROCESSING_FEE_PCT = Decimal('5.00')  # BR-7: fee on >24h cancellations


def cancellation_quote(booking):
    """BR-7 tiers -> (refund_amount, fee_amount, tier_label).
    >24h: full refund minus processing fee · 6-24h: 50% · <6h: none."""
    total = booking.total_amount or Decimal('0.00')
    hours_left = (booking.schedule.departure_time - timezone.now()).total_seconds() / 3600
    if hours_left > 24:
        fee = (total * CANCELLATION_PROCESSING_FEE_PCT / 100).quantize(Decimal('0.01'))
        return (total - fee), fee, f'more than 24h before departure ({CANCELLATION_PROCESSING_FEE_PCT}% processing fee)'
    if hours_left >= 6:
        refund = (total * Decimal('50') / 100).quantize(Decimal('0.01'))
        return refund, total - refund, '6-24h before departure (50% refund)'
    return Decimal('0.00'), total, 'less than 6h before departure (no refund)'


@role_required('PASSENGER')
def cancel_booking(request, booking_id):
    user = current_user(request)
    booking = Booking.objects.select_related('schedule').filter(id=booking_id, user=user).first()
    if booking is None:
        messages.error(request, 'Booking not found.')
    elif booking.booking_status in ['CANCELLED', 'REFUNDED', 'EXPIRED']:
        messages.error(request, 'This booking is already cancelled.')
    elif booking.schedule.departure_time <= timezone.now():
        messages.error(request, 'This trip has already departed and cannot be cancelled.')
    elif booking.booking_status == 'PENDING_PAYMENT':
        # Nothing was paid - just release the hold
        booking.booking_status = 'CANCELLED'
        booking.cancel_reason = 'Cancelled by passenger before payment'
        booking.cancelled_by = 'PASSENGER'
        booking.cancelled_at = timezone.now()
        booking.save(update_fields=['booking_status', 'cancel_reason', 'cancelled_by', 'cancelled_at'])
        Payment.objects.filter(booking=booking, status__in=['INITIATED', 'PENDING']).update(status='CANCELED')
        seat_numbers = list(booking.booking_seats.values_list('seat__seat_number', flat=True))
        if seat_numbers:
            from .realtime import broadcast_seat_update
            broadcast_seat_update(booking.schedule_id, seat_numbers, 'AVAILABLE')
        messages.success(request, f'Booking {booking.booking_code} cancelled. Nothing was charged.')
        log_activity(user, 'PASSENGER', 'BOOKING_CANCELLED', f'{booking.booking_code} - cancelled before payment')
    else:
        refund, fee, tier = cancellation_quote(booking)
        booking.booking_status = 'REFUNDED' if refund > 0 else 'CANCELLED'
        booking.cancel_reason = f'Cancelled by passenger - {tier}'
        booking.cancelled_by = 'PASSENGER'
        booking.cancellation_fee_amount = fee
        booking.cancelled_at = timezone.now()
        booking.save(update_fields=['booking_status', 'cancel_reason', 'cancelled_by',
                                    'cancellation_fee_amount', 'cancelled_at'])
        if refund > 0:
            new_status = 'FULL_REFUND' if fee == 0 else 'PARTIAL_REFUND'
            Payment.objects.filter(booking=booking, status='COMPLETE').update(status=new_status)
        seat_numbers = list(booking.booking_seats.values_list('seat__seat_number', flat=True))
        if seat_numbers:
            from .realtime import broadcast_seat_update
            broadcast_seat_update(booking.schedule_id, seat_numbers, 'AVAILABLE')
        from .realtime import notify_operator
        notify_operator(booking.schedule.bus.operator_id, 'booking_cancelled',
                        {'booking_code': booking.booking_code, 'refund': float(refund)})
        if refund > 0:
            messages.success(request, f'Booking {booking.booking_code} cancelled ({tier}). '
                                      f'Refund due: Rs. {refund} (fee withheld: Rs. {fee}).')
        else:
            messages.success(request, f'Booking {booking.booking_code} cancelled ({tier}). No refund is due.')
        log_activity(user, 'PASSENGER', 'BOOKING_CANCELLED',
                    f'{booking.booking_code} - {tier} - refund Rs.{refund}', amount=refund)
    return redirect('my_bookings')


@role_required('OPERATOR')
def cancel_schedule(request, schedule_id):
    op = _my_operator(request)
    schedule = Schedule.objects.filter(id=schedule_id, bus__operator=op).first()
    if schedule is None:
        messages.error(request, 'Trip not found.')
        return redirect('operator_schedules')
    if schedule.status == 'CANCELLED':
        messages.error(request, 'This trip is already cancelled.')
        return redirect('operator_schedules')

    # Cancel the trip
    schedule.status = 'CANCELLED'
    schedule.save(update_fields=['status'])

    # Full refund to every affected passenger (FR-22)
    affected = Booking.objects.filter(
        schedule=schedule, booking_status__in=['PENDING', 'CONFIRMED', 'CHECKED_IN']
    )
    count = affected.count()
    for b in affected:
        _refund_booking(b, 'Trip cancelled by operator - full refund')

    messages.success(request, f'Trip cancelled. {count} passenger booking(s) fully refunded.')
    return redirect('operator_schedules')


@role_required('ADMIN')
def manage_users(request):
    if request.method == 'POST':
        target = User.objects.select_related('role').filter(id=request.POST.get('user_id')).first()
        action = request.POST.get('action')
        reason = request.POST.get('reason_code', '').strip()

        if target is None:
            messages.error(request, 'User not found.')
            return redirect('manage_users')
        if target.role.name == 'ADMIN':
            messages.error(request, 'Admin accounts cannot be changed here.')
            return redirect('manage_users')
        if action in ('DEACTIVATE', 'SUSPEND') and not reason:
            messages.error(request, 'A reason code is required to deactivate or suspend a user.')
            return redirect('manage_users')

        if action == 'ACTIVATE':
            target.status = 'ACTIVE'
            target.save(update_fields=['status'])
            log_audit(request, 'USER_ACTIVATE', target.email, note='Account re-activated')
            messages.success(request, f'{target.full_name} is now ACTIVE.')
        elif action == 'DEACTIVATE':
            target.status = 'INACTIVE'
            target.save(update_fields=['status'])
            log_audit(request, 'USER_DEACTIVATE', target.email, reason_code=reason)
            messages.success(request, f'{target.full_name} set to INACTIVE.')
        elif action == 'SUSPEND':
            target.status = 'SUSPENDED'
            target.save(update_fields=['status'])
            log_audit(request, 'USER_SUSPEND', target.email, reason_code=reason)
            messages.success(request, f'{target.full_name} SUSPENDED.')
        return redirect('manage_users')

    q = request.GET.get('q', '').strip()
    users = User.objects.select_related('role').filter(role__name='PASSENGER')
    if q:
        users = users.filter(models.Q(full_name__icontains=q) | models.Q(email__icontains=q))
    return render(request, 'manage_users.html', {
        'users': users.order_by('-id')[:100], 'q': q, 'reason_codes': REASON_CODES,
    })

@role_required('ADMIN')
def manage_operators(request):
    if request.method == 'POST':
        op = Operator.objects.select_related('user').filter(id=request.POST.get('operator_id')).first()
        action = request.POST.get('action')
        reason = request.POST.get('reason_code', '').strip()

        if op is None:
            messages.error(request, 'Operator not found.')
            return redirect('manage_operators')

        if action == 'verify':
            op.verified = True
            op.save(update_fields=['verified'])
            log_audit(request, 'OPERATOR_APPROVE', op.company_name)
            messages.success(request, f'Access granted to {op.company_name}.')

        elif action == 'revoke':
            op.verified = False
            op.save(update_fields=['verified'])
            log_audit(request, 'OPERATOR_REVOKE', op.company_name)
            messages.success(request, f'Access revoked from {op.company_name}.')

        elif action == 'suspend':
            if not reason:
                messages.error(request, 'A reason code is required to suspend an operator.')
                return redirect('manage_operators')
            op.verified = False
            op.save(update_fields=['verified'])
            # Cascade: hide future trips from search, keep history
            hidden = Schedule.objects.filter(
                bus__operator=op, status='ACTIVE', departure_time__gte=timezone.now()
            ).update(status='CANCELLED')
            log_audit(request, 'OPERATOR_SUSPEND', op.company_name, reason_code=reason,
                      note=f'{hidden} future trip(s) hidden from search')
            messages.success(request, f'{op.company_name} suspended. {hidden} future trip(s) hidden. History retained.')

        return redirect('manage_operators')

    operators = Operator.objects.select_related('user').order_by('-id')
    return render(request, 'manage_operators.html', {'operators': operators, 'reason_codes': REASON_CODES})

@role_required('ADMIN')
def manage_cities(request):
    if request.method == 'POST':
        action = request.POST.get('action')
        if action == 'add':
            name = request.POST.get('city_name', '').strip()
            if not name:
                messages.error(request, 'Enter a city name.')
            elif City.objects.filter(city_name__iexact=name).exists():
                messages.error(request, 'That city already exists.')
            else:
                City.objects.create(city_name=name)
                log_audit(request, 'CITY_ADD', name)
                messages.success(request, f'City "{name}" added.')
        elif action == 'delete':
            city = City.objects.filter(id=request.POST.get('city_id')).first()
            if city:
                used = Route.objects.filter(models.Q(source_city=city) | models.Q(destination_city=city)).exists()
                if used:
                    messages.error(request, f'Cannot delete "{city.city_name}" — it is used by a route.')
                else:
                    name = city.city_name
                    city.delete()
                    log_audit(request, 'CITY_DELETE', name)
                    messages.success(request, f'City "{name}" deleted.')
        return redirect('manage_cities')

    return render(request, 'manage_cities.html', {'cities': City.objects.order_by('city_name')})

@role_required('ADMIN')
def booking_oversight(request):
    if request.method == 'POST':
        booking = Booking.objects.select_related('user').filter(id=request.POST.get('booking_id')).first()
        note = request.POST.get('note', '').strip()
        if booking is None:
            messages.error(request, 'Booking not found.')
        elif booking.booking_status in ['CANCELLED', 'REFUNDED']:
            messages.error(request, 'This booking is already cancelled/refunded.')
        elif not note:
            messages.error(request, 'A justification note is required for a manual refund.')
        else:
            _refund_booking(booking, f'Admin manual refund: {note}', cancelled_by='ADMIN')
            log_audit(request, 'MANUAL_REFUND', booking.booking_code, note=note)
            messages.success(request, f'Booking {booking.booking_code} refunded.')
        return redirect('booking_oversight')

    q = request.GET.get('q', '').strip()
    bookings = Booking.objects.select_related('user', 'schedule__bus__operator',
                                              'schedule__route__source_city', 'schedule__route__destination_city')
    if q:
        bookings = bookings.filter(models.Q(booking_code__icontains=q) | models.Q(user__full_name__icontains=q))
    return render(request, 'booking_oversight.html', {'bookings': bookings.order_by('-booked_at')[:100], 'q': q})

# ---------------------------------------------------------------------------
# FR-17: rate a completed trip     ·     FR-15: e-ticket (PDF + signed QR)
# ---------------------------------------------------------------------------

@role_required('PASSENGER')
def rate_booking(request, booking_id):
    if request.method != 'POST':
        return redirect('my_bookings')
    user = current_user(request)
    booking = Booking.objects.select_related('schedule__bus__operator').filter(
        id=booking_id, user=user).first()
    if booking is None:
        messages.error(request, 'Booking not found.')
    elif booking.booking_status not in ('CONFIRMED', 'CHECKED_IN', 'COMPLETED'):
        messages.error(request, 'Only paid bookings can be rated.')
    elif booking.schedule.arrival_time > timezone.now():
        messages.error(request, 'You can rate this trip after it arrives.')
    elif hasattr(booking, 'rating'):
        messages.error(request, 'You already rated this trip.')
    else:
        stars_raw = request.POST.get('stars', '')
        stars = int(stars_raw) if stars_raw.isdigit() else 0
        if not (1 <= stars <= 5):
            messages.error(request, 'Pick a rating from 1 to 5 stars.')
        else:
            issue_flag = request.POST.get('issue_flag') == 'on'
            Rating.objects.create(
                booking=booking, user=user, operator=booking.schedule.bus.operator,
                stars=stars,
                review=request.POST.get('review', '').strip()[:1000],
                issue_flag=issue_flag,
                issue_note=request.POST.get('issue_note', '').strip()[:1000] if issue_flag else '',
            )
            log_activity(user, 'PASSENGER', 'RATING_SUBMITTED',
                        f'{stars}★ on {booking.booking_code}' + (' (issue reported)' if issue_flag else ''))
            if issue_flag:
                messages.success(request, 'Thanks - your rating is saved and the issue was sent to the admin team.')
            else:
                messages.success(request, f'Thanks for rating this trip {stars}★!')
    return redirect('my_bookings')


@role_required('PASSENGER')
def eticket_pdf(request, booking_id):
    """Downloadable PDF e-ticket with a signed QR (payload is a signed token,
    never raw booking data - per FR-15/BR-12)."""
    import io
    import qrcode
    from django.core import signing
    from django.http import HttpResponse
    from reportlab.lib.pagesizes import A5
    from reportlab.lib.units import mm
    from reportlab.pdfgen import canvas
    from reportlab.lib.utils import ImageReader

    user = current_user(request)
    b = Booking.objects.select_related(
        'schedule__bus__operator', 'schedule__route__source_city',
        'schedule__route__destination_city').filter(id=booking_id, user=user).first()
    if b is None or b.booking_status not in ('CONFIRMED', 'CHECKED_IN', 'COMPLETED'):
        messages.error(request, 'E-tickets are available for confirmed bookings only.')
        return redirect('my_bookings')

    token = signing.dumps({'t': 'ticket', 'b': b.id, 'c': b.booking_code})
    qr_img = qrcode.make(token, box_size=6, border=2)
    qr_buf = io.BytesIO(); qr_img.save(qr_buf, format='PNG'); qr_buf.seek(0)

    buf = io.BytesIO()
    w, h = A5[1], A5[0]  # landscape A5
    c = canvas.Canvas(buf, pagesize=(w, h))
    c.setFillColorRGB(0.05, 0.16, 0.4)
    c.rect(0, h - 26 * mm, w, 26 * mm, stroke=0, fill=1)
    c.setFillColorRGB(1, 1, 1)
    c.setFont('Helvetica-Bold', 16)
    c.drawString(12 * mm, h - 15 * mm, 'YATRA BOOKING · E-TICKET')
    c.setFont('Helvetica', 9)
    c.drawString(12 * mm, h - 21 * mm, b.schedule.bus.operator.company_name)

    c.setFillColorRGB(0.12, 0.16, 0.22)
    y = h - 38 * mm
    rows = [
        ('Booking code', b.booking_code),
        ('Passenger', user.full_name),
        ('Trip', f"{b.schedule.route.source_city.city_name}  →  {b.schedule.route.destination_city.city_name}"),
        ('Bus', f"{b.schedule.bus.bus_name or ''} ({b.schedule.bus.registration_no or ''})"),
        ('Departure', timezone.localtime(b.schedule.departure_time).strftime('%a, %d %b %Y · %I:%M %p')),
        ('Seats', ', '.join(sorted(bs.seat.seat_number for bs in b.booking_seats.select_related('seat')))
                  or f'{b.seats_count} seat(s)'),
        ('Paid', f'Rs. {b.total_amount}'),
        ('Status', b.booking_status),
    ]
    for label, value in rows:
        c.setFont('Helvetica', 8.5); c.setFillColorRGB(0.45, 0.48, 0.55)
        c.drawString(12 * mm, y, label.upper())
        c.setFont('Helvetica-Bold', 11); c.setFillColorRGB(0.12, 0.16, 0.22)
        c.drawString(45 * mm, y, str(value))
        y -= 9 * mm

    c.drawImage(ImageReader(qr_buf), w - 52 * mm, h - 82 * mm, 40 * mm, 40 * mm)
    c.setFont('Helvetica', 7); c.setFillColorRGB(0.45, 0.48, 0.55)
    c.drawCentredString(w - 32 * mm, h - 86 * mm, 'Scan to verify · cryptographically signed')
    c.showPage(); c.save()
    buf.seek(0)
    resp = HttpResponse(buf.read(), content_type='application/pdf')
    resp['Content-Disposition'] = f'attachment; filename="yatra-{b.booking_code}.pdf"'
    return resp
