import { 
  addDays, 
  addWeeks, 
  addMonths, 
  addYears, 
  isSameDay, 
  getDay, 
  getDate, 
  setDate, 
  setDay, 
  setMonth, 
  startOfMonth, 
  endOfMonth, 
  isBefore, 
  isAfter,
  parseISO,
  format,
  getDaysInMonth
} from 'date-fns';
import { validateAndParseDate, isValidDate } from '@/lib/utils';

export const RECURRENCE_TYPES = {
  NONE: 'none',
  DAILY: 'daily',
  WEEKLY: 'weekly',
  MONTHLY: 'monthly',
  YEARLY: 'yearly',
  MONTHLY_BY_WEEK: 'monthly_by_week' // New type
};

export const RECURRENCE_END_TYPES = {
  NEVER: 'never',
  ON_DATE: 'date',
  AFTER_OCCURRENCES: 'count'
};

export const DAYS_OF_WEEK = [
  { value: 0, label: 'Sun', full: 'Sunday' },
  { value: 1, label: 'Mon', full: 'Monday' },
  { value: 2, label: 'Tue', full: 'Tuesday' },
  { value: 3, label: 'Wed', full: 'Wednesday' },
  { value: 4, label: 'Thu', full: 'Thursday' },
  { value: 5, label: 'Fri', full: 'Friday' },
  { value: 6, label: 'Sat', full: 'Saturday' },
];

export const WEEK_POSITIONS = [
  { value: '1', label: 'First' },
  { value: '2', label: 'Second' },
  { value: '3', label: 'Third' },
  { value: '4', label: 'Fourth' },
  { value: 'last', label: 'Last' }
];

/**
 * Generate a description string for the recurrence rule
 */
export const formatRecurrenceRule = (config) => {
  if (!config || config.type === RECURRENCE_TYPES.NONE) return 'Does not repeat';

  let description = '';
  const intervalStr = config.interval > 1 ? `every ${config.interval} ` : 'every ';

  switch (config.type) {
    case RECURRENCE_TYPES.DAILY:
      description = config.interval > 1 ? `Every ${config.interval} days` : 'Daily';
      break;
    case RECURRENCE_TYPES.WEEKLY:
      const days = (config.daysOfWeek || [])
        .sort((a, b) => a - b)
        .map(d => DAYS_OF_WEEK.find(day => day.value === d)?.label)
        .join(', ');
      description = config.interval > 1 
        ? `Every ${config.interval} weeks on ${days}` 
        : `Weekly on ${days}`;
      break;
    case RECURRENCE_TYPES.MONTHLY:
      description = config.interval > 1 ? `Every ${config.interval} months` : 'Monthly';
      break;
    case RECURRENCE_TYPES.YEARLY:
      description = config.interval > 1 ? `Every ${config.interval} years` : 'Yearly';
      break;
    case RECURRENCE_TYPES.MONTHLY_BY_WEEK:
      const pos = WEEK_POSITIONS.find(p => p.value == config.weekPosition)?.label || config.weekPosition;
      const dayName = DAYS_OF_WEEK.find(d => d.value == config.dayOfWeek)?.full || 'Day';
      description = `Every month on the ${pos} ${dayName}`;
      break;
    default:
      return 'Custom recurrence';
  }

  if (config.endType === RECURRENCE_END_TYPES.ON_DATE && config.endDate) {
    const validEndDate = validateAndParseDate(config.endDate);
    if (validEndDate) {
       description += ` until ${format(validEndDate, 'MMM d, yyyy')}`;
    }
  } else if (config.endType === RECURRENCE_END_TYPES.AFTER_OCCURRENCES) {
    description += `, ${config.occurrences} times`;
  }

  return description;
};

/**
 * Helper to get the Nth occurrence of a weekday in a month
 * e.g., "First Monday" or "Last Friday"
 */
