import { call, os } from "@orpc/server";
import { z } from "zod";
import { randomUUID } from "crypto";
import { createKV } from "../lib/create-kv.js";
import { assertOwner } from "../lib/auth.js";

// Datenmodelle
const RecurringRuleSchema = z.object({
  id: z.string(),
  dayOfWeek: z.number().int().min(0).max(6), // 0=Sonntag, 1=Montag, ..., 6=Samstag
  startTime: z.string().regex(/^\d{2}:\d{2}$/), // HH:MM Format
  endTime: z.string().regex(/^\d{2}:\d{2}$/), // HH:MM Format
  isActive: z.boolean(),
  createdAt: z.string(),
  // LEGACY: slotDurationMinutes - deprecated field for generateSlots only, will be removed
  slotDurationMinutes: z.number().int().min(1).optional(),
});

const TimeOffPeriodSchema = z.object({
  id: z.string(),
  startDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/), // YYYY-MM-DD
  endDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/), // YYYY-MM-DD
  reason: z.string(),
  createdAt: z.string(),
});

export type RecurringRule = z.output<typeof RecurringRuleSchema>;
export type TimeOffPeriod = z.output<typeof TimeOffPeriodSchema>;

// KV-Stores
const recurringRulesKV = createKV<RecurringRule>("recurringRules");
const timeOffPeriodsKV = createKV<TimeOffPeriod>("timeOffPeriods");


// Import bookings and treatments KV stores for getAvailableTimes endpoint
const bookingsKV = createKV<any>("bookings");
const treatmentsKV = createKV<any>("treatments");

// Owner-Authentifizierung zentralisiert in ../lib/auth.ts

// Helper-Funktionen
function parseTime(timeStr: string): number {
  const [hours, minutes] = timeStr.split(':').map(Number);
  return hours * 60 + minutes; // Minuten seit Mitternacht
}

function formatTime(minutes: number): string {
  const hours = Math.floor(minutes / 60);
  const mins = minutes % 60;
  return `${String(hours).padStart(2, '0')}:${String(mins).padStart(2, '0')}`;
}

function addDays(date: Date, days: number): Date {
  const result = new Date(date);
  result.setDate(result.getDate() + days);
  return result;
}

function formatDate(date: Date): string {
  return date.toISOString().split('T')[0];
}

function isDateInTimeOffPeriod(date: string, periods: TimeOffPeriod[]): boolean {
  return periods.some(period => date >= period.startDate && date <= period.endDate);
}

// Helper-Funktion zur Erkennung überlappender Regeln
function detectOverlappingRules(newRule: { dayOfWeek: number; startTime: string; endTime: string; id?: string }, existingRules: RecurringRule[]): RecurringRule[] {
  const newStart = parseTime(newRule.startTime);
  const newEnd = parseTime(newRule.endTime);
  
  return existingRules.filter(rule => {
    // Gleicher Wochentag und nicht dieselbe Regel (bei Updates)
    if (rule.dayOfWeek !== newRule.dayOfWeek || rule.id === newRule.id) {
      return false;
    }
    
    const existingStart = parseTime(rule.startTime);
    const existingEnd = parseTime(rule.endTime);
    
    // Überlappung wenn: neue Startzeit < bestehende Endzeit UND neue Endzeit > bestehende Startzeit
    return newStart < existingEnd && newEnd > existingStart;
  });
}

// CRUD-Endpoints für Recurring Rules
const createRule = os
  .input(
    z.object({
      sessionId: z.string(),
      dayOfWeek: z.number().int().min(0).max(6),
      startTime: z.string().regex(/^\d{2}:\d{2}$/),
      endTime: z.string().regex(/^\d{2}:\d{2}$/),
    }).passthrough()
  )
  .handler(async ({ input }) => {
    try {
      await assertOwner(input.sessionId);
      
      // Validierung: startTime < endTime
      const startMinutes = parseTime(input.startTime);
      const endMinutes = parseTime(input.endTime);
      if (startMinutes >= endMinutes) {
        throw new Error("Startzeit muss vor der Endzeit liegen.");
      }
      
      // Überlappungsprüfung
      const allRules = await recurringRulesKV.getAllItems();
      const overlappingRules = detectOverlappingRules(input, allRules);
      
      if (overlappingRules.length > 0) {
        const overlappingTimes = overlappingRules.map(rule => `${rule.startTime}-${rule.endTime}`).join(", ");
        throw new Error(`Überlappung mit bestehenden Regeln erkannt: ${overlappingTimes}. Bitte Zeitfenster anpassen.`);
      }
      
      const id = randomUUID();
      const rule: RecurringRule = {
        id,
        dayOfWeek: input.dayOfWeek,
        startTime: input.startTime,
        endTime: input.endTime,
        isActive: true,
        createdAt: new Date().toISOString(),
      };
      
      await recurringRulesKV.setItem(id, rule);
      return rule;
    } catch (err) {
      console.error("recurring-availability.createRule error", err);
      throw err;
    }
  });

