import { Hono } from 'hono';
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';
import { db, dbGet, dbAll, events, eventSlugAliases, tickets, payments, eventPaymentOverrides, emailLogs, invoices, siteSettings, isPostgres } from '../db/index.js';
import { eq, desc, and, gte, sql } from 'drizzle-orm';
import { requireAuth, getAuthUser } from '../lib/auth.js';
import { generateId, getNow, convertBooleansForDb, toDbDate, toDbDateTz, calculateAvailableSeats } from '../lib/utils.js';
import { slugify, uniqueSlug } from '../lib/slugify.js';
import { revalidateFrontendCache } from '../lib/revalidate.js';
import { eventSeatBreakdownQuery } from '../lib/capacity.js';

interface UserContext {
  id: string;
  email: string;
  name: string;
  role: string;
}

const eventsRouter = new Hono<{ Variables: { user: UserContext } }>();

// Helper to normalize event data for API response
// PostgreSQL decimal returns strings, booleans are stored as integers
function normalizeEvent(event: any) {
  if (!event) return event;
  return {
    ...event,
    // Convert price from string/decimal to clean number
    price: typeof event.price === 'string' ? parseFloat(event.price) : Number(event.price),
    // Convert capacity from string to number if needed
    capacity: typeof event.capacity === 'string' ? parseInt(event.capacity, 10) : Number(event.capacity),
    // Convert boolean integers to actual booleans for frontend
    externalBookingEnabled: Boolean(event.externalBookingEnabled),
  };
}

// Load every slug currently in use (canonical event slugs + historical aliases),
// optionally excluding a given event's own canonical slug + aliases.
async function getAllSlugsInUse(excludeEventId?: string): Promise<string[]> {
  const eventRows = await dbAll<any>(
    (db as any).select({ id: (events as any).id, slug: (events as any).slug }).from(events)
  );
  const aliasRows = await dbAll<any>(
    (db as any).select({ eventId: (eventSlugAliases as any).eventId, slug: (eventSlugAliases as any).slug }).from(eventSlugAliases)
  );
  const slugs: string[] = [];
  for (const row of eventRows) {
    if (row.slug && row.id !== excludeEventId) slugs.push(row.slug);
  }
  for (const row of aliasRows) {
    if (row.slug && row.eventId !== excludeEventId) slugs.push(row.slug);
  }
  return slugs;
}

const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;

// Resolve an event by canonical slug, primary id, or a historical slug alias.
// Slug is checked first because Postgres rejects non-UUID strings when comparing
// against the uuid `id` column, so id lookups are guarded behind a UUID check there.
async function resolveEventByParam(param: string): Promise<any | null> {
  let event = await dbGet<any>(
    (db as any).select().from(events).where(eq((events as any).slug, param))
  );
  
  if (!event && (!isPostgres() || UUID_PATTERN.test(param))) {
    event = await dbGet<any>(
      (db as any).select().from(events).where(eq((events as any).id, param))
    );
  }
  
  if (!event) {
    const alias = await dbGet<any>(
      (db as any).select().from(eventSlugAliases).where(eq((eventSlugAliases as any).slug, param))
    );
    if (alias) {
      event = await dbGet<any>(
        (db as any).select().from(events).where(eq((events as any).id, alias.eventId))
      );
    }
  }
  
  return event || null;
}

// Custom validation error handler
const validationHook = (result: any, c: any) => {
  if (!result.success) {
    const errors = result.error.issues.map((i: any) => `${i.path.join('.')}: ${i.message}`).join(', ');
    return c.json({ error: errors }, 400);
  }
};

// Helper to parse price from string (handles both "45000" and "41,44" formats)
const parsePrice = (val: unknown): number => {
  if (typeof val === 'number') return val;
  if (typeof val === 'string') {
    // Replace comma with dot for decimal parsing (European format)
    const normalized = val.replace(',', '.');
    const parsed = parseFloat(normalized);
    return isNaN(parsed) ? 0 : parsed;
  }
  return 0;
};

// Helper to normalize boolean (handles true/false and 0/1)
const normalizeBoolean = (val: unknown): boolean => {
  if (typeof val === 'boolean') return val;
  if (typeof val === 'number') return val !== 0;
  if (val === 'true') return true;
  if (val === 'false') return false;
  return false;
};