const getNthWeekdayOfMonth = (year, month, dayOfWeek, weekPosition) => {
  try {
    const targetDayOfWeek = parseInt(dayOfWeek);
    const date = new Date(year, month, 1);
    const daysInMonth = getDaysInMonth(date);
    
    // Find first occurrence of this dayOfWeek
    let day = 1;
    while (getDay(new Date(year, month, day)) !== targetDayOfWeek && day <= 7) {
      day++;
    }

    if (weekPosition === 'last') {
      // Find the last occurrence
      while (day + 7 <= daysInMonth) {
        day += 7;
      }
      return new Date(year, month, day);
    } else {
      // Add weeks based on position (1st = add 0 weeks, 2nd = add 1 week, etc)
      const weeksToAdd = parseInt(weekPosition) - 1;
      day += (weeksToAdd * 7);
      
      // Check if this date is still within the month
      if (day > daysInMonth) {
        return null; // The month doesn't have a 5th Monday, etc.
      }
      return new Date(year, month, day);
    }
  } catch (e) {
    console.error("Error calculating Nth weekday:", e);
    return null;
  }
};

/**
 * Generate dates based on recurrence rule
 * Returns array of { start: Date, end: Date } objects
 * Uses baseEvent duration to calculate end times
 */
export const generateRecurrenceDates = (startDate, endDate, config, maxLimit = 365) => {
  // Validate input dates first
  const validStart = validateAndParseDate(startDate);
  const validEnd = validateAndParseDate(endDate);

  if (!validStart || !validEnd) {
      console.error("Invalid dates passed to generateRecurrenceDates", { startDate, endDate });
      return [];
  }

  if (!config || config.type === RECURRENCE_TYPES.NONE) {
    return [{ start: validStart, end: validEnd }];
  }
  
  try {
      const durationMs = validEnd.getTime() - validStart.getTime();
      const instances = [];
      let currentStart = new Date(validStart);
      let count = 0;
      
      // Safety checks
      const maxOccurrences = config.endType === RECURRENCE_END_TYPES.AFTER_OCCURRENCES 
        ? Math.min(Math.max(1, config.occurrences || 1), maxLimit) // Ensure at least 1, cap at maxLimit
        : maxLimit; 
        
      let cutoffDate;
      if (config.endType === RECURRENCE_END_TYPES.ON_DATE && config.endDate) {
         cutoffDate = validateAndParseDate(config.endDate);
         // If invalid end date provided, fallback to default cap
         if (!cutoffDate) {
             cutoffDate = addYears(new Date(), 2);
         }
      } else {
         cutoffDate = addYears(new Date(), 2); // Default cap of 2 years if 'never'
      }

      // Start date should be valid before entering loop
      if (!isValidDate(currentStart)) {
          return [];
      }

      while (count < maxOccurrences) {
        // Check cutoff
        if (config.endType === RECURRENCE_END_TYPES.ON_DATE && isAfter(currentStart, cutoffDate)) {
          break;
        }
        
        let isValid = true;
        
        // --- VALIDITY CHECKS ---
        if (config.type === RECURRENCE_TYPES.WEEKLY) {
           const currentDay = getDay(currentStart);
           if (config.daysOfWeek && Array.isArray(config.daysOfWeek) && !config.daysOfWeek.includes(currentDay)) {
             isValid = false;
           }
        }

        // --- ADD INSTANCE ---
        if (isValid) {
          const instanceEnd = new Date(currentStart.getTime() + durationMs);
          
          if (isValidDate(currentStart) && isValidDate(instanceEnd)) {
             instances.push({
               start: new Date(currentStart),
               end: instanceEnd
             });
             count++;
          }
        }

        // --- ADVANCE DATE ---
        let nextStart;
        const interval = Math.max(1, config.interval || 1); // Prevent 0 or negative intervals
        
        if (config.type === RECURRENCE_TYPES.DAILY) {
          nextStart = addDays(currentStart, interval);
        } else if (config.type === RECURRENCE_TYPES.WEEKLY) {
           nextStart = getNextWeeklyDate(currentStart, config);
        } else if (config.type === RECURRENCE_TYPES.MONTHLY) {
          nextStart = addMonths(currentStart, interval);
        } else if (config.type === RECURRENCE_TYPES.YEARLY) {
          nextStart = addYears(currentStart, interval);
        } else if (config.type === RECURRENCE_TYPES.MONTHLY_BY_WEEK) {
          // Logic for monthly by week (e.g., First Monday)
          // We always jump months, but calculate specific day
          // Since we might be mid-month or past the day in current month, logic needs care
          // Simple approach: just add 1 month to current month base, then find the day
          const nextMonthDate = addMonths(currentStart, 1); // Only step 1 month at a time for calculation simplicity? Or interval?
          // Actually, recurrence interval applies to months too usually.
          // BUT "Monthly by Week" usually implies monthly interval unless specificied.
          // Let's assume interval applies to months.
          
          // Re-calculate based on next expected month
          // Note: currentStart might be the result of a calculation.
          // We need to move to the Nth day of the NEXT interval month.
          let tempDate = addMonths(currentStart, interval);
          
          // Calculate the target day for that month
          // However, if we just started, currentStart IS likely the first valid date (or close to it)
          // The loop logic:
          // 1. Validate current
          // 2. Add current
          // 3. Calculate next
          
          // So 'nextStart' needs to be the occurrence in the next month(s)
          // We take the current month, add interval, find the Nth weekday
          let targetNext = getNthWeekdayOfMonth(
              tempDate.getFullYear(), 
              tempDate.getMonth(), 
              config.dayOfWeek, 
              config.weekPosition
          );

          // Edge case: Sometimes a month doesn't HAVE a 5th Monday.
          // In that case, we should probably skip that month and go to the next interval?
          // Or just let it be null and the loop handles it?
          // If null, we should try the next interval to avoid infinite loop of stuck dates
          let safety = 0;
          while (!targetNext && safety < 12) {
             tempDate = addMonths(tempDate, 1);
             targetNext = getNthWeekdayOfMonth(
               tempDate.getFullYear(),
               tempDate.getMonth(),
               config.dayOfWeek,
               config.weekPosition
             );
             safety++;
          }
          
          nextStart = targetNext;
        }

        // Validate nextStart
        if (!isValidDate(nextStart)) {
            break; // Stop if calculation resulted in invalid date
        }

        currentStart = nextStart;
        
        // Safety break for infinite loops if nextStart isn't advancing significantly or invalid
        // or if we exceed a sane number of loop iterations (safeguard against complex logic errors)
        if (instances.length > maxLimit + 10) break;
        
        // Ensure we moved forward in time to avoid infinite loops
        if (instances.length > 0 && !isAfter(currentStart, instances[instances.length-1].start)) {
             // Force advance if logic didn't move forward
             if (config.type === RECURRENCE_TYPES.MONTHLY_BY_WEEK) {
                 currentStart = addMonths(currentStart, 1);
             } else {
                 currentStart = addDays(currentStart, 1);
             }
        }
      }

      return instances;

  } catch (err) {
      console.error("Error generating recurrence dates:", err);
      // Return at least the base event if generation fails, to allow saving single instance?
      // Or return empty array to signal failure?
      // Safer to return just the original instance if everything else fails but start/end valid
      return [{ start: validStart, end: validEnd }];
  }
};

