import { Hono } from 'hono';
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';
import { db, dbGet, dbAll, tickets, events, users, payments, paymentOptions, eventPaymentOverrides, siteSettings, isSqlite } from '../db/index.js';
import { eq, and, or, sql, inArray } from 'drizzle-orm';
import { requireAuth, getAuthUser } from '../lib/auth.js';
import { generateId, generateTicketCode, getNow, toDbDate, toDbBool, calculateAvailableSeats, isEventSoldOut } from '../lib/utils.js';
import { createInvoice, isLNbitsConfigured, LNBITS_INVOICE_EXPIRY_SECONDS } from '../lib/lnbits.js';
import { rateLimitMiddleware } from '../lib/rateLimit.js';
import emailService from '../lib/email.js';
import { generateTicketPDF, generateCombinedTicketsPDF } from '../lib/pdf.js';
import { reserveOnHoldBooking, HoldCapacityError } from '../lib/holdRecovery.js';
import { seatHolderCountQuery } from '../lib/capacity.js';

const ticketsRouter = new Hono();

// Attendee info schema for multi-ticket bookings
const attendeeSchema = z.object({
  firstName: z.string().min(2),
  lastName: z.string().min(2).optional().or(z.literal('')),
});

// Maximum tickets a single buyer can book at once (enforced server-side)
const MAX_TICKETS_PER_BOOKING = 5;

const createTicketSchema = z.object({
  eventId: z.string(),
  firstName: z.string().min(2),
  lastName: z.string().min(2).optional().or(z.literal('')),
  email: z.string().email(),
  phone: z.string().min(6).optional().or(z.literal('')),
  preferredLanguage: z.enum(['en', 'es']).optional(),
  // 'bancard' intentionally excluded: no checkout integration exists for it
  paymentMethod: z.enum(['lightning', 'cash', 'bank_transfer', 'tpago']).default('cash'),
  // Base + optional "-" + check digit; digits-only kept for older clients, normalized to dashed form on save
  ruc: z.string().regex(/^(\d{6,10}|\d{5,8}-\d)$/, 'Invalid RUC format').optional().or(z.literal('')),
  // Optional: array of attendees for multi-ticket booking (capped at MAX_TICKETS_PER_BOOKING)
  attendees: z.array(attendeeSchema).min(1).max(MAX_TICKETS_PER_BOOKING).optional(),
});

// Canonical stored RUC form is "base-checkdigit" (e.g. 1234567-9); older clients send digits only
function normalizeRuc(ruc: string | undefined): string | null {
  if (!ruc) return null;
  if (ruc.includes('-')) return ruc;
  return `${ruc.slice(0, -1)}-${ruc.slice(-1)}`;
}

// Maps a payment provider to the merged payment-option flag that enables it
function isPaymentMethodEnabled(method: string, merged: Record<string, any>): boolean {
  const truthy = (v: any) => v === true || v === 1;
  switch (method) {
    case 'tpago':
      return truthy(merged.tpagoEnabled);
    case 'bank_transfer':
      return truthy(merged.bankTransferEnabled);
    case 'lightning':
      return truthy(merged.lightningEnabled);
    case 'cash':
      return truthy(merged.cashEnabled);
    default:
      return false;
  }
}

const updateTicketSchema = z.object({
  status: z.enum(['pending', 'confirmed', 'cancelled', 'checked_in', 'on_hold']).optional(),
  adminNote: z.string().optional(),
});

const updateNoteSchema = z.object({
  note: z.string().max(1000),
});

const adminCreateTicketSchema = z.object({
  eventId: z.string(),
  firstName: z.string().min(2),
  lastName: z.string().optional().or(z.literal('')),
  email: z.string().email().optional().or(z.literal('')),
  phone: z.string().optional().or(z.literal('')),
  preferredLanguage: z.enum(['en', 'es']).optional(),
  autoCheckin: z.boolean().optional().default(false),
  adminNote: z.string().max(1000).optional(),
});