const baseEventSchema = z.object({
  title: z.string().min(1),
  titleEs: z.string().optional().nullable(),
  slug: z.string().optional(),
  description: z.string().min(1),
  descriptionEs: z.string().optional().nullable(),
  shortDescription: z.string().max(300).optional().nullable(),
  shortDescriptionEs: z.string().max(300).optional().nullable(),
  startDatetime: z.string(),
  endDatetime: z.string().optional().nullable(),
  location: z.string().min(1),
  locationUrl: z.string().url().optional().nullable().or(z.literal('')),
  // Accept price as number or string (handles "45000" and "41,44" formats)
  price: z.union([z.number(), z.string()]).transform(parsePrice).pipe(z.number().min(0)).default(0),
  currency: z.string().default('PYG'),
  capacity: z.union([z.number(), z.string()]).transform((val) => typeof val === 'string' ? parseInt(val, 10) || 50 : val).pipe(z.number().min(1)).default(50),
  status: z.enum(['draft', 'published', 'unlisted', 'cancelled', 'completed', 'archived']).default('draft'),
  // Accept relative paths (/uploads/...) or http(s) URLs only — reject schemes like
  // javascript:/data: that could be reflected into an href/src on the frontend.
  bannerUrl: z.string()
    .refine((v) => v === '' || v.startsWith('/') || /^https?:\/\//i.test(v), {
      message: 'Banner URL must be a relative path or an http(s) URL',
    })
    .optional().nullable().or(z.literal('')),
  // External booking support - accept boolean or number (0/1 from DB)
  externalBookingEnabled: z.union([z.boolean(), z.number()]).transform(normalizeBoolean).default(false),
  externalBookingUrl: z.string().url().optional().nullable().or(z.literal('')),
});

const createEventSchema = baseEventSchema.refine(
  (data) => {
    // If external booking is enabled, URL must be provided and must start with https://
    if (data.externalBookingEnabled) {
      return !!(data.externalBookingUrl && data.externalBookingUrl.startsWith('https://'));
    }
    return true;
  },
  {
    message: 'External booking URL is required and must be a valid HTTPS link when external booking is enabled',
    path: ['externalBookingUrl'],
  }
);

const updateEventSchema = baseEventSchema.partial().refine(
  (data) => {
    // If external booking is enabled, URL must be provided and must start with https://
    if (data.externalBookingEnabled) {
      return !!(data.externalBookingUrl && data.externalBookingUrl.startsWith('https://'));
    }
    return true;
  },
  {
    message: 'External booking URL is required and must be a valid HTTPS link when external booking is enabled',
    path: ['externalBookingUrl'],
  }
);

