import { ShiftStatus, SignupStatus } from '@prisma/client';
import { prisma } from '../../config/database';
import { redis } from '../../config/redis';
import { gancioClient, type GancioEvent } from '../../services/gancio.client';
import { env } from '../../config/env';
import { logger } from '../../utils/logger';

// --- Types ---

export interface UnifiedCalendarItem {
  id: string;
  type: 'shift' | 'event' | 'poll' | 'ticketed_event';
  title: string;
  date: string;          // YYYY-MM-DD
  startTime: string;     // HH:MM
  endTime: string;       // HH:MM
  location: string | null;
  tags: string[];
  // Shift-specific
  shiftId?: string;
  maxVolunteers?: number | null;
  currentVolunteers?: number;
  // Event-specific
  gancioEventId?: number;
  gancioUrl?: string;
  // Poll-specific
  pollId?: string;
  pollSlug?: string;
  pollStatus?: string;
  pollVoteCount?: number;
  // Ticketed event-specific
  ticketedEventId?: string;
  eventSlug?: string;
  eventFormat?: string;
  hasPaidTiers?: boolean;
  isSoldOut?: boolean;
  maxAttendees?: number | null;
  currentAttendees?: number;
}

export interface UnifiedCalendarResponse {
  dates: Record<string, { count: number; items: UnifiedCalendarItem[] }>;
}

const CACHE_PREFIX = 'events:calendar';
const CACHE_TTL = 120; // 2 minutes