// Book a ticket (public) - supports single or multi-ticket bookings
ticketsRouter.post('/', zValidator('json', createTicketSchema), async (c) => {
  const data = c.req.valid('json');
  const rucNumber = normalizeRuc(data.ruc);

  // Determine attendees list (use attendees array if provided, otherwise single attendee from main fields)
  const attendeesList = data.attendees && data.attendees.length > 0
    ? data.attendees
    : [{ firstName: data.firstName, lastName: data.lastName }];
  
  const ticketCount = attendeesList.length;

  // Enforce the per-booking ticket cap server-side (UI also caps, but the API is authoritative)
  if (ticketCount < 1 || ticketCount > MAX_TICKETS_PER_BOOKING) {
    return c.json({ error: `You can book between 1 and ${MAX_TICKETS_PER_BOOKING} tickets per order.` }, 400);
  }
  
  // Get event
  const event = await dbGet<any>(
    (db as any).select().from(events).where(eq((events as any).id, data.eventId))
  );
  if (!event) {
    return c.json({ error: 'Event not found' }, 404);
  }
  
  if (!['published', 'unlisted'].includes(event.status)) {
    return c.json({ error: 'Event is not available for booking' }, 400);
  }

  // Validate the requested payment method is actually enabled for this event
  // (merge global options with any event-level overrides; override wins when not null)
  const globalPaymentOptions = await dbGet<any>(
    (db as any).select().from(paymentOptions)
  );
  const eventOverrides = await dbGet<any>(
    (db as any).select().from(eventPaymentOverrides).where(eq((eventPaymentOverrides as any).eventId, data.eventId))
  );
  const mergedPaymentOptions: Record<string, any> = {
    tpagoEnabled: eventOverrides?.tpagoEnabled ?? globalPaymentOptions?.tpagoEnabled ?? false,
    bankTransferEnabled: eventOverrides?.bankTransferEnabled ?? globalPaymentOptions?.bankTransferEnabled ?? false,
    lightningEnabled: eventOverrides?.lightningEnabled ?? globalPaymentOptions?.lightningEnabled ?? true,
    cashEnabled: eventOverrides?.cashEnabled ?? globalPaymentOptions?.cashEnabled ?? true,
  };
  if (!isPaymentMethodEnabled(data.paymentMethod, mergedPaymentOptions)) {
    return c.json({ error: 'Selected payment method is not available for this event' }, 400);
  }
  
  // Check capacity against held seats (paid/checked-in tickets plus claimed
  // manual payments) — see lib/capacity.ts. Bare pending bookings hold no seat.
  const existingTicketCount = await dbGet<any>(
    seatHolderCountQuery(db, data.eventId)
  );

  const confirmedCount = existingTicketCount?.count || 0;
  const availableSeats = calculateAvailableSeats(event.capacity, confirmedCount);

  if (isEventSoldOut(event.capacity, confirmedCount)) {
    return c.json({ error: 'Event is sold out' }, 400);
  }

  if (ticketCount > availableSeats) {
    return c.json({
      error: `Not enough seats available. Only ${availableSeats} spot(s) remaining.`,
    }, 400);
  }
  
  // Find or create user
  let user = await dbGet<any>(
    (db as any).select().from(users).where(eq((users as any).email, data.email))
  );
  
  const now = getNow();
  
  const fullName = data.lastName && data.lastName.trim()
    ? `${data.firstName} ${data.lastName}`.trim()
    : data.firstName;
  
  if (!user) {
    const userId = generateId();
    user = {
      id: userId,
      email: data.email,
      password: null, // No password for guest bookings; set on claim (Better Auth credential account)
      name: fullName,
      phone: data.phone || null,
      role: 'user',
      languagePreference: null,
      rucNumber,
      isClaimed: toDbBool(false),
      accountStatus: 'unclaimed',
      emailVerified: false,
      createdAt: now,
      updatedAt: now,
    };
    await (db as any).insert(users).values(user);
  } else if (rucNumber) {
    // Keep the user's saved RUC up to date for future bookings, but never blank
    // out an existing value if this booking didn't include one.
    await (db as any)
      .update(users)
      .set({ rucNumber, updatedAt: now })
      .where(eq((users as any).id, user.id));
    user.rucNumber = rucNumber;
  }

  // Check for duplicate booking (unless allowDuplicateBookings is enabled)
  const allowDuplicateBookings = globalPaymentOptions?.allowDuplicateBookings ?? false;
  
  if (!allowDuplicateBookings) {
    const existingTicket = await dbGet<any>(
      (db as any)
        .select()
        .from(tickets)
        .where(
          and(
            eq((tickets as any).userId, user.id),
            eq((tickets as any).eventId, data.eventId)
          )
        )
    );
    
    if (existingTicket && existingTicket.status !== 'cancelled') {
      return c.json({ error: 'You have already booked this event' }, 400);
    }
  }
  
  // Generate booking ID to group multiple tickets
  const bookingId = generateId();

  // Atomically re-check capacity and insert tickets/payments inside a transaction
  // so concurrent bookings cannot oversell the same seats (TOCTOU race).
  class BookingCapacityError extends Error {
    constructor(public code: 'SOLD_OUT' | 'NOT_ENOUGH', public available?: number) {
      super(code);
    }
  }

  let createdTickets: any[] = [];
  let createdPayments: any[] = [];

  try {
    if (isSqlite()) {
      (db as any).transaction((tx: any) => {
        const countRow = seatHolderCountQuery(tx, data.eventId).get();
        const reserved = Number(countRow?.count || 0);
        if (isEventSoldOut(event.capacity, reserved)) {
          throw new BookingCapacityError('SOLD_OUT');
        }
        const seatsLeft = calculateAvailableSeats(event.capacity, reserved);
        if (ticketCount > seatsLeft) {
          throw new BookingCapacityError('NOT_ENOUGH', seatsLeft);
        }

        for (let i = 0; i < attendeesList.length; i++) {
          const attendee = attendeesList[i];
          const ticketId = generateId();
          const qrCode = generateTicketCode();
          const newTicket = {
            id: ticketId,
            bookingId: ticketCount > 1 ? bookingId : null,
            userId: user.id,
            eventId: data.eventId,
            attendeeFirstName: attendee.firstName,
            attendeeLastName: attendee.lastName && attendee.lastName.trim() ? attendee.lastName.trim() : null,
            attendeeEmail: data.email,
            attendeePhone: data.phone && data.phone.trim() ? data.phone.trim() : null,
            attendeeRuc: rucNumber,
            preferredLanguage: data.preferredLanguage || null,
            status: 'pending',
            qrCode,
            checkinAt: null,
            createdAt: now,
          };
          tx.insert(tickets).values(newTicket).run();
          createdTickets.push(newTicket);

          const paymentId = generateId();
          const newPayment = {
            id: paymentId,
            ticketId,
            provider: data.paymentMethod,
            amount: event.price,
            currency: event.currency,
            status: 'pending',
            reference: null,
            createdAt: now,
            updatedAt: now,
          };
          tx.insert(payments).values(newPayment).run();
          createdPayments.push(newPayment);
        }
      });
    } else {
      await (db as any).transaction(async (tx: any) => {
        const countRow = await dbGet<any>(seatHolderCountQuery(tx, data.eventId));
        const reserved = Number(countRow?.count || 0);
        if (isEventSoldOut(event.capacity, reserved)) {
          throw new BookingCapacityError('SOLD_OUT');
        }
        const seatsLeft = calculateAvailableSeats(event.capacity, reserved);
        if (ticketCount > seatsLeft) {
          throw new BookingCapacityError('NOT_ENOUGH', seatsLeft);
        }

        for (let i = 0; i < attendeesList.length; i++) {
          const attendee = attendeesList[i];
          const ticketId = generateId();
          const qrCode = generateTicketCode();
          const newTicket = {
            id: ticketId,
            bookingId: ticketCount > 1 ? bookingId : null,
            userId: user.id,
            eventId: data.eventId,
            attendeeFirstName: attendee.firstName,
            attendeeLastName: attendee.lastName && attendee.lastName.trim() ? attendee.lastName.trim() : null,
            attendeeEmail: data.email,
            attendeePhone: data.phone && data.phone.trim() ? data.phone.trim() : null,
            attendeeRuc: rucNumber,
            preferredLanguage: data.preferredLanguage || null,
            status: 'pending',
            qrCode,
            checkinAt: null,
            createdAt: now,
          };
          await tx.insert(tickets).values(newTicket);
          createdTickets.push(newTicket);

          const paymentId = generateId();
          const newPayment = {
            id: paymentId,
            ticketId,
            provider: data.paymentMethod,
            amount: event.price,
            currency: event.currency,
            status: 'pending',
            reference: null,
            createdAt: now,
            updatedAt: now,
          };
          await tx.insert(payments).values(newPayment);
          createdPayments.push(newPayment);
        }
      });
    }
  } catch (err: any) {
    if (err instanceof BookingCapacityError) {
      if (err.code === 'SOLD_OUT') {
        return c.json({ error: 'Event is sold out' }, 400);
      }
      return c.json({
        error: `Not enough seats available. Only ${err.available} spot(s) remaining.`,
      }, 400);
    }
    throw err;
  }
  
  const primaryTicket = createdTickets[0];
  const primaryPayment = createdPayments[0];
  
  // Send payment instructions email for manual payment methods (TPago, Bank Transfer)
  if (['bank_transfer', 'tpago'].includes(data.paymentMethod)) {
    // Send asynchronously - don't block the response
    emailService.sendPaymentInstructions(primaryTicket.id).then(result => {
      if (result.success) {
        console.log(`[Email] Payment instructions email sent successfully for ticket ${primaryTicket.id}`);
      } else {
        console.error(`[Email] Failed to send payment instructions email for ticket ${primaryTicket.id}:`, result.error);
      }
    }).catch(err => {
      console.error('[Email] Exception sending payment instructions email:', err);
    });
  }
  
  // If Lightning payment, create LNbits invoice (skip for free events — confirm immediately)
  let lnbitsInvoice = null;
  const totalPrice = event.price * ticketCount;

  // Free events: no payment step required — confirm tickets immediately
  if (totalPrice === 0) {
    for (const t of createdTickets) {
      await (db as any)
        .update(tickets)
        .set({ status: 'confirmed', paymentStatus: 'paid' })
        .where(and(eq((tickets as any).id, t.id), eq((tickets as any).status, 'pending')));
      await (db as any)
        .update(payments)
        .set({ status: 'paid', paidAt: now, updatedAt: now })
        .where(and(eq((payments as any).ticketId, t.id), eq((payments as any).status, 'pending')));
    }
    emailService.sendBookingConfirmation(primaryTicket.id).catch(err => {
      console.error('[Email] Failed to send free-booking confirmation:', err);
    });
  } else if (data.paymentMethod === 'lightning') {
    if (!isLNbitsConfigured()) {
      // Delete the tickets and payments we just created
      for (const payment of createdPayments) {
        await (db as any).delete(payments).where(eq((payments as any).id, payment.id));
      }
      for (const ticket of createdTickets) {
        await (db as any).delete(tickets).where(eq((tickets as any).id, ticket.id));
      }
      return c.json({ 
        error: 'Bitcoin Lightning payments are not available at this time' 
      }, 400);
    }
    
    try {
      const apiUrl = process.env.API_URL || 'http://localhost:3001';
      // Include the webhook secret (if configured) so the callback can be authenticated
      const webhookSecret = process.env.LNBITS_WEBHOOK_SECRET || '';
      const webhookUrl = webhookSecret
        ? `${apiUrl}/api/lnbits/webhook?token=${encodeURIComponent(webhookSecret)}`
        : `${apiUrl}/api/lnbits/webhook`;
      
      // Pass the fiat currency directly to LNbits - it handles conversion automatically
      // For multi-ticket, use total price
      lnbitsInvoice = await createInvoice({
        amount: totalPrice,
        unit: event.currency, // LNbits supports fiat currencies like USD, PYG, etc.
        memo: `Spanglish: ${event.title} - ${fullName}${ticketCount > 1 ? ` (${ticketCount} tickets)` : ''}`,
        webhookUrl,
        expiry: LNBITS_INVOICE_EXPIRY_SECONDS, // 15 minutes expiry for faster UX
        extra: {
          ticketId: primaryTicket.id,
          bookingId: ticketCount > 1 ? bookingId : null,
          ticketIds: createdTickets.map(t => t.id),
          eventId: event.id,
          eventTitle: event.title,
          attendeeName: fullName,
          attendeeEmail: data.email,
          ticketCount,
        },
      });

      const lnbitsExpiresAt = toDbDate(new Date(Date.now() + LNBITS_INVOICE_EXPIRY_SECONDS * 1000));

      // Update primary payment with the LNbits invoice - the BOLT11 string and
      // expiry are persisted so the "Pay now" page can redisplay this same
      // invoice later instead of erroring out on an unpaid Lightning booking.
      await (db as any)
        .update(payments)
        .set({
          reference: lnbitsInvoice.paymentHash,
          lnbitsInvoice: lnbitsInvoice.paymentRequest,
          lnbitsExpiresAt,
          lnbitsAmountSats: lnbitsInvoice.amount,
        })
        .where(eq((payments as any).id, primaryPayment.id));

      (primaryPayment as any).reference = lnbitsInvoice.paymentHash;
    } catch (error: any) {
      console.error('Failed to create Lightning invoice:', error);
      // Delete the tickets and payments we just created since Lightning payment failed
      for (const payment of createdPayments) {
        await (db as any).delete(payments).where(eq((payments as any).id, payment.id));
      }
      for (const ticket of createdTickets) {
        await (db as any).delete(tickets).where(eq((tickets as any).id, ticket.id));
      }
      return c.json({ 
        error: `Failed to create Lightning invoice: ${error.message || 'Unknown error'}` 
      }, 500);
    }
  }
  
  // Response format depends on single vs multi-ticket
  const eventInfo = {
    title: event.title,
    startDatetime: event.startDatetime,
    location: event.location,
  };
  
  return c.json({
    // For backward compatibility, include primary ticket as 'ticket'
    ticket: {
      ...primaryTicket,
      event: eventInfo,
    },
    // For multi-ticket bookings, include all tickets
    tickets: createdTickets.map(t => ({
      ...t,
      event: eventInfo,
    })),
    bookingId: ticketCount > 1 ? bookingId : null,
    payment: primaryPayment,
    payments: createdPayments,
    lightningInvoice: lnbitsInvoice ? {
      paymentHash: lnbitsInvoice.paymentHash,
      paymentRequest: lnbitsInvoice.paymentRequest,
      amount: lnbitsInvoice.amount, // Amount in satoshis
      fiatAmount: lnbitsInvoice.fiatAmount,
      fiatCurrency: lnbitsInvoice.fiatCurrency,
      expiry: lnbitsInvoice.expiry,
    } : null,
    message: ticketCount > 1 
      ? `${ticketCount} tickets booked successfully` 
      : 'Booking created successfully',
  }, 201);
});