// Get all events (public)
eventsRouter.get('/', async (c) => {
  const status = c.req.query('status');
  const upcoming = c.req.query('upcoming');
  // Pagination is opt-in: callers that pass neither page nor pageSize (public
  // pages, admin filter dropdowns) still get the full list.
  const pageParam = c.req.query('page');
  const pageSizeParam = c.req.query('pageSize');
  const paginated = pageParam !== undefined || pageSizeParam !== undefined;
  const page = Math.max(parseInt(pageParam || '1', 10) || 1, 1);
  const pageSize = Math.min(Math.max(parseInt(pageSizeParam || '25', 10) || 25, 1), 200);

  // Only privileged users may see non-public events (drafts, archived, etc.).
  // Anonymous/regular callers are restricted to published events regardless of
  // any client-supplied status filter, so drafts cannot leak.
  const authUser: any = await getAuthUser(c);
  const isPrivileged = !!authUser && ['admin', 'organizer', 'staff', 'marketing'].includes(authUser.role);

  const conditions: any[] = [];

  if (upcoming === 'true') {
    // Upcoming feed is always published + future-dated, for everyone.
    conditions.push(eq((events as any).status, 'published'));
    conditions.push(gte((events as any).startDatetime, getNow()));
  } else if (isPrivileged) {
    // Admins/staff may filter by any status (or list everything when unset).
    if (status) {
      conditions.push(eq((events as any).status, status));
    }
  } else {
    // Public listing: published events only, regardless of any status param.
    conditions.push(eq((events as any).status, 'published'));
  }

  const whereClause = conditions.length === 0
    ? undefined
    : conditions.length === 1 ? conditions[0] : and(...conditions);

  let query = (db as any).select().from(events);
  if (whereClause) query = query.where(whereClause);
  query = query.orderBy(desc((events as any).startDatetime));

  let total: number | undefined;
  if (paginated) {
    let countQuery = (db as any).select({ count: sql`count(*)` }).from(events);
    if (whereClause) countQuery = countQuery.where(whereClause);
    const totalRow = await dbGet<any>(countQuery);
    total = Number(totalRow?.count || 0);
    query = query.limit(pageSize).offset((page - 1) * pageSize);
  }

  const result = await dbAll<any>(query);

  // Single grouped query for seat counts across all events (avoids N+1: previously
  // this ran one COUNT query per event). bookedCount = paid (confirmed/checked_in);
  // claimedCount = "I've paid" claims awaiting admin verification. Both hold seats,
  // so availableSeats subtracts them together — the same formula the booking-creation
  // capacity check enforces (lib/capacity.ts).
  const countRows = await dbAll<any>(eventSeatBreakdownQuery(db));
  const countByEvent = new Map<string, { paid: number; claimed: number }>();
  for (const row of countRows) {
    countByEvent.set(row.eventId, {
      paid: Number(row.paidCount) || 0,
      claimed: Number(row.claimedCount) || 0,
    });
  }

  const eventsWithCounts = result.map((event: any) => {
    const normalized = normalizeEvent(event);
    const counts = countByEvent.get(event.id) || { paid: 0, claimed: 0 };
    return {
      ...normalized,
      bookedCount: counts.paid,
      claimedCount: counts.claimed,
      availableSeats: calculateAvailableSeats(normalized.capacity, counts.paid + counts.claimed),
    };
  });
  
  return paginated
    ? c.json({ events: eventsWithCounts, total, page, pageSize })
    : c.json({ events: eventsWithCounts });
});

// Get single event (public) - resolves by id, canonical slug, or historical alias
eventsRouter.get('/:id', async (c) => {
  const param = c.req.param('id');
  const event = await resolveEventByParam(param);
  
  if (!event) {
    return c.json({ error: 'Event not found' }, 404);
  }

  // Draft events are only visible to privileged users (admin preview); hide from public.
  if ((event as any).status === 'draft') {
    const authUser: any = await getAuthUser(c);
    const isPrivileged = !!authUser && ['admin', 'organizer', 'staff', 'marketing'].includes(authUser.role);
    if (!isPrivileged) {
      return c.json({ error: 'Event not found' }, 404);
    }
  }
  
  const normalized = normalizeEvent(event);
  const counts = await getEventSeatCounts(event.id);
  return c.json({
    event: {
      ...normalized,
      bookedCount: counts.paid,
      claimedCount: counts.claimed,
      availableSeats: calculateAvailableSeats(normalized.capacity, counts.paid + counts.claimed),
    },
  });
});

async function getSiteTimezone(): Promise<string> {
  const settings = await dbGet<any>(
    (db as any).select().from(siteSettings).limit(1)
  );
  return settings?.timezone || 'America/Asuncion';
}

// Helper: paid (confirmed/checked_in) and claimed (pending_approval-held) seat
// counts for one event — see lib/capacity.ts for the seat-holding rule.
async function getEventSeatCounts(eventId: string): Promise<{ paid: number; claimed: number }> {
  const row = await dbGet<any>(eventSeatBreakdownQuery(db, eventId));
  return {
    paid: Number(row?.paidCount) || 0,
    claimed: Number(row?.claimedCount) || 0,
  };
}

// Get the earliest upcoming published event with ticket counts (ignores featured promotion)
async function getNextChronologicalUpcoming(): Promise<any | null> {
  const now = getNow();
  const event = await dbGet<any>(
    (db as any)
      .select()
      .from(events)
      .where(
        and(
          eq((events as any).status, 'published'),
          gte((events as any).startDatetime, now)
        )
      )
      .orderBy((events as any).startDatetime)
      .limit(1)
  );

  if (!event) {
    return null;
  }

  const counts = await getEventSeatCounts(event.id);
  const normalized = normalizeEvent(event);
  return {
    ...normalized,
    bookedCount: counts.paid,
    claimedCount: counts.claimed,
    availableSeats: calculateAvailableSeats(normalized.capacity, counts.paid + counts.claimed),
  };
}

