import {
  CalendarLayerType,
  CalendarSystemType,
  CalendarItemType,
  CalendarBusyStatus,
  CalendarShowDetailsTo,
  CalendarItemSource,
  CalendarVisibility,
  SignupStatus,
  ShiftStatus,
  Prisma,
} from '@prisma/client';
import { prisma } from '../../config/database';
import { logger } from '../../utils/logger';
import { AppError } from '../../middleware/error-handler';
import type {
  CreateLayerInput,
  UpdateLayerInput,
  CreateItemInput,
  UpdateItemInput,
  SeriesEditScope,
} from './calendar.schemas';

// Shape reused by the personal calendar endpoint
interface PersonalCalendarItem {
  id: string;
  type: 'calendar_item' | 'shift' | 'ticket' | 'poll' | 'public_event';
  title: string;
  date: string;       // YYYY-MM-DD
  startTime: string;  // HH:MM
  endTime: string;    // HH:MM
  location: string | null;
  color: string | null;
  itemType: string;
  layerId: string;
  layerName: string;
  layerColor: string;
}

interface PersonalCalendarResponse {
  dates: Record<string, { count: number; items: PersonalCalendarItem[] }>;
}

// Recurrence rule shape stored in JSON
interface RecurrenceRule {
  frequency: 'DAILY' | 'WEEKLY' | 'BIWEEKLY' | 'MONTHLY';
  daysOfWeek?: number[];   // 1=Mon...7=Sun
  dayOfMonth?: number;
  interval?: number;
}

const SYSTEM_LAYER_DEFS: { systemType: CalendarSystemType; name: string; color: string }[] = [
  { systemType: CalendarSystemType.SHIFTS, name: 'My Shifts', color: '#52c41a' },
  { systemType: CalendarSystemType.TICKETS, name: 'My Tickets', color: '#fa8c16' },
  { systemType: CalendarSystemType.POLLS, name: 'My Polls', color: '#722ed1' },
  { systemType: CalendarSystemType.PUBLIC_EVENTS, name: 'Public Events', color: '#1890ff' },
];

const MAX_RECURRENCE_DAYS = 92; // ~3 months