// Download combined PDF for multi-ticket booking
// NOTE: This route MUST be defined before /:id/pdf to prevent the wildcard from matching "booking"
ticketsRouter.get('/booking/:bookingId/pdf', async (c) => {
  const bookingId = c.req.param('bookingId');
  const user: any = await getAuthUser(c);
  
  console.log(`[PDF] Generating combined PDF for booking: ${bookingId}`);
  
  // Get all tickets in this booking
  const bookingTickets = await dbAll(
    (db as any)
      .select()
      .from(tickets)
      .where(eq((tickets as any).bookingId, bookingId))
  );
  
  console.log(`[PDF] Found ${bookingTickets?.length || 0} tickets for booking ${bookingId}`);
  
  if (!bookingTickets || bookingTickets.length === 0) {
    return c.json({ error: 'Booking not found' }, 404);
  }
  
  const primaryTicket = bookingTickets[0] as any;
  
  // Check authorization - must be ticket owner or admin
  if (user) {
    const isAdmin = ['admin', 'organizer', 'staff'].includes(user.role);
    const isOwner = user.id === primaryTicket.userId;
    
    if (!isAdmin && !isOwner) {
      return c.json({ error: 'Unauthorized' }, 403);
    }
  }
  
  // Check that at least one ticket is confirmed
  const hasConfirmedTicket = bookingTickets.some((t: any) => 
    ['confirmed', 'checked_in'].includes(t.status)
  );
  
  if (!hasConfirmedTicket) {
    return c.json({ error: 'No confirmed tickets in this booking' }, 400);
  }
  
  // Get event
  const event = await dbGet<any>(
    (db as any).select().from(events).where(eq((events as any).id, primaryTicket.eventId))
  );
  
  if (!event) {
    return c.json({ error: 'Event not found' }, 404);
  }
  
  try {
    // Filter to only confirmed/checked_in tickets
    const confirmedTickets = bookingTickets.filter((t: any) => 
      ['confirmed', 'checked_in'].includes(t.status)
    );
    
    console.log(`[PDF] Generating PDF with ${confirmedTickets.length} confirmed tickets`);
    
    // Get site timezone for proper date/time formatting
    const settings = await dbGet<any>(
      (db as any).select().from(siteSettings).limit(1)
    );
    const timezone = settings?.timezone || 'America/Asuncion';
    
    const ticketsData = confirmedTickets.map((ticket: any) => ({
      id: ticket.id,
      qrCode: ticket.qrCode,
      attendeeName: `${ticket.attendeeFirstName} ${ticket.attendeeLastName || ''}`.trim(),
      attendeeEmail: ticket.attendeeEmail,
      event: {
        title: event.title,
        startDatetime: event.startDatetime,
        endDatetime: event.endDatetime,
        location: event.location,
        locationUrl: event.locationUrl,
      },
      timezone,
    }));
    
    const pdfBuffer = await generateCombinedTicketsPDF(ticketsData);
    
    // Set response headers for PDF download
    return new Response(new Uint8Array(pdfBuffer), {
      headers: {
        'Content-Type': 'application/pdf',
        'Content-Disposition': `attachment; filename="spanglish-booking-${bookingId}.pdf"`,
      },
    });
  } catch (error: any) {
    console.error('Combined PDF generation error:', error);
    return c.json({ error: 'Failed to generate PDF' }, 500);
  }
});