// Get next upcoming event (public) - earliest upcoming published event, ignores featured promotion
eventsRouter.get('/next', async (c) => {
  const event = await getNextChronologicalUpcoming();
  if (!event) {
    return c.json({ event: null });
  }
  return c.json({ event: { ...event, isFeatured: false } });
});

// Get next upcoming event (public) - returns featured event if valid, otherwise next upcoming
eventsRouter.get('/next/upcoming', async (c) => {
  const now = getNow();
  const nowMs = Date.now();
  
  // First, check if there's a featured event in site settings
  const settings = await dbGet<any>(
    (db as any).select().from(siteSettings).limit(1)
  );
  
  let featuredEvent = null;
  let shouldUnsetFeatured = false;
  
  if (settings?.featuredEventId) {
    featuredEvent = await dbGet<any>(
      (db as any)
        .select()
        .from(events)
        .where(eq((events as any).id, settings.featuredEventId))
    );
    
    if (featuredEvent) {
      const eventEndTime = featuredEvent.endDatetime || featuredEvent.startDatetime;
      const isPublished = featuredEvent.status === 'published';
      const hasNotEnded = new Date(eventEndTime).getTime() > nowMs;
      
      if (!isPublished || !hasNotEnded) {
        shouldUnsetFeatured = true;
        featuredEvent = null;
      }
    } else {
      shouldUnsetFeatured = true;
    }
  }
  
  if (shouldUnsetFeatured && settings) {
    try {
      await (db as any)
        .update(siteSettings)
        .set({ featuredEventId: null, updatedAt: now })
        .where(eq((siteSettings as any).id, settings.id));
      console.log('Featured event auto-cleared (event ended or unpublished)');
      revalidateFrontendCache();
    } catch (err: any) {
      console.error('Failed to clear featured event:', err);
    }
  }
  
  // If we have a valid featured event, return it
  if (featuredEvent) {
    const counts = await getEventSeatCounts(featuredEvent.id);
    const normalized = normalizeEvent(featuredEvent);
    return c.json({
      event: {
        ...normalized,
        bookedCount: counts.paid,
        claimedCount: counts.claimed,
        availableSeats: calculateAvailableSeats(normalized.capacity, counts.paid + counts.claimed),
        isFeatured: true,
      },
    });
  }
  
  // Fallback: get the next upcoming published event
  const event = await getNextChronologicalUpcoming();

  if (!event) {
    return c.json({ event: null });
  }

  return c.json({ event: { ...event, isFeatured: false } });
});

// Create event (admin/organizer only)
eventsRouter.post('/', requireAuth(['admin', 'organizer']), zValidator('json', createEventSchema, validationHook), async (c) => {
  const data = c.req.valid('json');
  const user = c.get('user');
  const now = getNow();
  const id = generateId();
  const tz = await getSiteTimezone();
  
  // Convert data for database compatibility
  const dbData = convertBooleansForDb(data);
  
  // Generate a unique slug from the title (manual slug is honored on update, not create)
  const existingSlugs = await getAllSlugsInUse();
  const slug = uniqueSlug(data.title, existingSlugs);
  
  const newEvent = {
    id,
    ...dbData,
    slug,
    startDatetime: toDbDateTz(data.startDatetime, tz),
    endDatetime: data.endDatetime ? toDbDateTz(data.endDatetime, tz) : null,
    createdAt: now,
    updatedAt: now,
  };
  
  await (db as any).insert(events).values(newEvent);
  
  // Revalidate sitemap when a new event is created
  revalidateFrontendCache();
  
  // Return normalized event data
  return c.json({ event: normalizeEvent(newEvent) }, 201);
});