export const calendarService = {
  // =========================================================================
  // Layer management
  // =========================================================================

  async ensureSystemLayers(userId: string) {
    for (const def of SYSTEM_LAYER_DEFS) {
      await prisma.calendarLayer.upsert({
        where: {
          userId_systemType: { userId, systemType: def.systemType },
        },
        update: {}, // no-op if exists
        create: {
          userId,
          name: def.name,
          color: def.color,
          layerType: CalendarLayerType.SYSTEM,
          systemType: def.systemType,
          visibility: CalendarVisibility.PRIVATE,
        },
      });
    }

    return this.getUserLayers(userId);
  },

  async getUserLayers(userId: string) {
    return prisma.calendarLayer.findMany({
      where: { userId },
      orderBy: [{ sortOrder: 'asc' }, { createdAt: 'asc' }],
    });
  },

  async createLayer(userId: string, data: CreateLayerInput) {
    // Get the next sort order
    const maxSort = await prisma.calendarLayer.aggregate({
      where: { userId },
      _max: { sortOrder: true },
    });
    const sortOrder = (maxSort._max.sortOrder ?? -1) + 1;

    return prisma.calendarLayer.create({
      data: {
        userId,
        name: data.name,
        color: data.color,
        visibility: data.visibility as CalendarVisibility,
        layerType: CalendarLayerType.USER,
        sortOrder,
      },
    });
  },

  async updateLayer(userId: string, layerId: string, data: UpdateLayerInput) {
    const layer = await prisma.calendarLayer.findFirst({
      where: { id: layerId, userId },
    });

    if (!layer) {
      throw new AppError(404, 'Layer not found', 'NOT_FOUND');
    }

    return prisma.calendarLayer.update({
      where: { id: layerId },
      data: {
        ...(data.name !== undefined && { name: data.name }),
        ...(data.color !== undefined && { color: data.color }),
        ...(data.visibility !== undefined && { visibility: data.visibility as CalendarVisibility }),
        ...(data.isEnabled !== undefined && { isEnabled: data.isEnabled }),
        ...(data.sortOrder !== undefined && { sortOrder: data.sortOrder }),
      },
    });
  },

  async deleteLayer(userId: string, layerId: string) {
    const layer = await prisma.calendarLayer.findFirst({
      where: { id: layerId, userId },
    });

    if (!layer) {
      throw new AppError(404, 'Layer not found', 'NOT_FOUND');
    }

    if (layer.layerType === CalendarLayerType.SYSTEM) {
      throw new AppError(400, 'Cannot delete system layers', 'CANNOT_DELETE_SYSTEM');
    }

    await prisma.calendarLayer.delete({ where: { id: layerId } });
  },

  // =========================================================================
  // Item management
  // =========================================================================

  async getItems(userId: string, startDate: string, endDate: string, layerIds?: string[]) {
    const start = new Date(startDate);
    const end = new Date(endDate);

    const where: Prisma.CalendarItemWhereInput = {
      userId,
      date: { gte: start, lte: end },
    };

    if (layerIds && layerIds.length > 0) {
      where.layerId = { in: layerIds };
    }

    return prisma.calendarItem.findMany({
      where,
      orderBy: [{ date: 'asc' }, { startTime: 'asc' }],
      include: {
        layer: { select: { name: true, color: true, layerType: true, systemType: true } },
      },
    });
  },

  async createItem(userId: string, data: CreateItemInput) {
    // Verify layer ownership
    const layer = await prisma.calendarLayer.findFirst({
      where: { id: data.layerId, userId },
    });

    if (!layer) {
      throw new AppError(404, 'Layer not found', 'LAYER_NOT_FOUND');
    }

    if (layer.layerType === CalendarLayerType.SYSTEM) {
      throw new AppError(400, 'Cannot add items to system layers', 'CANNOT_ADD_TO_SYSTEM');
    }

    const recurrenceRule = data.recurrenceRule
      ? (data.recurrenceRule as unknown as Prisma.InputJsonValue)
      : undefined;

    // Create the template item
    const templateItem = await prisma.calendarItem.create({
      data: {
        userId,
        layerId: data.layerId,
        title: data.title,
        description: data.description,
        date: new Date(data.date),
        startTime: data.startTime,
        endTime: data.endTime,
        isAllDay: data.isAllDay ?? false,
        itemType: (data.itemType as CalendarItemType) ?? CalendarItemType.EVENT,
        location: data.location,
        color: data.color,
        visibility: data.visibility as CalendarVisibility | undefined,
        busyStatus: (data.busyStatus as CalendarBusyStatus) ?? CalendarBusyStatus.BUSY,
        showDetailsTo: (data.showDetailsTo as CalendarShowDetailsTo) ?? CalendarShowDetailsTo.FRIENDS,
        recurrenceRule,
        recurrenceEnd: data.recurrenceEnd ? new Date(data.recurrenceEnd) : undefined,
        sourceType: CalendarItemSource.MANUAL,
      },
    });

    // If recurring, set seriesId to own id and materialize instances
    if (data.recurrenceRule) {
      await prisma.calendarItem.update({
        where: { id: templateItem.id },
        data: { seriesId: templateItem.id },
      });

      const startDate = new Date(data.date);
      const endDate = new Date(startDate);
      endDate.setDate(endDate.getDate() + MAX_RECURRENCE_DAYS);

      // Limit recurrence end
      const recEnd = data.recurrenceEnd ? new Date(data.recurrenceEnd) : endDate;
      const effectiveEnd = recEnd < endDate ? recEnd : endDate;

      await this.materializeRecurrence(
        { ...templateItem, seriesId: templateItem.id },
        startDate,
        effectiveEnd
      );
    }

    return prisma.calendarItem.findUnique({
      where: { id: templateItem.id },
      include: {
        layer: { select: { name: true, color: true, layerType: true, systemType: true } },
      },
    });
  },

  async updateItem(
    userId: string,
    itemId: string,
    data: UpdateItemInput,
    scope?: SeriesEditScope
  ) {
    const item = await prisma.calendarItem.findFirst({
      where: { id: itemId, userId },
    });

    if (!item) {
      throw new AppError(404, 'Item not found', 'NOT_FOUND');
    }

    const updateData: Prisma.CalendarItemUncheckedUpdateInput = {};
    if (data.title !== undefined) updateData.title = data.title;
    if (data.description !== undefined) updateData.description = data.description;
    if (data.date !== undefined) updateData.date = new Date(data.date);
    if (data.startTime !== undefined) updateData.startTime = data.startTime;
    if (data.endTime !== undefined) updateData.endTime = data.endTime;
    if (data.isAllDay !== undefined) updateData.isAllDay = data.isAllDay;
    if (data.itemType !== undefined) updateData.itemType = data.itemType as CalendarItemType;
    if (data.location !== undefined) updateData.location = data.location;
    if (data.color !== undefined) updateData.color = data.color;
    if (data.visibility !== undefined) updateData.visibility = data.visibility as CalendarVisibility | null;
    if (data.busyStatus !== undefined) updateData.busyStatus = data.busyStatus as CalendarBusyStatus;
    if (data.showDetailsTo !== undefined) updateData.showDetailsTo = data.showDetailsTo as CalendarShowDetailsTo;
    if (data.recurrenceRule !== undefined) {
      updateData.recurrenceRule = data.recurrenceRule as unknown as Prisma.InputJsonValue ?? Prisma.JsonNull;
    }
    if (data.recurrenceEnd !== undefined) {
      updateData.recurrenceEnd = data.recurrenceEnd ? new Date(data.recurrenceEnd) : null;
    }

    // Non-recurring or no scope: simple update
    if (!item.seriesId || !scope || scope === 'THIS_ONLY') {
      if (item.seriesId && scope === 'THIS_ONLY') {
        // Mark as exception so future series updates skip it
        updateData.isException = true;
      }

      return prisma.calendarItem.update({
        where: { id: itemId },
        data: updateData,
        include: {
          layer: { select: { name: true, color: true, layerType: true, systemType: true } },
        },
      });
    }

    if (scope === 'ALL') {
      // Update all non-exception instances in the series
      await prisma.calendarItem.updateMany({
        where: { seriesId: item.seriesId, userId, isException: false },
        data: updateData,
      });

      return prisma.calendarItem.findFirst({
        where: { id: itemId },
        include: {
          layer: { select: { name: true, color: true, layerType: true, systemType: true } },
        },
      });
    }

    if (scope === 'THIS_AND_FUTURE') {
      // Update this item and all future non-exception instances
      await prisma.calendarItem.updateMany({
        where: {
          seriesId: item.seriesId,
          userId,
          isException: false,
          date: { gte: item.date },
        },
        data: updateData,
      });

      return prisma.calendarItem.findFirst({
        where: { id: itemId },
        include: {
          layer: { select: { name: true, color: true, layerType: true, systemType: true } },
        },
      });
    }

    // Fallback: simple update
    return prisma.calendarItem.update({
      where: { id: itemId },
      data: updateData,
      include: {
        layer: { select: { name: true, color: true, layerType: true, systemType: true } },
      },
    });
  },

  async deleteItem(userId: string, itemId: string, scope?: SeriesEditScope) {
    const item = await prisma.calendarItem.findFirst({
      where: { id: itemId, userId },
    });

    if (!item) {
      throw new AppError(404, 'Item not found', 'NOT_FOUND');
    }

    // Non-recurring or no scope: delete single item
    if (!item.seriesId || !scope || scope === 'THIS_ONLY') {
      await prisma.calendarItem.delete({ where: { id: itemId } });
      return;
    }

    if (scope === 'ALL') {
      await prisma.calendarItem.deleteMany({
        where: { seriesId: item.seriesId, userId },
      });
      return;
    }

    if (scope === 'THIS_AND_FUTURE') {
      await prisma.calendarItem.deleteMany({
        where: {
          seriesId: item.seriesId,
          userId,
          date: { gte: item.date },
        },
      });
    }
  },

  // =========================================================================
  // Recurrence materialization
  // =========================================================================

  async materializeRecurrence(
    templateItem: {
      id: string;
      userId: string;
      layerId: string;
      seriesId: string;
      title: string;
      description: string | null;
      startTime: string;
      endTime: string;
      isAllDay: boolean;
      itemType: CalendarItemType;
      location: string | null;
      color: string | null;
      visibility: CalendarVisibility | null;
      busyStatus: CalendarBusyStatus;
      showDetailsTo: CalendarShowDetailsTo;
      recurrenceRule: unknown;
      date: Date;
    },
    startDate: Date,
    endDate: Date
  ) {
    const rule = templateItem.recurrenceRule as RecurrenceRule | null;
    if (!rule) return;

    const interval = rule.interval ?? 1;
    const dates = generateRecurrenceDates(rule, startDate, endDate, interval);

    // Remove the template item's own date (it already exists)
    const templateDateStr = templateItem.date.toISOString().split('T')[0];
    const filteredDates = dates.filter(d => d.toISOString().split('T')[0] !== templateDateStr);

    if (filteredDates.length === 0) return;

    // Fetch existing instance dates to avoid duplicates
    const existing = await prisma.calendarItem.findMany({
      where: {
        seriesId: templateItem.seriesId,
        userId: templateItem.userId,
      },
      select: { date: true },
    });
    const existingDateSet = new Set(existing.map(e => e.date.toISOString().split('T')[0]));

    const newDates = filteredDates.filter(
      d => !existingDateSet.has(d.toISOString().split('T')[0])
    );

    if (newDates.length === 0) return;

    // Batch create
    await prisma.calendarItem.createMany({
      data: newDates.map(date => ({
        userId: templateItem.userId,
        layerId: templateItem.layerId,
        title: templateItem.title,
        description: templateItem.description,
        date,
        startTime: templateItem.startTime,
        endTime: templateItem.endTime,
        isAllDay: templateItem.isAllDay,
        itemType: templateItem.itemType,
        location: templateItem.location,
        color: templateItem.color,
        visibility: templateItem.visibility,
        busyStatus: templateItem.busyStatus,
        showDetailsTo: templateItem.showDetailsTo,
        seriesId: templateItem.seriesId,
        sourceType: CalendarItemSource.MANUAL,
      })),
    });

    logger.debug(`Materialized ${newDates.length} recurrence instances for series ${templateItem.seriesId}`);
  },

  // =========================================================================
  // Personal calendar (merged view)
  // =========================================================================

  async getPersonalCalendar(
    userId: string,
    startDate: string,
    endDate: string
  ): Promise<PersonalCalendarResponse> {
    const start = new Date(startDate);
    const end = new Date(endDate);

    // Fetch user's layers
    const layers = await prisma.calendarLayer.findMany({
      where: { userId, isEnabled: true },
    });

    const layerMap = new Map(layers.map(l => [l.id, l]));
    const systemLayersByType = new Map(
      layers.filter(l => l.systemType).map(l => [l.systemType!, l])
    );

    const allItems: PersonalCalendarItem[] = [];

    // 1. User's CalendarItems from enabled layers
    const enabledLayerIds = layers.map(l => l.id);
    if (enabledLayerIds.length > 0) {
      const items = await prisma.calendarItem.findMany({
        where: {
          userId,
          layerId: { in: enabledLayerIds },
          date: { gte: start, lte: end },
        },
        orderBy: [{ date: 'asc' }, { startTime: 'asc' }],
      });

      for (const item of items) {
        const layer = layerMap.get(item.layerId);
        if (!layer) continue;
        allItems.push({
          id: item.id,
          type: 'calendar_item',
          title: item.title,
          date: item.date.toISOString().split('T')[0],
          startTime: item.startTime,
          endTime: item.endTime,
          location: item.location,
          color: item.color ?? layer.color,
          itemType: item.itemType,
          layerId: layer.id,
          layerName: layer.name,
          layerColor: layer.color,
        });
      }
    }

    // 2. System layer: SHIFTS (from ShiftSignup where userId matches)
    const shiftsLayer = systemLayersByType.get(CalendarSystemType.SHIFTS);
    if (shiftsLayer) {
      try {
        const signups = await prisma.shiftSignup.findMany({
          where: {
            userId,
            status: SignupStatus.CONFIRMED,
            shift: {
              status: { not: ShiftStatus.CANCELLED },
              date: { gte: start, lte: end },
            },
          },
          include: {
            shift: {
              select: { id: true, title: true, date: true, startTime: true, endTime: true, location: true },
            },
          },
        });

        for (const signup of signups) {
          const s = signup.shift;
          allItems.push({
            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,
            color: shiftsLayer.color,
            itemType: 'EVENT',
            layerId: shiftsLayer.id,
            layerName: shiftsLayer.name,
            layerColor: shiftsLayer.color,
          });
        }
      } catch (err) {
        logger.debug('Failed to fetch shifts for personal calendar:', err);
      }
    }

    // 3. System layer: TICKETS (from Ticket where userId matches)
    const ticketsLayer = systemLayersByType.get(CalendarSystemType.TICKETS);
    if (ticketsLayer) {
      try {
        const tickets = await prisma.ticket.findMany({
          where: {
            userId,
            status: 'VALID',
            event: {
              date: { gte: start, lte: end },
            },
          },
          include: {
            event: {
              select: { id: true, title: true, date: true, startTime: true, endTime: true, venueName: true },
            },
          },
        });

        for (const ticket of tickets) {
          const e = ticket.event;
          allItems.push({
            id: `ticket-${ticket.id}`,
            type: 'ticket',
            title: e.title,
            date: e.date.toISOString().split('T')[0],
            startTime: e.startTime,
            endTime: e.endTime,
            location: e.venueName,
            color: ticketsLayer.color,
            itemType: 'EVENT',
            layerId: ticketsLayer.id,
            layerName: ticketsLayer.name,
            layerColor: ticketsLayer.color,
          });
        }
      } catch (err) {
        logger.debug('Failed to fetch tickets for personal calendar:', err);
      }
    }

    // 4. System layer: POLLS (from SchedulingPollVote where userId matches)
    const pollsLayer = systemLayersByType.get(CalendarSystemType.POLLS);
    if (pollsLayer) {
      try {
        const votes = await prisma.schedulingPollVote.findMany({
          where: {
            userId,
            option: {
              date: { gte: start, lte: end },
            },
          },
          include: {
            poll: { select: { id: true, title: true, location: true } },
            option: { select: { id: true, date: true, startTime: true, endTime: true } },
          },
        });

        // Deduplicate by poll+option (a user may have multiple votes per poll but same option)
        const seen = new Set<string>();
        for (const vote of votes) {
          const key = `${vote.poll.id}-${vote.option.id}`;
          if (seen.has(key)) continue;
          seen.add(key);

          allItems.push({
            id: `poll-${vote.poll.id}-${vote.option.id}`,
            type: 'poll',
            title: vote.poll.title,
            date: vote.option.date.toISOString().split('T')[0],
            startTime: vote.option.startTime,
            endTime: vote.option.endTime,
            location: vote.poll.location,
            color: pollsLayer.color,
            itemType: 'EVENT',
            layerId: pollsLayer.id,
            layerName: pollsLayer.name,
            layerColor: pollsLayer.color,
          });
        }
      } catch (err) {
        logger.debug('Failed to fetch polls for personal calendar:', err);
      }
    }

    // Sort all items by date then time
    allItems.sort((a, b) => {
      const dateCompare = a.date.localeCompare(b.date);
      if (dateCompare !== 0) return dateCompare;
      return a.startTime.localeCompare(b.startTime);
    });

    // Group by date
    const dates: Record<string, { count: number; items: PersonalCalendarItem[] }> = {};

    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);
    }

    return { dates };
  },
};