// Download ticket as PDF (single ticket)
ticketsRouter.get('/:id/pdf', async (c) => {
  const id = c.req.param('id');
  const user: any = await getAuthUser(c);
  
  const ticket = await dbGet<any>(
    (db as any).select().from(tickets).where(eq((tickets as any).id, id))
  );
  
  if (!ticket) {
    return c.json({ error: 'Ticket not found' }, 404);
  }
  
  // Check authorization - must be ticket owner or admin
  if (user) {
    const isAdmin = ['admin', 'organizer', 'staff'].includes(user.role);
    const isOwner = user.id === ticket.userId;
    
    if (!isAdmin && !isOwner) {
      return c.json({ error: 'Unauthorized' }, 403);
    }
  } else {
    // Allow unauthenticated access via ticket ID for email links
    // The ticket ID itself serves as a secure token (UUID)
  }
  
  // Only generate PDF for confirmed or checked-in tickets
  if (!['confirmed', 'checked_in'].includes(ticket.status)) {
    return c.json({ error: 'Ticket is not confirmed' }, 400);
  }
  
  // Get event
  const event = await dbGet<any>(
    (db as any).select().from(events).where(eq((events as any).id, ticket.eventId))
  );
  
  if (!event) {
    return c.json({ error: 'Event not found' }, 404);
  }
  
  try {
    // Get site timezone for proper date/time formatting
    const settings = await dbGet<any>(
      (db as any).select().from(siteSettings).limit(1)
    );
    const timezone = settings?.timezone || 'America/Asuncion';
    
    const pdfBuffer = await generateTicketPDF({
      id: ticket.id,
      qrCode: ticket.qrCode,
      attendeeName: `${ticket.attendeeFirstName} ${ticket.attendeeLastName || ''}`.trim(),
      attendeeEmail: ticket.attendeeEmail,
      event: {
        title: event.title,
        startDatetime: event.startDatetime,
        endDatetime: event.endDatetime,
        location: event.location,
        locationUrl: event.locationUrl,
      },
      timezone,
    });
    
    // Set response headers for PDF download
    return new Response(new Uint8Array(pdfBuffer), {
      headers: {
        'Content-Type': 'application/pdf',
        'Content-Disposition': `attachment; filename="spanglish-ticket-${ticket.qrCode}.pdf"`,
      },
    });
  } catch (error: any) {
    console.error('PDF generation error:', error);
    return c.json({ error: 'Failed to generate PDF' }, 500);
  }
});

// Get event check-in stats for scanner (lightweight endpoint for staff)
ticketsRouter.get('/stats/checkin', requireAuth(['admin', 'organizer', 'staff']), async (c) => {
  const eventId = c.req.query('eventId');

  if (!eventId) {
    return c.json({ error: 'eventId is required' }, 400);
  }

  // Get event info
  const event = await dbGet<any>(
    (db as any).select().from(events).where(eq((events as any).id, eventId))
  );

  if (!event) {
    return c.json({ error: 'Event not found' }, 404);
  }

  // Count checked-in tickets
  const checkedInCount = await dbGet<any>(
    (db as any)
      .select({ count: sql<number>`count(*)` })
      .from(tickets)
      .where(
        and(
          eq((tickets as any).eventId, eventId),
          eq((tickets as any).status, 'checked_in')
        )
      )
  );

  // Count confirmed + checked_in (total active)
  const totalActiveCount = await dbGet<any>(
    (db as any)
      .select({ count: sql<number>`count(*)` })
      .from(tickets)
      .where(
        and(
          eq((tickets as any).eventId, eventId),
          sql`${(tickets as any).status} IN ('confirmed', 'checked_in')`
        )
      )
  );

  return c.json({
    eventId,
    capacity: event.capacity,
    checkedIn: checkedInCount?.count || 0,
    totalActive: totalActiveCount?.count || 0,
  });
});

// Live search tickets (GET - for scanner live search)
ticketsRouter.get('/search', requireAuth(['admin', 'organizer', 'staff']), async (c) => {
  const q = c.req.query('q')?.trim() || '';
  const eventId = c.req.query('eventId');

  if (q.length < 2) {
    return c.json({ tickets: [] });
  }

  const searchTerm = `%${q.toLowerCase()}%`;

  // Search by name (ILIKE), email (ILIKE), ticket ID (exact or partial)
  const nameEmailConditions = [
    sql`LOWER(${(tickets as any).attendeeEmail}) LIKE ${searchTerm}`,
    sql`LOWER(${(tickets as any).attendeeFirstName}) LIKE ${searchTerm}`,
    sql`LOWER(${(tickets as any).attendeeLastName}) LIKE ${searchTerm}`,
    sql`LOWER(${(tickets as any).attendeeFirstName} || ' ' || COALESCE(${(tickets as any).attendeeLastName}, '')) LIKE ${searchTerm}`,
    // Ticket ID exact or partial match (cast UUID to text for LOWER)
    sql`LOWER(CAST(${(tickets as any).id} AS TEXT)) LIKE ${searchTerm}`,
    sql`LOWER(CAST(${(tickets as any).qrCode} AS TEXT)) LIKE ${searchTerm}`,
  ];

  let whereClause: any = and(
    or(...nameEmailConditions),
    // Exclude cancelled tickets by default
    sql`${(tickets as any).status} != 'cancelled'`
  );

  if (eventId) {
    whereClause = and(whereClause, eq((tickets as any).eventId, eventId));
  }

  const matchingTickets = await dbAll<any>(
    (db as any)
      .select()
      .from(tickets)
      .where(whereClause)
      .limit(20)
  );

  // Enrich with event details
  const results = await Promise.all(
    matchingTickets.map(async (ticket: any) => {
      const event = await dbGet<any>(
        (db as any).select().from(events).where(eq((events as any).id, ticket.eventId))
      );
      return {
        ticket_id: ticket.id,
        name: `${ticket.attendeeFirstName} ${ticket.attendeeLastName || ''}`.trim(),
        email: ticket.attendeeEmail,
        status: ticket.status,
        checked_in: ticket.status === 'checked_in',
        checkinAt: ticket.checkinAt,
        event_id: ticket.eventId,
        qrCode: ticket.qrCode,
        event: event ? {
          id: event.id,
          title: event.title,
          startDatetime: event.startDatetime,
          location: event.location,
        } : null,
      };
    })
  );

  return c.json({ tickets: results });
});

// Get ticket by ID
// Capability-based access: the unguessable ticket UUID acts as the access token for
// guest bookings (no account required). For anonymous callers we withhold attendee PII
// (email/phone/RUC); the full record is only returned to the owner or admin/staff.
ticketsRouter.get('/:id', async (c) => {
  const id = c.req.param('id');
  
  const ticket = await dbGet<any>(
    (db as any).select().from(tickets).where(eq((tickets as any).id, id))
  );
  
  if (!ticket) {
    return c.json({ error: 'Ticket not found' }, 404);
  }
  
  // Get associated event
  const event = await dbGet(
    (db as any).select().from(events).where(eq((events as any).id, ticket.eventId))
  );
  
  // Get payment
  const payment = await dbGet(
    (db as any).select().from(payments).where(eq((payments as any).ticketId, id))
  );
  
  // Count how many tickets belong to this booking (for per-quantity payment links)
  let bookingTicketCount = 1;
  if (ticket.bookingId) {
    const bookingTickets = await dbAll<any>(
      (db as any).select().from(tickets).where(eq((tickets as any).bookingId, ticket.bookingId))
    );
    bookingTicketCount = bookingTickets.length || 1;
  }

  // Determine whether the requester is the owner or an admin/staff member
  const authUser: any = await getAuthUser(c);
  const isPrivileged = !!authUser && (
    ['admin', 'organizer', 'staff'].includes(authUser.role) || authUser.id === ticket.userId
  );

  const ticketPayload: any = { ...ticket, event, payment, bookingTicketCount };
  if (!isPrivileged) {
    // Strip attendee PII for anonymous capability-based access
    delete ticketPayload.attendeeEmail;
    delete ticketPayload.attendeePhone;
    delete ticketPayload.attendeeRuc;
  }
  
  return c.json({ ticket: ticketPayload });
});