const updateRule = os
  .input(RecurringRuleSchema.extend({ sessionId: z.string() }).passthrough())
  .handler(async ({ input }) => {
    await assertOwner(input.sessionId);
    
    // Validierung: startTime < endTime
    const startMinutes = parseTime(input.startTime);
    const endMinutes = parseTime(input.endTime);
    if (startMinutes >= endMinutes) {
      throw new Error("Startzeit muss vor der Endzeit liegen.");
    }
    
    // Überlappungsprüfung
    const allRules = await recurringRulesKV.getAllItems();
    const overlappingRules = detectOverlappingRules(input, allRules);
    
    if (overlappingRules.length > 0) {
      const overlappingTimes = overlappingRules.map(rule => `${rule.startTime}-${rule.endTime}`).join(", ");
      throw new Error(`Überlappung mit bestehenden Regeln erkannt: ${overlappingTimes}. Bitte Zeitfenster anpassen.`);
    }
    
    const { sessionId, ...rule } = input as any;
    await recurringRulesKV.setItem(rule.id, rule as RecurringRule);
    return rule as RecurringRule;
  });

const deleteRule = os
  .input(z.object({ sessionId: z.string(), id: z.string() }))
  .handler(async ({ input }) => {
    await assertOwner(input.sessionId);
    await recurringRulesKV.removeItem(input.id);
  });

const toggleRuleActive = os
  .input(z.object({ sessionId: z.string(), id: z.string() }))
  .handler(async ({ input }) => {
    await assertOwner(input.sessionId);
    const rule = await recurringRulesKV.getItem(input.id);
    if (!rule) throw new Error("Regel nicht gefunden.");
    
    rule.isActive = !rule.isActive;
    await recurringRulesKV.setItem(input.id, rule);
    return rule;
  });

const listRules = os.handler(async () => {
  const allRules = await recurringRulesKV.getAllItems();
  return allRules.sort((a, b) => {
    if (a.dayOfWeek !== b.dayOfWeek) return a.dayOfWeek - b.dayOfWeek;
    return a.startTime.localeCompare(b.startTime);
  });
});

const adminListRules = os
  .input(z.object({ sessionId: z.string() }))
  .handler(async ({ input }) => {
    await assertOwner(input.sessionId);
    const allRules = await recurringRulesKV.getAllItems();
    return allRules.sort((a, b) => {
      if (a.dayOfWeek !== b.dayOfWeek) return a.dayOfWeek - b.dayOfWeek;
      return a.startTime.localeCompare(b.startTime);
    });
  });

// CRUD-Endpoints für Time-Off Periods
const createTimeOff = os
  .input(
    z.object({
      sessionId: z.string(),
      startDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
      endDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
      reason: z.string(),
    })
  )
  .handler(async ({ input }) => {
    try {
      await assertOwner(input.sessionId);
      
      // Validierung: startDate <= endDate
      if (input.startDate > input.endDate) {
        throw new Error("Startdatum muss vor oder am Enddatum liegen.");
      }
      
      const id = randomUUID();
      const timeOff: TimeOffPeriod = {
        id,
        startDate: input.startDate,
        endDate: input.endDate,
        reason: input.reason,
        createdAt: new Date().toISOString(),
      };
      
      await timeOffPeriodsKV.setItem(id, timeOff);
      return timeOff;
    } catch (err) {
      console.error("recurring-availability.createTimeOff error", err);
      throw err;
    }
  });

const updateTimeOff = os
  .input(TimeOffPeriodSchema.extend({ sessionId: z.string() }).passthrough())
  .handler(async ({ input }) => {
    await assertOwner(input.sessionId);
    
    // Validierung: startDate <= endDate
    if (input.startDate > input.endDate) {
      throw new Error("Startdatum muss vor oder am Enddatum liegen.");
    }
    
    const { sessionId, ...timeOff } = input as any;
    await timeOffPeriodsKV.setItem(timeOff.id, timeOff as TimeOffPeriod);
    return timeOff as TimeOffPeriod;
  });