// =========================================================================
// Helpers
// =========================================================================

/**
 * Generate dates matching a recurrence rule within a range.
 */
function generateRecurrenceDates(
  rule: RecurrenceRule,
  startDate: Date,
  endDate: Date,
  interval: number
): Date[] {
  const dates: Date[] = [];
  const maxOccurrences = 200; // safety limit

  const current = new Date(startDate);
  let count = 0;

  switch (rule.frequency) {
    case 'DAILY': {
      while (current <= endDate && count < maxOccurrences) {
        dates.push(new Date(current));
        count++;
        current.setDate(current.getDate() + interval);
      }
      break;
    }

    case 'WEEKLY': {
      const daysOfWeek = rule.daysOfWeek ?? [];
      if (daysOfWeek.length === 0) break;

      // Walk day-by-day, tracking week boundaries for interval
      const weekStart = new Date(startDate);
      let weekNum = 0;

      while (current <= endDate && count < maxOccurrences) {
        // Check if we moved to a new week (ISO: Monday=1)
        const daysSinceStart = Math.floor(
          (current.getTime() - weekStart.getTime()) / (1000 * 60 * 60 * 24)
        );
        const currentWeek = Math.floor(daysSinceStart / 7);
        if (currentWeek !== weekNum) {
          weekNum = currentWeek;
        }

        // Only include if this week matches the interval
        if (weekNum % interval === 0) {
          // Convert JS day (0=Sun) to ISO day (1=Mon...7=Sun)
          const jsDay = current.getDay();
          const isoDay = jsDay === 0 ? 7 : jsDay;
          if (daysOfWeek.includes(isoDay)) {
            dates.push(new Date(current));
            count++;
          }
        }

        current.setDate(current.getDate() + 1);
      }
      break;
    }

    case 'BIWEEKLY': {
      const biDays = rule.daysOfWeek ?? [];
      if (biDays.length === 0) break;

      const biWeekStart = new Date(startDate);
      let biWeekNum = 0;

      while (current <= endDate && count < maxOccurrences) {
        const daysSinceStart = Math.floor(
          (current.getTime() - biWeekStart.getTime()) / (1000 * 60 * 60 * 24)
        );
        const currentWeek = Math.floor(daysSinceStart / 7);
        if (currentWeek !== biWeekNum) {
          biWeekNum = currentWeek;
        }

        // Every 2 weeks
        if (biWeekNum % 2 === 0) {
          const jsDay = current.getDay();
          const isoDay = jsDay === 0 ? 7 : jsDay;
          if (biDays.includes(isoDay)) {
            dates.push(new Date(current));
            count++;
          }
        }

        current.setDate(current.getDate() + 1);
      }
      break;
    }

    case 'MONTHLY': {
      const dayOfMonth = rule.dayOfMonth ?? startDate.getDate();
      while (current <= endDate && count < maxOccurrences) {
        // Set to the target day of month
        const targetDate = new Date(current.getFullYear(), current.getMonth(), dayOfMonth);
        // Handle months with fewer days (e.g., Feb 30 → clamp to last day)
        if (targetDate.getMonth() !== current.getMonth()) {
          // Day overflowed — use last day of month
          targetDate.setDate(0); // goes to last day of previous month
          targetDate.setMonth(targetDate.getMonth() + 1);
          targetDate.setDate(0);
        }

        if (targetDate >= startDate && targetDate <= endDate) {
          dates.push(targetDate);
          count++;
        }

        current.setMonth(current.getMonth() + interval);
      }
      break;
    }
  }

  return dates;
}