// Update ticket status (admin/organizer)
ticketsRouter.put('/:id', requireAuth(['admin', 'organizer', 'staff']), zValidator('json', updateTicketSchema), async (c) => {
  const id = c.req.param('id');
  const data = c.req.valid('json');
  
  const ticket = await dbGet<any>(
    (db as any).select().from(tickets).where(eq((tickets as any).id, id))
  );
  
  if (!ticket) {
    return c.json({ error: 'Ticket not found' }, 404);
  }
  
  const updates: any = {};
  
  if (data.status) {
    updates.status = data.status;
    if (data.status === 'checked_in') {
      updates.checkinAt = getNow();
    }
  }
  
  if (Object.keys(updates).length > 0) {
    await (db as any).update(tickets).set(updates).where(eq((tickets as any).id, id));
  }
  
  const updated = await dbGet(
    (db as any).select().from(tickets).where(eq((tickets as any).id, id))
  );
  
  return c.json({ ticket: updated });
});

// Search tickets by name/email (for scanner manual search)
ticketsRouter.post('/search', requireAuth(['admin', 'organizer', 'staff']), async (c) => {
  const body = await c.req.json().catch(() => ({}));
  const { query, eventId } = body;

  if (!query || typeof query !== 'string' || query.trim().length < 2) {
    return c.json({ error: 'Search query must be at least 2 characters' }, 400);
  }

  const searchTerm = `%${query.trim().toLowerCase()}%`;

  const conditions = [
    sql`LOWER(${(tickets as any).attendeeEmail}) LIKE ${searchTerm}`,
    sql`LOWER(${(tickets as any).attendeeFirstName}) LIKE ${searchTerm}`,
    sql`LOWER(${(tickets as any).attendeeLastName}) LIKE ${searchTerm}`,
    sql`LOWER(${(tickets as any).attendeeFirstName} || ' ' || COALESCE(${(tickets as any).attendeeLastName}, '')) LIKE ${searchTerm}`,
  ];

  let whereClause = or(...conditions);

  if (eventId) {
    whereClause = and(whereClause, eq((tickets as any).eventId, eventId));
  }

  const matchingTickets = await dbAll<any>(
    (db as any)
      .select()
      .from(tickets)
      .where(whereClause)
      .limit(20)
  );

  // Enrich with event details
  const results = await Promise.all(
    matchingTickets.map(async (ticket: any) => {
      const event = await dbGet<any>(
        (db as any).select().from(events).where(eq((events as any).id, ticket.eventId))
      );
      return {
        id: ticket.id,
        qrCode: ticket.qrCode,
        attendeeName: `${ticket.attendeeFirstName} ${ticket.attendeeLastName || ''}`.trim(),
        attendeeEmail: ticket.attendeeEmail,
        attendeePhone: ticket.attendeePhone,
        status: ticket.status,
        checkinAt: ticket.checkinAt,
        event: event ? {
          id: event.id,
          title: event.title,
          startDatetime: event.startDatetime,
          location: event.location,
        } : null,
      };
    })
  );

  return c.json({ tickets: results });
});

// Validate ticket by QR code (for scanner)
ticketsRouter.post('/validate', requireAuth(['admin', 'organizer', 'staff']), async (c) => {
  const body = await c.req.json().catch(() => ({}));
  const { code, eventId } = body;
  
  if (!code) {
    return c.json({ error: 'Code is required' }, 400);
  }
  
  // Try to find ticket by QR code or ID
  let ticket = await dbGet<any>(
    (db as any)
      .select()
      .from(tickets)
      .where(eq((tickets as any).qrCode, code))
  );
  
  // If not found by QR, try by ID
  if (!ticket) {
    ticket = await dbGet<any>(
      (db as any)
        .select()
        .from(tickets)
        .where(eq((tickets as any).id, code))
    );
  }
  
  if (!ticket) {
    return c.json({ 
      valid: false,
      error: 'Ticket not found',
      status: 'invalid',
    });
  }
  
  // If eventId is provided, verify the ticket is for that event
  if (eventId && ticket.eventId !== eventId) {
    return c.json({
      valid: false,
      error: 'Ticket is for a different event',
      status: 'wrong_event',
    });
  }
  
  // Get event details
  const event = await dbGet<any>(
    (db as any)
      .select()
      .from(events)
      .where(eq((events as any).id, ticket.eventId))
  );
  
  // Determine validity status
  let validityStatus = 'invalid';
  let canCheckIn = false;
  
  if (ticket.status === 'cancelled') {
    validityStatus = 'cancelled';
  } else if (ticket.status === 'pending') {
    validityStatus = 'pending_payment';
  } else if (ticket.status === 'checked_in') {
    validityStatus = 'already_checked_in';
  } else if (ticket.status === 'confirmed') {
    validityStatus = 'valid';
    canCheckIn = true;
  }
  
  // Get admin who checked in (if applicable)
  let checkedInBy = null;
  if (ticket.checkedInByAdminId) {
    const admin = await dbGet<any>(
      (db as any)
        .select()
        .from(users)
        .where(eq((users as any).id, ticket.checkedInByAdminId))
    );
    checkedInBy = admin ? admin.name : null;
  }
  
  return c.json({
    valid: validityStatus === 'valid',
    status: validityStatus,
    canCheckIn,
    ticket: {
      id: ticket.id,
      qrCode: ticket.qrCode,
      attendeeName: `${ticket.attendeeFirstName} ${ticket.attendeeLastName || ''}`.trim(),
      attendeeEmail: ticket.attendeeEmail,
      attendeePhone: ticket.attendeePhone,
      status: ticket.status,
      paymentStatus: ticket.paymentStatus,
      // Balance to collect at the door for unpaid tickets
      amountDue: ticket.paymentStatus === 'unpaid' && event ? event.price : 0,
      checkinAt: ticket.checkinAt,
      checkedInBy,
    },
    event: event ? {
      id: event.id,
      title: event.title,
      startDatetime: event.startDatetime,
      location: event.location,
    } : null,
  });
});

// Check-in ticket
ticketsRouter.post('/:id/checkin', requireAuth(['admin', 'organizer', 'staff']), async (c) => {
  const id = c.req.param('id');
  const adminUser = (c as any).get('user');
  
  const ticket = await dbGet<any>(
    (db as any).select().from(tickets).where(eq((tickets as any).id, id))
  );
  
  if (!ticket) {
    return c.json({ error: 'Ticket not found' }, 404);
  }
  
  if (ticket.status === 'checked_in') {
    return c.json({ error: 'Ticket already checked in' }, 400);
  }
  
  if (ticket.status !== 'confirmed') {
    return c.json({ error: 'Ticket must be confirmed before check-in' }, 400);
  }
  
  const now = getNow();
  
  await (db as any)
    .update(tickets)
    .set({ 
      status: 'checked_in', 
      checkinAt: now,
      checkedInByAdminId: adminUser?.id || null,
    })
    .where(eq((tickets as any).id, id));
  
  const updated = await dbGet<any>(
    (db as any).select().from(tickets).where(eq((tickets as any).id, id))
  );
  
  // Get event for response
  const event = await dbGet<any>(
    (db as any).select().from(events).where(eq((events as any).id, ticket.eventId))
  );
  
  return c.json({ 
    ticket: {
      ...updated,
      attendeeName: `${updated.attendeeFirstName} ${updated.attendeeLastName || ''}`.trim(),
    },
    event: event ? {
      id: event.id,
      title: event.title,
    } : null,
    message: 'Check-in successful' 
  });
});