// Helper for weekly recurrence calculation
const getNextWeeklyDate = (currentDate, config) => {
  try {
      const currentDayOfWeek = getDay(currentDate);
      const days = (config.daysOfWeek || []).sort((a, b) => a - b);
      
      if (days.length === 0) return addWeeks(currentDate, Math.max(1, config.interval || 1));

      // Find if there is a day later in this week
      const nextDayInWeek = days.find(d => d > currentDayOfWeek);
      
      if (nextDayInWeek !== undefined) {
        // Jump to that day in same week
        return addDays(currentDate, nextDayInWeek - currentDayOfWeek);
      } else {
        // Jump to first available day in next interval week
        const firstDay = days[0];
        
        // Move to start of next interval week
        const interval = Math.max(1, config.interval || 1);
        const nextWeekBase = addWeeks(currentDate, interval);
        
        // Find the 'firstDay' in that week
        // We use setDay to find the specific day of week in that target week
        // weekStartsOn: 0 (Sunday) ensures we stay within the "week" defined by Sunday start
        const targetDate = setDay(nextWeekBase, firstDay, { weekStartsOn: 0 });
        
        // Ensure we moved forward. setDay might move backwards if we are late in the week
        // e.g. current is Fri, add 1 week -> next Fri. setDay(Mon) -> Mon of that same next week. Correct.
        // e.g. current is Sun, add 1 week -> next Sun. setDay(Mon) -> Mon of that next week. Correct.
        
        // Edge case: if addWeeks + setDay results in a date <= currentDate (should not happen with interval >= 1)
        if (!isAfter(targetDate, currentDate)) {
             return addDays(currentDate, 7 * interval); // Fallback
        }
        
        return targetDate;
      }
  } catch (e) {
      console.error("Error calculating next weekly date", e);
      return addDays(currentDate, 7); // Safe fallback
  }
};