const deleteTimeOff = os
  .input(z.object({ sessionId: z.string(), id: z.string() }))
  .handler(async ({ input }) => {
    await assertOwner(input.sessionId);
    await timeOffPeriodsKV.removeItem(input.id);
  });

const listTimeOff = os.handler(async () => {
  const allTimeOff = await timeOffPeriodsKV.getAllItems();
  return allTimeOff.sort((a, b) => a.startDate.localeCompare(b.startDate));
});

const adminListTimeOff = os
  .input(z.object({ sessionId: z.string() }))
  .handler(async ({ input }) => {
    await assertOwner(input.sessionId);
    const allTimeOff = await timeOffPeriodsKV.getAllItems();
    return allTimeOff.sort((a, b) => a.startDate.localeCompare(b.startDate));
  });


// Get Available Times Endpoint
const getAvailableTimes = os
  .input(
    z.object({
      date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
      treatmentIds: z.array(z.string())
        .min(1, "Mindestens eine Behandlung muss ausgewählt werden")
        .max(3, "Maximal 3 Behandlungen können ausgewählt werden")
        .refine(list => {
          return list.length === new Set(list).size;
        }, { message: "Doppelte Behandlungen sind nicht erlaubt" }),
    })
  )
  .handler(async ({ input }) => {
    try {
      // Validate that the date is not in the past
      const today = new Date();
      const inputDate = new Date(input.date);
      today.setHours(0, 0, 0, 0);
      inputDate.setHours(0, 0, 0, 0);
      
      if (inputDate < today) {
        return [];
      }
      
      // Get multiple treatments and calculate total duration
      const treatments = await Promise.all(
        input.treatmentIds.map(id => treatmentsKV.getItem(id))
      );
      
      // Validate that all treatments exist
      const missingTreatments = treatments
        .map((t, i) => t ? null : input.treatmentIds[i])
        .filter(id => id !== null);
      
      if (missingTreatments.length > 0) {
        throw new Error(`Behandlung(en) nicht gefunden: ${missingTreatments.join(', ')}`);
      }
      
      // Calculate total duration by summing all treatment durations
      const treatmentDuration = treatments.reduce((sum, t) => sum + (t?.duration || 0), 0);
      
      // Parse the date to get day of week
      const [year, month, day] = input.date.split('-').map(Number);
      const localDate = new Date(year, month - 1, day);
      const dayOfWeek = localDate.getDay(); // 0=Sonntag, 1=Montag, ...
      
      // Find matching recurring rules
      const allRules = await recurringRulesKV.getAllItems();
      const matchingRules = allRules.filter(rule => 
        rule.isActive === true && rule.dayOfWeek === dayOfWeek
      );
      
      if (matchingRules.length === 0) {
        return []; // No rules for this day of week
      }
      
      // Check time-off periods
      const timeOffPeriods = await timeOffPeriodsKV.getAllItems();
      if (isDateInTimeOffPeriod(input.date, timeOffPeriods)) {
        return []; // Date is blocked by time-off period
      }
      
      // Generate 15-minute intervals with boundary alignment
      const availableTimes: string[] = [];
      
      // Helper functions for 15-minute boundary alignment
      const ceilTo15 = (m: number) => m % 15 === 0 ? m : m + (15 - (m % 15));
      const floorTo15 = (m: number) => m - (m % 15);
      
      for (const rule of matchingRules) {
        const startMinutes = parseTime(rule.startTime);
        const endMinutes = parseTime(rule.endTime);
        
        let currentMinutes = ceilTo15(startMinutes);
        const endBound = floorTo15(endMinutes);
        
        while (currentMinutes + treatmentDuration <= endBound) {
          const timeStr = formatTime(currentMinutes);
          availableTimes.push(timeStr);
          currentMinutes += 15; // 15-minute intervals
        }
      }
      
      // Get all bookings for this date and their treatments
      const allBookings = await bookingsKV.getAllItems();
      const dateBookings = allBookings.filter(booking => 
        booking.appointmentDate === input.date && 
        ['pending', 'confirmed', 'completed'].includes(booking.status)
      );
      
      // Build cache only for legacy treatmentId bookings
      const legacyTreatmentIds = [...new Set(dateBookings.filter(b => b.treatmentId).map(b => b.treatmentId as string))];
      const treatmentDurationMap = new Map<string, number>();
      
      // Only build cache if there are legacy bookings
      if (legacyTreatmentIds.length > 0) {
        for (const id of legacyTreatmentIds) {
          const t = await treatmentsKV.getItem(id);
          treatmentDurationMap.set(id, t?.duration || 60);
        }
      }
      
      // Filter out booking conflicts
      const availableTimesFiltered = availableTimes.filter(slotTime => {
        const slotStartMinutes = parseTime(slotTime);
        const slotEndMinutes = slotStartMinutes + treatmentDuration; // total from selected treatments
        
        const hasConflict = dateBookings.some(booking => {
          let bookingDuration: number;
          if (booking.treatments && booking.treatments.length > 0) {
            bookingDuration = booking.treatments.reduce((sum: number, t: { duration: number }) => sum + t.duration, 0);
          } else if (booking.bookedDurationMinutes) {
            bookingDuration = booking.bookedDurationMinutes;
          } else if (booking.treatmentId) {
            bookingDuration = treatmentDurationMap.get(booking.treatmentId) || 60;
          } else {
            bookingDuration = 60;
          }
          
          const bookingStart = parseTime(booking.appointmentTime);
          const bookingEnd = bookingStart + bookingDuration;
          return slotStartMinutes < bookingEnd && slotEndMinutes > bookingStart;
        });
        
        return !hasConflict;
      });
      
      // Filter out past times for today
      const now = new Date();
      const isToday = inputDate.getTime() === today.getTime();
      
      const finalAvailableTimes = isToday 
        ? availableTimesFiltered.filter(timeStr => {
            const slotTime = parseTime(timeStr);
            const currentTime = now.getHours() * 60 + now.getMinutes();
            return slotTime > currentTime;
          })
        : availableTimesFiltered;
      
      // Deduplicate and sort chronologically
      const unique = Array.from(new Set(finalAvailableTimes));
      return unique.sort((a, b) => a.localeCompare(b));
      
    } catch (err) {
      console.error("recurring-availability.getAvailableTimes error", err);
      throw err;
    }
  });