// Mark payment as received (for cash payments - admin only)
// Supports multi-ticket bookings - confirms all tickets in the booking
ticketsRouter.post('/:id/mark-paid', requireAuth(['admin', 'organizer', 'staff']), async (c) => {
  const id = c.req.param('id');
  const user = (c as any).get('user');
  
  const ticket = await dbGet<any>(
    (db as any).select().from(tickets).where(eq((tickets as any).id, id))
  );
  
  if (!ticket) {
    return c.json({ error: 'Ticket not found' }, 404);
  }
  
  // Confirmed/checked-in tickets can still be marked paid when they carry an
  // unpaid balance (admin-added unpaid tickets collected at the door)
  if (['confirmed', 'checked_in'].includes(ticket.status) && ticket.paymentStatus !== 'unpaid') {
    return c.json({ error: 'Ticket already confirmed' }, 400);
  }

  if (ticket.status === 'cancelled') {
    return c.json({ error: 'Cannot confirm cancelled ticket' }, 400);
  }
  
  const now = getNow();
  
  // Get all tickets in this booking (if multi-ticket)
  let ticketsToConfirm: any[] = [ticket];
  
  if (ticket.bookingId) {
    // This is a multi-ticket booking - get all tickets with same bookingId
    ticketsToConfirm = await dbAll(
      (db as any)
        .select()
        .from(tickets)
        .where(eq((tickets as any).bookingId, ticket.bookingId))
    );
  }
  
  if (ticket.status === 'on_hold') {
    // The seat was released when this booking went on hold - re-check capacity
    // before confirming it directly.
    try {
      await reserveOnHoldBooking(
        ticket.eventId,
        ticketsToConfirm.map((t: any) => t.id),
        'confirmed',
        'paid',
        { paidByAdminId: user.id }
      );
    } catch (err) {
      if (err instanceof HoldCapacityError) {
        return c.json({
          error: 'This event is now full. Your spot was released after the payment deadline passed.',
        }, 400);
      }
      throw err;
    }
  } else {
    // Confirm all tickets in the booking (checked-in tickets keep their status)
    for (const t of ticketsToConfirm) {
      // Update ticket status
      await (db as any)
        .update(tickets)
        .set({ status: t.status === 'checked_in' ? 'checked_in' : 'confirmed', paymentStatus: 'paid' })
        .where(eq((tickets as any).id, t.id));

      // Update payment status
      await (db as any)
        .update(payments)
        .set({
          status: 'paid',
          paidAt: now,
          paidByAdminId: user.id,
          updatedAt: now,
        })
        .where(eq((payments as any).ticketId, t.id));
    }
  }

  // Get payment for sending receipt
  const payment = await dbGet<any>(
    (db as any)
      .select()
      .from(payments)
      .where(eq((payments as any).ticketId, id))
  );
  
  // Send confirmation emails asynchronously (don't block the response)
  Promise.all([
    emailService.sendBookingConfirmation(id),
    payment ? emailService.sendPaymentReceipt(payment.id) : Promise.resolve(),
  ]).catch(err => {
    console.error('[Email] Failed to send confirmation emails:', err);
  });
  
  const updated = await dbGet(
    (db as any).select().from(tickets).where(eq((tickets as any).id, id))
  );
  
  return c.json({ 
    ticket: updated, 
    message: ticketsToConfirm.length > 1 
      ? `${ticketsToConfirm.length} tickets marked as paid` 
      : 'Payment marked as received' 
  });
});

// User marks payment as sent (for manual payment methods: bank_transfer, tpago)
// This sets status to "pending_approval" and notifies admin
ticketsRouter.post('/:id/mark-payment-sent', rateLimitMiddleware({ max: 10, windowMs: 10 * 60 * 1000, prefix: 'mark-payment-sent' }), async (c) => {
  const id = c.req.param('id');
  const body = await c.req.json().catch(() => ({}));
  const { payerName } = body;
  
  const ticket = await dbGet<any>(
    (db as any).select().from(tickets).where(eq((tickets as any).id, id))
  );
  
  if (!ticket) {
    return c.json({ error: 'Ticket not found' }, 404);
  }
  
  // Get the payment
  const payment = await dbGet<any>(
    (db as any)
      .select()
      .from(payments)
      .where(eq((payments as any).ticketId, id))
  );
  
  if (!payment) {
    return c.json({ error: 'Payment not found' }, 404);
  }
  
  // Only allow for manual payment methods
  if (!['bank_transfer', 'tpago'].includes(payment.provider)) {
    return c.json({ error: 'This action is only available for bank transfer or TPago payments' }, 400);
  }
  
  // Handle idempotency - if already marked as sent or paid, return success with current state
  if (payment.status === 'pending_approval') {
    return c.json({ 
      payment, 
      message: 'Payment was already marked as sent. Waiting for admin approval.',
      alreadyProcessed: true,
    });
  }
  
  if (payment.status === 'paid') {
    return c.json({
      payment,
      message: 'Payment has already been confirmed.',
      alreadyProcessed: true,
    });
  }

  // A booking that was auto-released after the hold threshold: recover it by
  // re-reserving the seat(s) and moving back into the admin approval queue.
  if (payment.status === 'on_hold') {
    let ticketsToRecover: any[] = [ticket];
    if (ticket.bookingId) {
      ticketsToRecover = await dbAll<any>(
        (db as any).select().from(tickets).where(eq((tickets as any).bookingId, ticket.bookingId))
      );
    }

    try {
      await reserveOnHoldBooking(
        ticket.eventId,
        ticketsToRecover.map((t: any) => t.id),
        'pending',
        'pending_approval',
        { extraPaymentFields: { userMarkedPaidAt: getNow(), payerName: payerName?.trim() || null } }
      );
    } catch (err) {
      if (err instanceof HoldCapacityError) {
        return c.json({
          error: 'This event is now full. Your spot was released after the payment deadline passed.',
        }, 400);
      }
      throw err;
    }

    const recoveredPayment = await dbGet(
      (db as any).select().from(payments).where(eq((payments as any).id, payment.id))
    );

    return c.json({
      payment: recoveredPayment,
      message: 'Payment marked as sent. Waiting for admin approval.',
    });
  }

  // Only allow if currently pending
  if (payment.status !== 'pending') {
    return c.json({ error: 'Payment has already been processed' }, 400);
  }

  const now = getNow();
  
  // Update payment status to pending_approval for this ticket and any siblings
  // in a multi-ticket booking (mirrors the approve flow's bookingId fan-out).
  let ticketsToMark: any[] = [ticket];
  if (ticket.bookingId) {
    ticketsToMark = await dbAll<any>(
      (db as any).select().from(tickets).where(eq((tickets as any).bookingId, ticket.bookingId))
    );
  }

  for (const t of ticketsToMark) {
    await (db as any)
      .update(payments)
      .set({ 
        status: 'pending_approval',
        userMarkedPaidAt: now,
        payerName: payerName?.trim() || null,
        updatedAt: now,
      })
      .where(
        and(
          eq((payments as any).ticketId, (t as any).id),
          eq((payments as any).status, 'pending')
        )
      );
  }
  
  // Get updated payment for the requested ticket
  const updatedPayment = await dbGet(
    (db as any)
      .select()
      .from(payments)
      .where(eq((payments as any).id, payment.id))
  );
  
  // TODO: Send notification to admin about pending payment approval
  
  return c.json({ 
    payment: updatedPayment, 
    message: 'Payment marked as sent. Waiting for admin approval.' 
  });
});