// Update event (admin/organizer only)
eventsRouter.put('/:id', requireAuth(['admin', 'organizer']), zValidator('json', updateEventSchema, validationHook), async (c) => {
  const id = c.req.param('id');
  const data = c.req.valid('json');
  
  const existing = await dbGet<any>(
    (db as any).select().from(events).where(eq((events as any).id, id))
  );
  if (!existing) {
    return c.json({ error: 'Event not found' }, 404);
  }
  
  const now = getNow();
  const tz = await getSiteTimezone();
  // Convert data for database compatibility
  const updateData: Record<string, any> = { ...convertBooleansForDb(data), updatedAt: now };
  // Slug changes are handled explicitly below to manage aliases
  delete updateData.slug;
  // Convert datetime fields if present
  if (data.startDatetime) {
    updateData.startDatetime = toDbDateTz(data.startDatetime, tz);
  }
  if (data.endDatetime !== undefined) {
    updateData.endDatetime = data.endDatetime ? toDbDateTz(data.endDatetime, tz) : null;
  }
  
  // Resolve slug: explicit admin edit takes priority, then title-derived regeneration
  const oldSlug: string | null = existing.slug || null;
  let newSlug: string | null = oldSlug;
  if (typeof data.slug === 'string' && data.slug.trim() !== '') {
    const normalized = slugify(data.slug);
    if (!normalized) {
      return c.json({ error: 'Invalid slug' }, 400);
    }
    if (normalized !== oldSlug) {
      const taken = await getAllSlugsInUse(id);
      if (taken.includes(normalized)) {
        return c.json({ error: 'Slug already in use' }, 400);
      }
      newSlug = normalized;
    }
  } else if (data.title && slugify(data.title) !== slugify(existing.title || '')) {
    const taken = await getAllSlugsInUse(id);
    newSlug = uniqueSlug(data.title, taken);
  }
  
  if (newSlug && newSlug !== oldSlug) {
    // If this slug was previously one of THIS event's aliases, reclaim it as canonical
    await (db as any)
      .delete(eventSlugAliases)
      .where(and(eq((eventSlugAliases as any).slug, newSlug), eq((eventSlugAliases as any).eventId, id)));
    // Preserve the old slug as an alias so existing shared links keep redirecting
    if (oldSlug) {
      try {
        await (db as any).insert(eventSlugAliases).values({ slug: oldSlug, eventId: id, createdAt: now });
      } catch (e) { /* alias may already exist */ }
    }
    updateData.slug = newSlug;
  }
  
  await (db as any)
    .update(events)
    .set(updateData)
    .where(eq((events as any).id, id));
  
  const updated = await dbGet(
    (db as any).select().from(events).where(eq((events as any).id, id))
  );
  
  // Revalidate sitemap when an event is updated (status/dates may have changed)
  revalidateFrontendCache();
  
  return c.json({ event: normalizeEvent(updated) });
});

// Delete event (admin only)
eventsRouter.delete('/:id', requireAuth(['admin']), async (c) => {
  const id = c.req.param('id');
  
  const existing = await dbGet(
    (db as any).select().from(events).where(eq((events as any).id, id))
  );
  if (!existing) {
    return c.json({ error: 'Event not found' }, 404);
  }
  
  // Get all tickets for this event
  const eventTickets = await dbAll<any>(
    (db as any)
      .select()
      .from(tickets)
      .where(eq((tickets as any).eventId, id))
  );
  
  // Delete invoices and payments for all tickets of this event
  for (const ticket of eventTickets) {
    // Get payments for this ticket
    const ticketPayments = await dbAll<any>(
      (db as any)
        .select()
        .from(payments)
        .where(eq((payments as any).ticketId, ticket.id))
    );
    
    // Delete invoices for each payment
    for (const payment of ticketPayments) {
      await (db as any).delete(invoices).where(eq((invoices as any).paymentId, payment.id));
    }
    
    // Delete payments for this ticket
    await (db as any).delete(payments).where(eq((payments as any).ticketId, ticket.id));
  }
  
  // Delete all tickets for this event
  await (db as any).delete(tickets).where(eq((tickets as any).eventId, id));
  
  // Delete event payment overrides
  await (db as any).delete(eventPaymentOverrides).where(eq((eventPaymentOverrides as any).eventId, id));
  
  // Delete slug aliases for this event
  await (db as any).delete(eventSlugAliases).where(eq((eventSlugAliases as any).eventId, id));
  
  // Set eventId to null on email logs (they reference this event but can exist without it)
  await (db as any)
    .update(emailLogs)
    .set({ eventId: null })
    .where(eq((emailLogs as any).eventId, id));
  
  // Finally delete the event
  await (db as any).delete(events).where(eq((events as any).id, id));
  
  // Revalidate sitemap when an event is deleted
  revalidateFrontendCache();
  
  return c.json({ message: 'Event deleted successfully' });
});