// Live-Queries
const live = {
  listRules: os.handler(async function* ({ signal }) {
    yield call(listRules, {}, { signal });
    for await (const _ of recurringRulesKV.subscribe()) {
      yield call(listRules, {}, { signal });
    }
  }),
  
  listTimeOff: os.handler(async function* ({ signal }) {
    yield call(listTimeOff, {}, { signal });
    for await (const _ of timeOffPeriodsKV.subscribe()) {
      yield call(listTimeOff, {}, { signal });
    }
  }),
  
  adminListRules: os
    .input(z.object({ sessionId: z.string() }))
    .handler(async function* ({ input, signal }) {
      await assertOwner(input.sessionId);
      const allRules = await recurringRulesKV.getAllItems();
      const sortedRules = allRules.sort((a, b) => {
        if (a.dayOfWeek !== b.dayOfWeek) return a.dayOfWeek - b.dayOfWeek;
        return a.startTime.localeCompare(b.startTime);
      });
      yield sortedRules;
      for await (const _ of recurringRulesKV.subscribe()) {
        const updatedRules = await recurringRulesKV.getAllItems();
        const sortedUpdatedRules = updatedRules.sort((a, b) => {
          if (a.dayOfWeek !== b.dayOfWeek) return a.dayOfWeek - b.dayOfWeek;
          return a.startTime.localeCompare(b.startTime);
        });
        yield sortedUpdatedRules;
      }
    }),
    
  adminListTimeOff: os
    .input(z.object({ sessionId: z.string() }))
    .handler(async function* ({ input, signal }) {
      await assertOwner(input.sessionId);
      const allTimeOff = await timeOffPeriodsKV.getAllItems();
      const sortedTimeOff = allTimeOff.sort((a, b) => a.startDate.localeCompare(b.startDate));
      yield sortedTimeOff;
      for await (const _ of timeOffPeriodsKV.subscribe()) {
        const updatedTimeOff = await timeOffPeriodsKV.getAllItems();
        const sortedUpdatedTimeOff = updatedTimeOff.sort((a, b) => a.startDate.localeCompare(b.startDate));
        yield sortedUpdatedTimeOff;
      }
    }),
};

export const router = {
  // Recurring Rules
  createRule,
  updateRule,
  deleteRule,
  toggleRuleActive,
  listRules,
  adminListRules,
  
  // Time-Off Periods
  createTimeOff,
  updateTimeOff,
  deleteTimeOff,
  listTimeOff,
  adminListTimeOff,
  
  // Availability
  getAvailableTimes,
  
  // Live queries
  live,
};