// Cancel ticket
ticketsRouter.post('/:id/cancel', async (c) => {
  const id = c.req.param('id');
  const user: any = await getAuthUser(c);
  
  const ticket = await dbGet<any>(
    (db as any).select().from(tickets).where(eq((tickets as any).id, id))
  );
  
  if (!ticket) {
    return c.json({ error: 'Ticket not found' }, 404);
  }
  
  // Check authorization (admin or ticket owner)
  if (!user || (user.role !== 'admin' && user.id !== ticket.userId)) {
    return c.json({ error: 'Unauthorized' }, 403);
  }
  
  if (ticket.status === 'cancelled') {
    return c.json({ error: 'Ticket already cancelled' }, 400);
  }
  
  await (db as any).update(tickets).set({ status: 'cancelled' }).where(eq((tickets as any).id, id));
  
  return c.json({ message: 'Ticket cancelled successfully' });
});

// Remove check-in (reset to confirmed)
ticketsRouter.post('/:id/remove-checkin', requireAuth(['admin', 'organizer', 'staff']), async (c) => {
  const id = c.req.param('id');
  
  const ticket = await dbGet<any>(
    (db as any).select().from(tickets).where(eq((tickets as any).id, id))
  );
  
  if (!ticket) {
    return c.json({ error: 'Ticket not found' }, 404);
  }
  
  if (ticket.status !== 'checked_in') {
    return c.json({ error: 'Ticket is not checked in' }, 400);
  }
  
  await (db as any)
    .update(tickets)
    .set({ status: 'confirmed', checkinAt: null })
    .where(eq((tickets as any).id, id));
  
  const updated = await dbGet(
    (db as any).select().from(tickets).where(eq((tickets as any).id, id))
  );
  
  return c.json({ ticket: updated, message: 'Check-in removed successfully' });
});

// Update admin note
ticketsRouter.post('/:id/note', requireAuth(['admin', 'organizer', 'staff']), zValidator('json', updateNoteSchema), async (c) => {
  const id = c.req.param('id');
  const { note } = c.req.valid('json');
  
  const ticket = await dbGet<any>(
    (db as any).select().from(tickets).where(eq((tickets as any).id, id))
  );
  
  if (!ticket) {
    return c.json({ error: 'Ticket not found' }, 404);
  }
  
  await (db as any)
    .update(tickets)
    .set({ adminNote: note || null })
    .where(eq((tickets as any).id, id));
  
  const updated = await dbGet(
    (db as any).select().from(tickets).where(eq((tickets as any).id, id))
  );
  
  return c.json({ ticket: updated, message: 'Note updated successfully' });
});

// Admin create ticket (at the door)
ticketsRouter.post('/admin/create', requireAuth(['admin', 'organizer', 'staff']), zValidator('json', adminCreateTicketSchema), async (c) => {
  const data = c.req.valid('json');
  
  // Get event
  const event = await dbGet<any>(
    (db as any).select().from(events).where(eq((events as any).id, data.eventId))
  );
  if (!event) {
    return c.json({ error: 'Event not found' }, 404);
  }

  // Admin create at door: bypass capacity check (allow over-capacity for walk-ins)

  const now = getNow();

  // For door sales, email might be empty - use a generated placeholder
  const attendeeEmail = data.email && data.email.trim() 
    ? data.email.trim() 
    : `door-${generateId()}@doorentry.local`;
  
  // Find or create user
  let user = await dbGet<any>(
    (db as any).select().from(users).where(eq((users as any).email, attendeeEmail))
  );
  
  const adminFullName = data.lastName && data.lastName.trim() 
    ? `${data.firstName} ${data.lastName}`.trim()
    : data.firstName;
  
  if (!user) {
    const userId = generateId();
    user = {
      id: userId,
      email: attendeeEmail,
      password: null,
      name: adminFullName,
      phone: data.phone || null,
      role: 'user',
      languagePreference: null,
      isClaimed: toDbBool(false),
      accountStatus: 'unclaimed',
      emailVerified: false,
      createdAt: now,
      updatedAt: now,
    };
    await (db as any).insert(users).values(user);
  }
  
  // Check for existing active ticket for this user and event (only if real email provided)
  if (data.email && data.email.trim() && !data.email.includes('@doorentry.local')) {
    const existingTicket = await dbGet<any>(
      (db as any)
        .select()
        .from(tickets)
        .where(
          and(
            eq((tickets as any).userId, user.id),
            eq((tickets as any).eventId, data.eventId)
          )
        )
    );
    
    if (existingTicket && existingTicket.status !== 'cancelled') {
      return c.json({ error: 'This person already has a ticket for this event' }, 400);
    }
  }
  
  // Create ticket
  const ticketId = generateId();
  const qrCode = generateTicketCode();
  
  // For door sales, mark as confirmed (or checked_in if auto-checkin)
  const ticketStatus = data.autoCheckin ? 'checked_in' : 'confirmed';
  
  const newTicket = {
    id: ticketId,
    userId: user.id,
    eventId: data.eventId,
    attendeeFirstName: data.firstName,
    attendeeLastName: data.lastName && data.lastName.trim() ? data.lastName.trim() : null,
    attendeeEmail: data.email && data.email.trim() ? data.email.trim() : null,
    attendeePhone: data.phone && data.phone.trim() ? data.phone.trim() : null,
    preferredLanguage: data.preferredLanguage || null,
    status: ticketStatus,
    paymentStatus: 'paid',
    qrCode,
    checkinAt: data.autoCheckin ? now : null,
    adminNote: data.adminNote || null,
    createdAt: now,
  };
  
  await (db as any).insert(tickets).values(newTicket);
  
  // Create payment record (marked as paid for door sales)
  const paymentId = generateId();
  const adminUser = (c as any).get('user');
  const newPayment = {
    id: paymentId,
    ticketId,
    provider: 'cash',
    amount: event.price,
    currency: event.currency,
    status: 'paid',
    reference: 'Door sale',
    paidAt: now,
    paidByAdminId: adminUser?.id || null,
    createdAt: now,
    updatedAt: now,
  };
  
  await (db as any).insert(payments).values(newPayment);
  
  return c.json({
    ticket: {
      ...newTicket,
      event: {
        title: event.title,
        startDatetime: event.startDatetime,
        location: event.location,
      },
    },
    payment: newPayment,
    message: data.autoCheckin 
      ? 'Attendee added and checked in successfully' 
      : 'Attendee added successfully',
  }, 201);
});