export const unifiedCalendarService = {
  /**
   * Fetch shifts + Gancio events in a date range, merge, deduplicate, group by date.
   */
  async getCalendar(startDate: string, endDate: string): Promise<UnifiedCalendarResponse> {
    const cacheKey = `${CACHE_PREFIX}:${startDate}:${endDate}`;

    // Check cache
    try {
      const cached = await redis.get(cacheKey);
      if (cached) return JSON.parse(cached);
    } catch { /* cache miss */ }

    const start = new Date(startDate);
    const end = new Date(endDate);
    // Set end to end of day
    end.setHours(23, 59, 59, 999);

    // Fetch shifts, Gancio events, polls, and ticketed events in parallel
    const [shifts, gancioEvents, pollItems, ticketedEventItems] = await Promise.all([
      this.fetchShifts(start, end),
      this.fetchGancioEvents(start, end),
      this.fetchPolls(start, end),
      this.fetchTicketedEvents(start, end),
    ]);

    // Build set of Gancio event IDs that correspond to synced shifts or ticketed events (to deduplicate)
    const syncedGancioIds = new Set([
      ...shifts
        .filter(s => s.gancioEventId !== null)
        .map(s => s.gancioEventId!),
      ...ticketedEventItems
        .filter(te => te.gancioEventId)
        .map(te => te.gancioEventId!),
    ]);

    // Normalize shifts into calendar items
    const shiftItems: UnifiedCalendarItem[] = shifts.map(s => ({
      id: `shift-${s.id}`,
      type: 'shift',
      title: s.title,
      date: s.date.toISOString().split('T')[0],
      startTime: s.startTime,
      endTime: s.endTime,
      location: s.location,
      tags: ['volunteer', 'shift'],
      shiftId: s.id,
      maxVolunteers: s.maxVolunteers,
      currentVolunteers: s._count?.signups ?? s.currentVolunteers,
    }));

    // Normalize Gancio events, excluding those already represented by a shift
    const eventItems: UnifiedCalendarItem[] = gancioEvents
      .filter(e => !syncedGancioIds.has(e.id))
      .map(e => {
        const startDt = new Date(e.start_datetime * 1000);
        const endDt = e.end_datetime ? new Date(e.end_datetime * 1000) : startDt;
        return {
          id: `event-${e.id}`,
          type: 'event' as const,
          title: e.title,
          date: startDt.toISOString().split('T')[0],
          startTime: `${String(startDt.getHours()).padStart(2, '0')}:${String(startDt.getMinutes()).padStart(2, '0')}`,
          endTime: `${String(endDt.getHours()).padStart(2, '0')}:${String(endDt.getMinutes()).padStart(2, '0')}`,
          location: e.place_name || e.place_address || null,
          tags: Array.isArray(e.tags) ? e.tags : [],
          gancioEventId: e.id,
          gancioUrl: `${env.GANCIO_URL}/event/${e.id}`,
        };
      });

    // Merge and group by date
    const allItems = [...shiftItems, ...eventItems, ...pollItems, ...ticketedEventItems];
    allItems.sort((a, b) => a.startTime.localeCompare(b.startTime));

    const dates: Record<string, { count: number; items: UnifiedCalendarItem[] }> = {};

    for (const item of allItems) {
      if (!dates[item.date]) {
        dates[item.date] = { count: 0, items: [] };
      }
      dates[item.date].count++;
      dates[item.date].items.push(item);
    }

    const result: UnifiedCalendarResponse = { dates };

    // Cache
    try {
      await redis.setex(cacheKey, CACHE_TTL, JSON.stringify(result));
    } catch { /* non-critical */ }

    return result;
  },

  /**
   * Bust all calendar cache keys (called when shifts change).
   */
  async bustCache(): Promise<void> {
    try {
      const keys = await redis.keys(`${CACHE_PREFIX}:*`);
      if (keys.length > 0) {
        await redis.del(...keys);
      }
    } catch (err) {
      logger.debug('Calendar cache bust failed:', err);
    }
  },

  // --- Private helpers ---

  async fetchShifts(start: Date, end: Date) {
    return prisma.shift.findMany({
      where: {
        isPublic: true,
        status: { not: ShiftStatus.CANCELLED },
        date: { gte: start, lte: end },
      },
      select: {
        id: true,
        title: true,
        date: true,
        startTime: true,
        endTime: true,
        location: true,
        maxVolunteers: true,
        currentVolunteers: true,
        gancioEventId: true,
        _count: {
          select: { signups: { where: { status: SignupStatus.CONFIRMED } } },
        },
      },
      orderBy: [{ date: 'asc' }, { startTime: 'asc' }],
    });
  },

  async fetchPolls(start: Date, end: Date): Promise<UnifiedCalendarItem[]> {
    try {
      const polls = await prisma.schedulingPoll.findMany({
        where: {
          status: { in: ['OPEN', 'FINALIZED'] },
          options: {
            some: { date: { gte: start, lte: end } },
          },
        },
        include: {
          options: { orderBy: { sortOrder: 'asc' } },
          _count: { select: { votes: true } },
        },
      });

      const items: UnifiedCalendarItem[] = [];
      for (const poll of polls) {
        // For finalized polls, only show the selected option
        const optionsToShow = poll.finalizedOptionId
          ? poll.options.filter(o => o.id === poll.finalizedOptionId)
          : poll.options;

        for (const opt of optionsToShow) {
          const optDate = opt.date.toISOString().split('T')[0];
          items.push({
            id: `poll-${poll.id}-${opt.id}`,
            type: 'poll',
            title: poll.title,
            date: optDate,
            startTime: opt.startTime,
            endTime: opt.endTime,
            location: poll.location,
            tags: ['scheduling', 'poll'],
            pollId: poll.id,
            pollSlug: poll.slug,
            pollStatus: poll.status,
            pollVoteCount: poll._count.votes,
          });
        }
      }
      return items;
    } catch (err) {
      logger.debug('Failed to fetch polls for calendar:', err);
      return [];
    }
  },

  async fetchTicketedEvents(start: Date, end: Date): Promise<UnifiedCalendarItem[]> {
    try {
      const events = await prisma.ticketedEvent.findMany({
        where: {
          status: 'PUBLISHED',
          date: { gte: start, lte: end },
        },
        select: {
          id: true,
          slug: true,
          title: true,
          date: true,
          startTime: true,
          endTime: true,
          eventFormat: true,
          venueName: true,
          maxAttendees: true,
          currentAttendees: true,
          gancioEventId: true,
          ticketTiers: {
            select: { tierType: true, soldCount: true, maxQuantity: true },
          },
        },
        orderBy: [{ date: 'asc' }, { startTime: 'asc' }],
      });

      return events.map(e => {
        const hasPaidTiers = e.ticketTiers.some(t => t.tierType === 'PAID');
        const isSoldOut = e.maxAttendees ? e.currentAttendees >= e.maxAttendees : false;
        const formatTags: string[] = [];
        if (e.eventFormat === 'ONLINE') formatTags.push('online');
        if (e.eventFormat === 'HYBRID') formatTags.push('hybrid');
        const location = e.eventFormat === 'ONLINE' ? 'Online Event' : e.venueName;
        return {
          id: `ticketed-${e.id}`,
          type: 'ticketed_event' as const,
          title: e.title,
          date: e.date.toISOString().split('T')[0],
          startTime: e.startTime,
          endTime: e.endTime,
          location,
          tags: ['ticketed', ...(hasPaidTiers ? ['paid'] : ['free']), ...formatTags],
          ticketedEventId: e.id,
          eventSlug: e.slug,
          eventFormat: e.eventFormat,
          gancioEventId: e.gancioEventId ?? undefined,
          hasPaidTiers,
          isSoldOut,
          maxAttendees: e.maxAttendees,
          currentAttendees: e.currentAttendees,
        };
      });
    } catch (err) {
      logger.debug('Failed to fetch ticketed events for calendar:', err);
      return [];
    }
  },

  async fetchGancioEvents(start: Date, end: Date): Promise<GancioEvent[]> {
    try {
      const events = await gancioClient.fetchPublicEvents();
      const startUnix = Math.floor(start.getTime() / 1000);
      const endUnix = Math.floor(end.getTime() / 1000);
      return events.filter(e =>
        e.start_datetime >= startUnix && e.start_datetime <= endUnix,
      );
    } catch (err) {
      logger.debug('Failed to fetch Gancio events for calendar:', err);
      return [];
    }
  },
};