// Get event attendees (admin/organizer only)
eventsRouter.get('/:id/attendees', requireAuth(['admin', 'organizer', 'staff']), async (c) => {
  const id = c.req.param('id');
  
  const attendees = await dbAll(
    (db as any)
      .select()
      .from(tickets)
      .where(eq((tickets as any).eventId, id))
  );
  
  return c.json({ attendees });
});

// Duplicate event (admin/organizer only)
eventsRouter.post('/:id/duplicate', requireAuth(['admin', 'organizer']), async (c) => {
  const id = c.req.param('id');
  
  const existing = await dbGet<any>(
    (db as any).select().from(events).where(eq((events as any).id, id))
  );
  if (!existing) {
    return c.json({ error: 'Event not found' }, 404);
  }
  
  const now = getNow();
  const newId = generateId();
  const duplicatedTitle = `${existing.title} (Copy)`;
  const existingSlugs = await getAllSlugsInUse();
  const slug = uniqueSlug(duplicatedTitle, existingSlugs);
  
  // Create a copy with modified title and draft status
  const duplicatedEvent = {
    id: newId,
    slug,
    title: duplicatedTitle,
    titleEs: existing.titleEs ? `${existing.titleEs} (Copia)` : null,
    description: existing.description,
    descriptionEs: existing.descriptionEs,
    shortDescription: existing.shortDescription,
    shortDescriptionEs: existing.shortDescriptionEs,
    startDatetime: existing.startDatetime, // Already in DB format from existing record
    endDatetime: existing.endDatetime,
    location: existing.location,
    locationUrl: existing.locationUrl,
    price: existing.price,
    currency: existing.currency,
    capacity: existing.capacity,
    status: 'draft',
    bannerUrl: existing.bannerUrl,
    externalBookingEnabled: existing.externalBookingEnabled ?? 0, // Already in DB format (0/1)
    externalBookingUrl: existing.externalBookingUrl,
    createdAt: now,
    updatedAt: now,
  };
  
  await (db as any).insert(events).values(duplicatedEvent);
  
  return c.json({ event: normalizeEvent(duplicatedEvent), message: 'Event duplicated successfully' }, 201);
});

// List slug aliases for an event (admin/organizer only)
eventsRouter.get('/:id/slug-aliases', requireAuth(['admin', 'organizer']), async (c) => {
  const id = c.req.param('id');
  
  const existing = await dbGet<any>(
    (db as any).select().from(events).where(eq((events as any).id, id))
  );
  if (!existing) {
    return c.json({ error: 'Event not found' }, 404);
  }
  
  const aliases = await dbAll<any>(
    (db as any)
      .select({ slug: (eventSlugAliases as any).slug, createdAt: (eventSlugAliases as any).createdAt })
      .from(eventSlugAliases)
      .where(eq((eventSlugAliases as any).eventId, id))
  );
  
  return c.json({ aliases });
});

// Remove a slug alias from an event (admin/organizer only)
eventsRouter.delete('/:id/slug-aliases/:slug', requireAuth(['admin', 'organizer']), async (c) => {
  const id = c.req.param('id');
  const slug = c.req.param('slug');
  
  const existing = await dbGet<any>(
    (db as any).select().from(events).where(eq((events as any).id, id))
  );
  if (!existing) {
    return c.json({ error: 'Event not found' }, 404);
  }
  
  await (db as any)
    .delete(eventSlugAliases)
    .where(and(eq((eventSlugAliases as any).eventId, id), eq((eventSlugAliases as any).slug, slug)));
  
  return c.json({ message: 'Alias removed' });
});

export default eventsRouter;