// Unified admin add-attendee endpoint backing the single Add Ticket modal.
// type drives payment handling:
//   paid   — email required; paid cash payment; confirmation email + QR sent
//   door   — paid in cash at the door; all fields optional; counts toward revenue;
//            confirmation email only when an email is provided
//   unpaid — QR issued with balance due (collect at door); pending tpago payment;
//            pay-link (Bancard/TPago) email sent when an email is provided
//   guest  — free comp ticket, not counted in revenue; confirmation email only
//            when an email is provided
ticketsRouter.post('/admin/add', requireAuth(['admin', 'organizer', 'staff']), zValidator('json', z.object({
  eventId: z.string(),
  type: z.enum(['paid', 'door', 'unpaid', 'guest']),
  // Door walk-ins can be logged with nothing filled in, so firstName is only
  // required for the other types
  firstName: z.string().optional().or(z.literal('')),
  lastName: z.string().optional().or(z.literal('')),
  email: z.string().email().optional().or(z.literal('')),
  phone: z.string().optional().or(z.literal('')),
  preferredLanguage: z.enum(['en', 'es']).optional(),
  checkinNow: z.boolean().optional().default(false),
  adminNote: z.string().max(1000).optional(),
}).refine((d) => d.type !== 'paid' || !!(d.email && d.email.trim()), {
  message: 'Email is required for paid tickets',
  path: ['email'],
}).refine((d) => d.type === 'door' || !!(d.firstName && d.firstName.trim()), {
  message: 'First name is required',
  path: ['firstName'],
})), async (c) => {
  const data = c.req.valid('json');

  const event = await dbGet<any>(
    (db as any).select().from(events).where(eq((events as any).id, data.eventId))
  );
  if (!event) {
    return c.json({ error: 'Event not found' }, 404);
  }

  // Admin-added tickets bypass the capacity check (intentional over-capacity)

  const now = getNow();
  const adminUser = (c as any).get('user');
  const hasEmail = !!(data.email && data.email.trim());
  const attendeeEmail = hasEmail
    ? data.email!.trim()
    : `${data.type === 'guest' ? 'guest' : 'door'}-${generateId()}@${data.type === 'guest' ? 'guestinvite' : 'doorentry'}.local`;

  // Nameless door walk-ins still need a display name on the ticket
  const firstName = (data.firstName && data.firstName.trim()) || 'Walk-in';
  const fullName = data.lastName && data.lastName.trim()
    ? `${firstName} ${data.lastName.trim()}`
    : firstName;

  // Find or create user
  let user = await dbGet<any>(
    (db as any).select().from(users).where(eq((users as any).email, attendeeEmail))
  );

  if (!user) {
    const userId = generateId();
    user = {
      id: userId,
      email: attendeeEmail,
      password: null,
      name: fullName,
      phone: data.phone || null,
      role: 'user',
      languagePreference: null,
      isClaimed: toDbBool(false),
      accountStatus: 'unclaimed',
      emailVerified: false,
      createdAt: now,
      updatedAt: now,
    };
    await (db as any).insert(users).values(user);
  }

  // Check for existing active ticket (only when a real email was provided)
  if (hasEmail) {
    const existingTicket = await dbGet<any>(
      (db as any)
        .select()
        .from(tickets)
        .where(
          and(
            eq((tickets as any).userId, user.id),
            eq((tickets as any).eventId, data.eventId)
          )
        )
    );
    if (existingTicket && existingTicket.status !== 'cancelled') {
      return c.json({ error: 'This person already has a ticket for this event' }, 400);
    }
  }

  const ticketId = generateId();
  const qrCode = generateTicketCode();
  const paymentStatus = data.type === 'guest' ? 'comp' : data.type === 'unpaid' ? 'unpaid' : 'paid';

  const newTicket = {
    id: ticketId,
    userId: user.id,
    eventId: data.eventId,
    attendeeFirstName: firstName,
    attendeeLastName: data.lastName && data.lastName.trim() ? data.lastName.trim() : null,
    attendeeEmail: hasEmail ? data.email!.trim() : null,
    attendeePhone: data.phone && data.phone.trim() ? data.phone.trim() : null,
    preferredLanguage: data.preferredLanguage || null,
    status: data.checkinNow ? 'checked_in' : 'confirmed',
    isGuest: data.type === 'guest' ? 1 : 0,
    paymentStatus,
    qrCode,
    checkinAt: data.checkinNow ? now : null,
    checkedInByAdminId: data.checkinNow ? adminUser?.id || null : null,
    adminNote: data.adminNote || null,
    createdAt: now,
  };

  await (db as any).insert(tickets).values(newTicket);

  // Payment record: paid cash for paid/door/guest ($0 for guest), pending tpago for unpaid
  const paymentId = generateId();
  const newPayment = data.type === 'unpaid'
    ? {
        id: paymentId,
        ticketId,
        provider: 'tpago',
        amount: event.price,
        currency: event.currency,
        status: 'pending',
        reference: 'Unpaid ticket — collect at door',
        paidAt: null,
        paidByAdminId: null,
        createdAt: now,
        updatedAt: now,
      }
    : {
        id: paymentId,
        ticketId,
        provider: 'cash',
        amount: data.type === 'guest' ? 0 : event.price,
        currency: event.currency,
        status: 'paid',
        reference: data.type === 'guest'
          ? 'Guest invite'
          : data.type === 'door'
            ? 'Paid at door'
            : 'Manual ticket',
        paidAt: now,
        paidByAdminId: adminUser?.id || null,
        createdAt: now,
        updatedAt: now,
      };

  await (db as any).insert(payments).values(newPayment);

  // Emails (asynchronous): paid always confirms; door/guest confirm only when an
  // email exists; unpaid sends the TPago (Bancard) pay-link instructions instead
  if (data.type === 'unpaid') {
    if (hasEmail) {
      emailService.sendPaymentInstructions(ticketId).then(result => {
        if (!result.success) {
          console.error(`[Email] Failed to send pay link for unpaid ticket ${ticketId}:`, result.error);
        }
      }).catch(err => {
        console.error('[Email] Exception sending pay link for unpaid ticket:', err);
      });
    }
  } else if (data.type === 'paid' || hasEmail) {
    emailService.sendBookingConfirmation(ticketId).then(result => {
      if (!result.success) {
        console.error(`[Email] Failed to send booking confirmation for ${data.type} ticket ${ticketId}:`, result.error);
      }
    }).catch(err => {
      console.error(`[Email] Exception sending booking confirmation for ${data.type} ticket:`, err);
    });
  }

  const messages: Record<string, string> = {
    paid: 'Ticket created — confirmation email sent',
    door: hasEmail
      ? 'Ticket created — paid at the door, confirmation email sent'
      : 'Ticket created — paid at the door',
    unpaid: hasEmail
      ? 'Unpaid ticket created — payment link sent'
      : 'Unpaid ticket created — collect payment at the door',
    guest: hasEmail
      ? 'Guest invited — confirmation email sent'
      : 'Guest invited',
  };

  return c.json({
    ticket: {
      ...newTicket,
      event: {
        title: event.title,
        startDatetime: event.startDatetime,
        location: event.location,
      },
    },
    payment: newPayment,
    message: data.checkinNow ? `${messages[data.type]} · checked in` : messages[data.type],
  }, 201);
});

// Get all tickets (admin) - includes payment for each ticket
ticketsRouter.get('/', requireAuth(['admin', 'organizer']), async (c) => {
  const eventId = c.req.query('eventId');
  const status = c.req.query('status');
  
  let query = (db as any).select().from(tickets);
  
  const conditions = [];
  if (eventId) {
    conditions.push(eq((tickets as any).eventId, eventId));
  }
  if (status) {
    conditions.push(eq((tickets as any).status, status));
  }
  
  if (conditions.length > 0) {
    query = query.where(and(...conditions));
  }
  
  const ticketsList = await dbAll(query);
  const ticketIds = ticketsList.map((t: any) => t.id);

  let paymentByTicketId: Record<string, any> = {};
  if (ticketIds.length > 0) {
    const paymentsList = await dbAll(
      (db as any).select().from(payments).where(inArray((payments as any).ticketId, ticketIds))
    );
    for (const p of paymentsList as any[]) {
      paymentByTicketId[p.ticketId] = p;
    }
  }

  const ticketsWithPayment = ticketsList.map((t: any) => ({
    ...t,
    payment: paymentByTicketId[t.id] || null,
  }));

  return c.json({ tickets: ticketsWithPayment });
});

export default ticketsRouter;
