import { call, os } from "@orpc/server";
import { z } from "zod";
import { randomUUID } from "crypto";
import { createKV } from "../lib/create-kv.js";
import { sendEmail, sendEmailWithAGB, sendEmailWithAGBAndCalendar, sendEmailWithInspirationPhoto } from "../lib/email.js";
import { renderBookingPendingHTML, renderBookingConfirmedHTML, renderBookingCancelledHTML, renderAdminBookingNotificationHTML, renderBookingRescheduleProposalHTML, renderAdminRescheduleAcceptedHTML, renderAdminRescheduleDeclinedHTML, renderCustomerMessageHTML } from "../lib/email-templates.js";
import { router as rootRouter } from "./index.js";
import { createORPCClient } from "@orpc/client";
import { RPCLink } from "@orpc/client/fetch";
import { checkBookingRateLimit, getClientIP } from "../lib/rate-limiter.js";
import { validateEmail } from "../lib/email-validator.js";

// Create a server-side client to call other RPC endpoints
const serverPort = process.env.PORT ? parseInt(process.env.PORT) : 3000;
const link = new RPCLink({ url: `http://localhost:${serverPort}/rpc` });
// Typisierung über any, um Build-Inkompatibilität mit NestedClient zu vermeiden (nur für interne Server-Calls)
const queryClient = createORPCClient<any>(link);

// Helper function to convert date from yyyy-mm-dd to dd.mm.yyyy
function formatDateGerman(dateString: string): string {
  const [year, month, day] = dateString.split('-');
  return `${day}.${month}.${year}`;
}

// Helper function to generate URLs from DOMAIN environment variable
function generateUrl(path: string = ''): string {
  const domain = process.env.DOMAIN || 'localhost:5173';
  const protocol = domain.includes('localhost') ? 'http' : 'https';
  return `${protocol}://${domain}${path}`;
}

// Helper function to parse time string to minutes since midnight
function parseTime(timeStr: string): number {
  const [hours, minutes] = timeStr.split(':').map(Number);
  return hours * 60 + minutes;
}

// Helper function to check if date is in time-off period
function isDateInTimeOffPeriod(date: string, periods: TimeOffPeriod[]): boolean {
  return periods.some(period => date >= period.startDate && date <= period.endDate);
}

// Helper function to validate booking time against recurring rules
async function validateBookingAgainstRules(
  date: string,
  time: string,
  totalDuration: number
): Promise<void> {
  // Parse date to get day of week
  const [year, month, day] = date.split('-').map(Number);
  const localDate = new Date(year, month - 1, day);
  const dayOfWeek = localDate.getDay();
  
  // Check time-off periods
  const timeOffPeriods = await timeOffPeriodsKV.getAllItems();
  if (isDateInTimeOffPeriod(date, timeOffPeriods)) {
    throw new Error("Dieser Tag ist nicht verfügbar (Urlaubszeit).");
  }
  
  // Find matching recurring rules
  const allRules = await recurringRulesKV.getAllItems();
  const matchingRules = allRules.filter(rule => 
    rule.isActive === true && rule.dayOfWeek === dayOfWeek
  );
  
  if (matchingRules.length === 0) {
    throw new Error("Für diesen Wochentag sind keine Termine verfügbar.");
  }
  
  // Check if booking time falls within any rule's time span
  const bookingStartMinutes = parseTime(time);
  const bookingEndMinutes = bookingStartMinutes + totalDuration;
  
  const isWithinRules = matchingRules.some(rule => {
    const ruleStartMinutes = parseTime(rule.startTime);
    const ruleEndMinutes = parseTime(rule.endTime);
    
    // Booking must start at or after rule start and end at or before rule end
    return bookingStartMinutes >= ruleStartMinutes && bookingEndMinutes <= ruleEndMinutes;
  });
  
  if (!isWithinRules) {
    throw new Error("Die gewählte Uhrzeit liegt außerhalb der verfügbaren Zeiten.");
  }
}

// Helper function to check for booking conflicts
async function checkBookingConflicts(
  date: string,
  time: string,
  totalDuration: number,
  excludeBookingId?: string
): Promise<void> {
  const allBookings = await kv.getAllItems();
  const dateBookings = allBookings.filter(booking => 
    booking.appointmentDate === date && 
    ['pending', 'confirmed', 'completed'].includes(booking.status) &&
    booking.id !== excludeBookingId
  );
  
  const bookingStartMinutes = parseTime(time);
  const bookingEndMinutes = bookingStartMinutes + totalDuration;
  
  // Cache treatment durations by ID to avoid N+1 lookups (for backward compatibility with old bookings)
  const uniqueTreatmentIds = [...new Set(dateBookings.filter(b => b.treatmentId).map(booking => booking.treatmentId!))];
  const treatmentDurationMap = new Map<string, number>();
  
  for (const treatmentId of uniqueTreatmentIds) {
    const treatment = await treatmentsKV.getItem(treatmentId);
    treatmentDurationMap.set(treatmentId, treatment?.duration || 60);
  }
  
  // Check for overlaps with existing bookings
  for (const existingBooking of dateBookings) {
    let existingDuration: number;
    
    // Handle both new bookings with treatments array and old bookings with treatmentId
    if (existingBooking.treatments && existingBooking.treatments.length > 0) {
      // New format: calculate duration from treatments array
      existingDuration = existingBooking.treatments.reduce((sum, t) => sum + t.duration, 0);
    } else if (existingBooking.treatmentId) {
      // Old format: use cached duration or fallback to bookedDurationMinutes if available
      existingDuration = treatmentDurationMap.get(existingBooking.treatmentId) || 60;
      if (existingBooking.bookedDurationMinutes) {
        existingDuration = existingBooking.bookedDurationMinutes;
      }
    } else {
      // Fallback for bookings without treatment info
      existingDuration = existingBooking.bookedDurationMinutes || 60;
    }
    
    const existingStartMinutes = parseTime(existingBooking.appointmentTime);
    const existingEndMinutes = existingStartMinutes + existingDuration;
    
    // Check overlap: bookingStart < existingEnd && bookingEnd > existingStart
    if (bookingStartMinutes < existingEndMinutes && bookingEndMinutes > existingStartMinutes) {
      throw new Error("Dieser Zeitslot ist bereits gebucht. Bitte wähle eine andere Zeit.");
    }
  }
}

// Reusable treatments array schema with duplicate validation
const TreatmentsArraySchema = z.array(z.object({
  id: z.string(),
  name: z.string(),
  duration: z.number().positive(),
  price: z.number().nonnegative(),
}))
  .min(1, "Mindestens eine Behandlung muss ausgewählt werden")
  .max(3, "Maximal 3 Behandlungen können ausgewählt werden")
  .refine(list => {
    const ids = list.map(t => t.id);
    return ids.length === new Set(ids).size;
  }, { message: "Doppelte Behandlungen sind nicht erlaubt" });

const CreateBookingInputSchema = z.object({
  treatments: TreatmentsArraySchema,
  customerName: z.string().min(2, "Name muss mindestens 2 Zeichen lang sein"),
  customerEmail: z.string().email("Ungültige E-Mail-Adresse"),
  customerPhone: z.string().min(5, "Telefonnummer muss mindestens 5 Zeichen lang sein").optional(),
  appointmentDate: z.string(), // ISO date string
  appointmentTime: z.string(), // HH:MM format
  notes: z.string().optional(),
  inspirationPhoto: z.string().optional(), // Base64 encoded image data
});

const BookingSchema = z.object({
  id: z.string(),
  treatments: z.array(z.object({
    id: z.string(),
    name: z.string(),
    duration: z.number().positive(),
    price: z.number().nonnegative()
  })),
  customerName: z.string().min(2, "Name muss mindestens 2 Zeichen lang sein"),
  customerEmail: z.string().email("Ungültige E-Mail-Adresse").optional(),
  customerPhone: z.string().min(5, "Telefonnummer muss mindestens 5 Zeichen lang sein").optional(),
  appointmentDate: z.string(), // ISO date string
  appointmentTime: z.string(), // HH:MM format
  status: z.enum(["pending", "confirmed", "cancelled", "completed"]),
  notes: z.string().optional(),
  inspirationPhoto: z.string().optional(), // Base64 encoded image data
  createdAt: z.string(),
  // DEPRECATED: slotId is no longer used for validation, kept for backward compatibility
  slotId: z.string().optional(),
  // DEPRECATED: treatmentId and bookedDurationMinutes kept for backward compatibility
  treatmentId: z.string().optional(),
  bookedDurationMinutes: z.number().optional(),
});

type Booking = z.output<typeof BookingSchema>;

const kv = createKV<Booking>("bookings");

// DEPRECATED: Availability slots are no longer used for booking validation
// type Availability = {
//   id: string;
//   date: string;
//   time: string;
//   durationMinutes: number;
//   status: "free" | "reserved";
//   reservedByBookingId?: string;
//   createdAt: string;
// };
// const availabilityKV = createAvailabilityKV<Availability>("availability");

type RecurringRule = {
  id: string;
  dayOfWeek: number;
  startTime: string;
  endTime: string;
  isActive: boolean;
  createdAt: string;
  slotDurationMinutes?: number;
};

type TimeOffPeriod = {
  id: string;
  startDate: string;
  endDate: string;
  reason: string;
  createdAt: string;
};

const recurringRulesKV = createKV<RecurringRule>("recurringRules");
const timeOffPeriodsKV = createKV<TimeOffPeriod>("timeOffPeriods");

// Import treatments KV for admin notifications
type Treatment = {
  id: string;
  name: string;
  description: string;
  price: number;
  duration: number;
  category: string;
  createdAt: string;
};
const treatmentsKV = createKV<Treatment>("treatments");

const create = os
  .input(CreateBookingInputSchema)
  .handler(async ({ input }) => {
    // console.log("Booking create called with input:", {
    //   ...input,
    //   inspirationPhoto: input.inspirationPhoto ? `[${input.inspirationPhoto.length} chars]` : null
    // });
    
    try {
    // Rate limiting check (ohne IP, falls Context-Header im Build nicht verfügbar sind)
    const rateLimitResult = checkBookingRateLimit({
      ip: undefined,
      email: input.customerEmail,
    });
    
    if (!rateLimitResult.allowed) {
      const retryMinutes = rateLimitResult.retryAfterSeconds 
        ? Math.ceil(rateLimitResult.retryAfterSeconds / 60) 
        : 10;
      throw new Error(
        `Zu viele Buchungsanfragen. Bitte versuche es in ${retryMinutes} Minute${retryMinutes > 1 ? 'n' : ''} erneut.`
      );
    }
    
    // Email validation before slot reservation
    console.log(`Validating email: ${input.customerEmail}`);
    const emailValidation = await validateEmail(input.customerEmail);
    console.log(`Email validation result:`, emailValidation);
    if (!emailValidation.valid) {
      console.log(`Email validation failed: ${emailValidation.reason}`);
      throw new Error(emailValidation.reason || "Ungültige E-Mail-Adresse");
    }

    // Validate appointment time is on 15-minute grid
    const appointmentMinutes = parseTime(input.appointmentTime);
    if (appointmentMinutes % 15 !== 0) {
      throw new Error("Termine müssen auf 15-Minuten-Raster ausgerichtet sein (z.B. 09:00, 09:15, 09:30, 09:45).");
    }
    
    // Validate that the booking is not in the past
    const today = new Date().toISOString().split("T")[0]; // YYYY-MM-DD
    if (input.appointmentDate < today) {
      throw new Error("Buchungen für vergangene Termine sind nicht möglich.");
    }
    
    // For today's bookings, check if the time is not in the past
    if (input.appointmentDate === today) {
      const now = new Date();
      const currentTime = `${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}`;
      if (input.appointmentTime <= currentTime) {
        throw new Error("Buchungen für vergangene Uhrzeiten sind nicht möglich.");
      }
    }
    
    // Prevent double booking: same customer email with pending/confirmed on same date
    // Skip duplicate check when DISABLE_DUPLICATE_CHECK is set
    if (!process.env.DISABLE_DUPLICATE_CHECK) {
      const existing = await kv.getAllItems();
      const hasConflict = existing.some(b =>
        (b.customerEmail && b.customerEmail.toLowerCase() === input.customerEmail.toLowerCase()) &&
        b.appointmentDate === input.appointmentDate &&
        (b.status === "pending" || b.status === "confirmed")
      );
      if (hasConflict) {
        throw new Error("Du hast bereits eine Buchung für dieses Datum. Bitte wähle einen anderen Tag oder storniere zuerst.");
      }
    }
    // Validate all treatments exist and snapshot them from KV
    const snapshottedTreatments = [] as Array<{id: string; name: string; duration: number; price: number}>;
    for (const inputTreatment of input.treatments) {
      const treatment = await treatmentsKV.getItem(inputTreatment.id);
      if (!treatment) {
        throw new Error(`Behandlung "${inputTreatment.name}" nicht gefunden.`);
      }
      // Verify snapshot data matches current treatment data
      if (treatment.name !== inputTreatment.name || treatment.duration !== inputTreatment.duration || treatment.price !== inputTreatment.price) {
        throw new Error(`Behandlungsdaten für "${inputTreatment.name}" stimmen nicht überein. Bitte lade die Seite neu.`);
      }
      snapshottedTreatments.push({ id: treatment.id, name: treatment.name, duration: treatment.duration, price: treatment.price });
    }
    const totalDuration = snapshottedTreatments.reduce((sum, t) => sum + t.duration, 0);

    // Validate booking time against recurring rules
    await validateBookingAgainstRules(
      input.appointmentDate,
      input.appointmentTime,
      totalDuration
    );

    // Check for booking conflicts
    await checkBookingConflicts(
      input.appointmentDate,
      input.appointmentTime,
      totalDuration
    );

    const id = randomUUID();
    const booking = { 
      id, 
      treatments: snapshottedTreatments,
      customerName: input.customerName,
      customerEmail: input.customerEmail,
      customerPhone: input.customerPhone,
      appointmentDate: input.appointmentDate,
      appointmentTime: input.appointmentTime,
      notes: input.notes,
      inspirationPhoto: input.inspirationPhoto,
      status: "pending" as const,
      createdAt: new Date().toISOString()
    };

    // Save the booking
    await kv.setItem(id, booking);

    // Notify customer: request received (pending)
    void (async () => {
      // Create booking access token for status viewing
      const bookingAccessToken = await queryClient.cancellation.createToken({ bookingId: id });
      const bookingUrl = generateUrl(`/booking/${bookingAccessToken.token}`);
      
      const formattedDate = formatDateGerman(input.appointmentDate);
      const homepageUrl = generateUrl();
      const html = await renderBookingPendingHTML({ 
        name: input.customerName, 
        date: input.appointmentDate, 
        time: input.appointmentTime,
        statusUrl: bookingUrl,
        treatments: input.treatments
      });
      
      const treatmentsText = input.treatments.map(t => `- ${t.name} (${t.duration} Min, ${(t.price / 100).toFixed(2)} €)`).join('\n');
      const totalDuration = input.treatments.reduce((sum, t) => sum + t.duration, 0);
      const totalPrice = input.treatments.reduce((sum, t) => sum + (t.price / 100), 0);
      
      await sendEmail({
      to: input.customerEmail,
      subject: "Deine Terminanfrage ist eingegangen",
      text: `Hallo ${input.customerName},\n\nwir haben deine Anfrage für ${formattedDate} um ${input.appointmentTime} erhalten.\n\nBehandlungen:\n${treatmentsText}\n\nGesamt: ${totalDuration} Min, ${totalPrice.toFixed(2)} €\n\nWir bestätigen deinen Termin in Kürze. Du erhältst eine weitere E-Mail, sobald der Termin bestätigt ist.\n\nTermin-Status ansehen: ${bookingUrl}\n\nRechtliche Informationen: ${generateUrl('/legal')}\nZur Website: ${homepageUrl}\n\nLiebe Grüße\nStargirlnails Kiel`,
        html,
      }).catch(() => {});
    })();

    // Notify admin: new booking request (with photo if available)
    void (async () => {
      if (!process.env.ADMIN_EMAIL) return;
      
      const adminHtml = await renderAdminBookingNotificationHTML({
        name: input.customerName,
        date: input.appointmentDate,
        time: input.appointmentTime,
        treatments: input.treatments,
        phone: input.customerPhone || "Nicht angegeben",
        notes: input.notes,
        hasInspirationPhoto: !!input.inspirationPhoto
      });

      const homepageUrl = generateUrl();
      const treatmentsText = input.treatments.map(t => `  - ${t.name} (${t.duration} Min, ${(t.price / 100).toFixed(2)} €)`).join('\n');
      const totalDuration = input.treatments.reduce((sum, t) => sum + t.duration, 0);
      const totalPrice = input.treatments.reduce((sum, t) => sum + (t.price / 100), 0);
      
      const adminText = `Neue Buchungsanfrage eingegangen:\n\n` +
        `Name: ${input.customerName}\n` +
        `Telefon: ${input.customerPhone || "Nicht angegeben"}\n` +
        `Behandlungen:\n${treatmentsText}\n` +
        `Gesamt: ${totalDuration} Min, ${totalPrice.toFixed(2)} €\n` +
        `Datum: ${formatDateGerman(input.appointmentDate)}\n` +
        `Uhrzeit: ${input.appointmentTime}\n` +
        `${input.notes ? `Notizen: ${input.notes}\n` : ''}` +
        `Inspiration-Foto: ${input.inspirationPhoto ? 'Im Anhang verfügbar' : 'Kein Foto hochgeladen'}\n\n` +
        `Zur Website: ${homepageUrl}\n\n` +
        `Bitte logge dich in das Admin-Panel ein, um die Buchung zu bearbeiten.`;

      if (input.inspirationPhoto) {
        await sendEmailWithInspirationPhoto({
          to: process.env.ADMIN_EMAIL,
          subject: `Neue Buchungsanfrage - ${input.customerName}`,
          text: adminText,
          html: adminHtml,
        }, input.inspirationPhoto, input.customerName).catch(() => {});
      } else {
        await sendEmail({
          to: process.env.ADMIN_EMAIL,
          subject: `Neue Buchungsanfrage - ${input.customerName}`,
          text: adminText,
          html: adminHtml,
        }).catch(() => {});
      }
    })();
    return booking;
  } catch (error) {
      console.error("Booking creation error:", error);
      
      // Re-throw the error for oRPC to handle
      throw error;
    }
  });

// Owner check reuse (simple inline version)
type Session = { id: string; userId: string; expiresAt: string; createdAt: string };
type User = { id: string; username: string; email: string; passwordHash: string; role: "customer" | "owner"; createdAt: string };
const sessionsKV = createKV<Session>("sessions");
const usersKV = createKV<User>("users");
async function assertOwner(sessionId: string): Promise<void> {
  const session = await sessionsKV.getItem(sessionId);
  if (!session) throw new Error("Invalid session");
  if (new Date(session.expiresAt) < new Date()) throw new Error("Session expired");
  const user = await usersKV.getItem(session.userId);
  if (!user || user.role !== "owner") throw new Error("Forbidden");
}

const updateStatus = os
  .input(z.object({
    sessionId: z.string(),
    id: z.string(),
    status: z.enum(["pending", "confirmed", "cancelled", "completed"])
  }))
  .handler(async ({ input }) => {
    await assertOwner(input.sessionId);
    const booking = await kv.getItem(input.id);
    if (!booking) throw new Error("Booking not found");
    
    const previousStatus = booking.status;
    const updatedBooking = { ...booking, status: input.status };
    await kv.setItem(input.id, updatedBooking);

    // Note: Slot state management removed - bookings now validated against recurring rules
    // Email notifications on status changes
    try {
      if (input.status === "confirmed") {
        // Create booking access token for this booking (status + cancellation)
        const bookingAccessToken = await queryClient.cancellation.createToken({ bookingId: booking.id });
        
        const formattedDate = formatDateGerman(booking.appointmentDate);
        const bookingUrl = generateUrl(`/booking/${bookingAccessToken.token}`);
        const homepageUrl = generateUrl();
        
        const html = await renderBookingConfirmedHTML({ 
          name: booking.customerName, 
          date: booking.appointmentDate, 
          time: booking.appointmentTime,
          cancellationUrl: bookingUrl, // Now points to booking status page
          reviewUrl: generateUrl(`/review/${bookingAccessToken.token}`),
          treatments: booking.treatments
        });
        
        const treatmentsText = booking.treatments.map(t => `- ${t.name} (${t.duration} Min, ${(t.price / 100).toFixed(2)} €)`).join('\n');
        const totalDuration = booking.treatments.reduce((sum, t) => sum + t.duration, 0);
        const totalPrice = booking.treatments.reduce((sum, t) => sum + (t.price / 100), 0);
        
        if (booking.customerEmail) {
        await sendEmailWithAGBAndCalendar({
          to: booking.customerEmail,
          subject: "Dein Termin wurde bestätigt - AGB im Anhang",
          text: `Hallo ${booking.customerName},\n\nwir haben deinen Termin am ${formattedDate} um ${booking.appointmentTime} bestätigt.\n\nBehandlungen:\n${treatmentsText}\n\nGesamt: ${totalDuration} Min, ${totalPrice.toFixed(2)} €\n\nWichtiger Hinweis: Die Allgemeinen Geschäftsbedingungen (AGB) findest du im Anhang dieser E-Mail. Bitte lies sie vor deinem Termin durch.\n\nTermin-Status ansehen und verwalten: ${bookingUrl}\nFalls du den Termin stornieren möchtest, kannst du das über den obigen Link tun.\n\nRechtliche Informationen: ${generateUrl('/legal')}\nZur Website: ${homepageUrl}\n\nBis bald!\nStargirlnails Kiel`,
          html,
          bcc: process.env.ADMIN_EMAIL ? [process.env.ADMIN_EMAIL] : undefined,
        }, {
          date: booking.appointmentDate,
          time: booking.appointmentTime,
          customerName: booking.customerName,
          customerEmail: booking.customerEmail,
          treatments: booking.treatments
        });
        }
      } else if (input.status === "cancelled") {
        const formattedDate = formatDateGerman(booking.appointmentDate);
        const homepageUrl = generateUrl();
        const html = await renderBookingCancelledHTML({ 
          name: booking.customerName, 
          date: booking.appointmentDate, 
          time: booking.appointmentTime,
          treatments: booking.treatments
        });
        
        const treatmentsText = booking.treatments.map(t => `- ${t.name} (${t.duration} Min, ${(t.price / 100).toFixed(2)} €)`).join('\n');
        const totalDuration = booking.treatments.reduce((sum, t) => sum + t.duration, 0);
        const totalPrice = booking.treatments.reduce((sum, t) => sum + (t.price / 100), 0);
        
        if (booking.customerEmail) {
        await sendEmail({
          to: booking.customerEmail,
          subject: "Dein Termin wurde abgesagt",
          text: `Hallo ${booking.customerName},\n\nleider wurde dein Termin am ${formattedDate} um ${booking.appointmentTime} abgesagt.\n\nBehandlungen:\n${treatmentsText}\n\nGesamt: ${totalDuration} Min, ${totalPrice.toFixed(2)} €\n\nBitte buche einen neuen Termin.\n\nRechtliche Informationen: ${generateUrl('/legal')}\nZur Website: ${homepageUrl}\n\nLiebe Grüße\nStargirlnails Kiel`,
          html,
          bcc: process.env.ADMIN_EMAIL ? [process.env.ADMIN_EMAIL] : undefined,
        });
        }
      }
    } catch (e) {
      console.error("Email send failed:", e);
    }
    return updatedBooking;
  });

const remove = os
  .input(z.object({
    sessionId: z.string(),
    id: z.string(),
    sendEmail: z.boolean().optional().default(false)
  }))
  .handler(async ({ input }) => {
    await assertOwner(input.sessionId);
    const booking = await kv.getItem(input.id);
    if (!booking) throw new Error("Booking not found");

    // Guard against deletion of past bookings or completed bookings
    const today = new Date().toISOString().split("T")[0];
    const isPastDate = booking.appointmentDate < today;
    const isCompleted = booking.status === 'completed';
    
    if (isPastDate || isCompleted) {
      // For past/completed bookings, disable email sending to avoid confusing customers
      if (input.sendEmail) {
        console.log(`Email sending disabled for past/completed booking ${input.id}`);
      }
      input.sendEmail = false;
    }

    const wasAlreadyCancelled = booking.status === 'cancelled';
    const updatedBooking = { ...booking, status: "cancelled" as const };
    await kv.setItem(input.id, updatedBooking);

    if (input.sendEmail && !wasAlreadyCancelled && booking.customerEmail) {
      try {
        const formattedDate = formatDateGerman(booking.appointmentDate);
        const homepageUrl = generateUrl();
        const html = await renderBookingCancelledHTML({ 
          name: booking.customerName, 
          date: booking.appointmentDate, 
          time: booking.appointmentTime,
          treatments: booking.treatments
        });
        
        const treatmentsText = booking.treatments.map(t => `- ${t.name} (${t.duration} Min, ${(t.price / 100).toFixed(2)} €)`).join('\n');
        const totalDuration = booking.treatments.reduce((sum, t) => sum + t.duration, 0);
        const totalPrice = booking.treatments.reduce((sum, t) => sum + (t.price / 100), 0);
        
        await sendEmail({
          to: booking.customerEmail,
          subject: "Dein Termin wurde abgesagt",
          text: `Hallo ${booking.customerName},\n\nleider wurde dein Termin am ${formattedDate} um ${booking.appointmentTime} abgesagt.\n\nBehandlungen:\n${treatmentsText}\n\nGesamt: ${totalDuration} Min, ${totalPrice.toFixed(2)} €\n\nBitte buche einen neuen Termin.\n\nRechtliche Informationen: ${generateUrl('/legal')}\nZur Website: ${homepageUrl}\n\nLiebe Grüße\nStargirlnails Kiel`,
          html,
          bcc: process.env.ADMIN_EMAIL ? [process.env.ADMIN_EMAIL] : undefined,
        });
      } catch (e) {
        console.error("Email send failed:", e);
      }
    }

    return updatedBooking;
  });

// Admin-only manual booking creation (immediately confirmed)
const createManual = os
  .input(z.object({
    sessionId: z.string(),
    treatments: TreatmentsArraySchema,
    customerName: z.string().min(2, "Name muss mindestens 2 Zeichen lang sein"),
    customerEmail: z.string().email("Ungültige E-Mail-Adresse").optional(),
    customerPhone: z.string().min(5, "Telefonnummer muss mindestens 5 Zeichen lang sein").optional(),
    appointmentDate: z.string(),
    appointmentTime: z.string(),
    notes: z.string().optional(),
  }))
  .handler(async ({ input }) => {
    // Admin authentication
    await assertOwner(input.sessionId);

    // Validate appointment time is on 15-minute grid
    const appointmentMinutes = parseTime(input.appointmentTime);
    if (appointmentMinutes % 15 !== 0) {
      throw new Error("Termine müssen auf 15-Minuten-Raster ausgerichtet sein (z.B. 09:00, 09:15, 09:30, 09:45).");
    }

    // Validate that the booking is not in the past
    const today = new Date().toISOString().split("T")[0];
    if (input.appointmentDate < today) {
      throw new Error("Buchungen für vergangene Termine sind nicht möglich.");
    }

    // For today's bookings, check if the time is not in the past
    if (input.appointmentDate === today) {
      const now = new Date();
      const currentTime = `${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}`;
      if (input.appointmentTime <= currentTime) {
        throw new Error("Buchungen für vergangene Uhrzeiten sind nicht möglich.");
      }
    }

    // Validate all treatments exist and snapshot them from KV
    const snapshottedTreatments = [] as Array<{id: string; name: string; duration: number; price: number}>;
    for (const inputTreatment of input.treatments) {
      const treatment = await treatmentsKV.getItem(inputTreatment.id);
      if (!treatment) {
        throw new Error(`Behandlung "${inputTreatment.name}" nicht gefunden.`);
      }
      // Verify snapshot data matches current treatment data
      if (treatment.name !== inputTreatment.name || treatment.duration !== inputTreatment.duration || treatment.price !== inputTreatment.price) {
        throw new Error(`Behandlungsdaten für "${inputTreatment.name}" stimmen nicht überein. Bitte lade die Seite neu.`);
      }
      snapshottedTreatments.push({ id: treatment.id, name: treatment.name, duration: treatment.duration, price: treatment.price });
    }
    const totalDuration = snapshottedTreatments.reduce((sum, t) => sum + t.duration, 0);

    // Validate booking time against recurring rules
    await validateBookingAgainstRules(
      input.appointmentDate,
      input.appointmentTime,
      totalDuration
    );

    // Check for booking conflicts
    await checkBookingConflicts(
      input.appointmentDate,
      input.appointmentTime,
      totalDuration
    );

    const id = randomUUID();
    const booking = {
      id,
      treatments: snapshottedTreatments,
      customerName: input.customerName,
      customerEmail: input.customerEmail,
      customerPhone: input.customerPhone,
      appointmentDate: input.appointmentDate,
      appointmentTime: input.appointmentTime,
      notes: input.notes,
      status: "confirmed" as const,
      createdAt: new Date().toISOString()
    } as Booking;

    // Save the booking
    await kv.setItem(id, booking);

    // Create booking access token for status viewing and cancellation (always create token)
    const bookingAccessToken = await queryClient.cancellation.createToken({ bookingId: id });

    // Send confirmation email if email is provided
    if (input.customerEmail) {
      void (async () => {
        try {
          const bookingUrl = generateUrl(`/booking/${bookingAccessToken.token}`);
          const formattedDate = formatDateGerman(input.appointmentDate);
          const homepageUrl = generateUrl();

          const html = await renderBookingConfirmedHTML({
            name: input.customerName,
            date: input.appointmentDate,
            time: input.appointmentTime,
            cancellationUrl: bookingUrl,
            reviewUrl: generateUrl(`/review/${bookingAccessToken.token}`),
            treatments: input.treatments
          });

          const treatmentsText = input.treatments.map(t => `- ${t.name} (${t.duration} Min, ${(t.price / 100).toFixed(2)} €)`).join('\n');
          const totalPrice = input.treatments.reduce((sum, t) => sum + (t.price / 100), 0);

        await sendEmailWithAGBAndCalendar({
          to: input.customerEmail!,
          subject: "Dein Termin wurde bestätigt - AGB im Anhang",
          text: `Hallo ${input.customerName},\n\nwir haben deinen Termin am ${formattedDate} um ${input.appointmentTime} bestätigt.\n\nBehandlungen:\n${treatmentsText}\n\nGesamt: ${totalDuration} Min, ${totalPrice.toFixed(2)} €\n\nWichtiger Hinweis: Die Allgemeinen Geschäftsbedingungen (AGB) findest du im Anhang dieser E-Mail. Bitte lies sie vor deinem Termin durch.\n\nTermin-Status ansehen und verwalten: ${bookingUrl}\nFalls du den Termin stornieren möchtest, kannst du das über den obigen Link tun.\n\nRechtliche Informationen: ${generateUrl('/legal')}\nZur Website: ${homepageUrl}\n\nBis bald!\nStargirlnails Kiel`,
          html,
        }, {
            date: input.appointmentDate,
            time: input.appointmentTime,
            customerName: input.customerName,
            customerEmail: input.customerEmail,
            treatments: input.treatments
          });
        } catch (e) {
          console.error("Email send failed for manual booking:", e);
        }
      })();
    }

    // Optionally return the token in the RPC response for UI to copy/share (admin usage only)
    return {
      ...booking,
      bookingAccessToken: bookingAccessToken.token
    };
  });

const list = os.handler(async () => {
  return kv.getAllItems();
});

const get = os.input(z.string()).handler(async ({ input }) => {
  return kv.getItem(input);
});

const getByDate = os
  .input(z.string()) // YYYY-MM-DD format
  .handler(async ({ input }) => {
    const allBookings = await kv.getAllItems();
    return allBookings.filter(booking => booking.appointmentDate === input);
  });

const live = {
  list: os.handler(async function* ({ signal }) {
    yield call(list, {}, { signal });
    for await (const _ of kv.subscribe()) {
      yield call(list, {}, { signal });
    }
  }),
  
  byDate: os
    .input(z.string())
    .handler(async function* ({ input, signal }) {
      yield call(getByDate, input, { signal });
      for await (const _ of kv.subscribe()) {
        yield call(getByDate, input, { signal });
      }
    }),
};

export const router = {
  create,
  createManual,
  updateStatus,
  remove,
  list,
  get,
  getByDate,
  live,
  // Admin proposes a reschedule for a confirmed booking
  proposeReschedule: os
    .input(z.object({
      sessionId: z.string(),
      bookingId: z.string(),
      proposedDate: z.string(),
      proposedTime: z.string(),
    }))
    .handler(async ({ input }) => {
      await assertOwner(input.sessionId);
      const booking = await kv.getItem(input.bookingId);
      if (!booking) throw new Error("Booking not found");
      if (booking.status !== "confirmed") throw new Error("Nur bestätigte Termine können umgebucht werden.");

      // Calculate total duration from treatments array
      const totalDuration = booking.treatments && booking.treatments.length > 0
        ? booking.treatments.reduce((sum, t) => sum + t.duration, 0)
        : (booking.bookedDurationMinutes || 60);

      // Validate grid and not in past
      const appointmentMinutes = parseTime(input.proposedTime);
      if (appointmentMinutes % 15 !== 0) {
        throw new Error("Termine müssen auf 15-Minuten-Raster ausgerichtet sein (z.B. 09:00, 09:15, 09:30, 09:45).");
      }
      const today = new Date().toISOString().split("T")[0];
      if (input.proposedDate < today) {
        throw new Error("Buchungen für vergangene Termine sind nicht möglich.");
      }
      if (input.proposedDate === today) {
        const now = new Date();
        const currentTime = `${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}`;
        if (input.proposedTime <= currentTime) {
          throw new Error("Buchungen für vergangene Uhrzeiten sind nicht möglich.");
        }
      }

      await validateBookingAgainstRules(input.proposedDate, input.proposedTime, totalDuration);
      await checkBookingConflicts(input.proposedDate, input.proposedTime, totalDuration, booking.id);

      // Invalidate and create new reschedule token via cancellation router
      const res = await queryClient.cancellation.createRescheduleToken({
        bookingId: booking.id,
        proposedDate: input.proposedDate,
        proposedTime: input.proposedTime,
      });
      const acceptUrl = generateUrl(`/booking/${res.token}?action=accept`);
      const declineUrl = generateUrl(`/booking/${res.token}?action=decline`);

      // Send proposal email to customer
      if (booking.customerEmail) {
        const treatmentName = booking.treatments && booking.treatments.length > 0
          ? booking.treatments.map(t => t.name).join(', ')
          : "Behandlung";
        const html = await renderBookingRescheduleProposalHTML({
          name: booking.customerName,
          originalDate: booking.appointmentDate,
          originalTime: booking.appointmentTime,
          proposedDate: input.proposedDate,
          proposedTime: input.proposedTime,
          treatmentName: treatmentName,
          acceptUrl,
          declineUrl,
          expiresAt: res.expiresAt,
        });
        await sendEmail({
          to: booking.customerEmail,
          subject: "Vorschlag zur Terminänderung",
          text: `Hallo ${booking.customerName}, wir schlagen vor, deinen Termin von ${formatDateGerman(booking.appointmentDate)} ${booking.appointmentTime} auf ${formatDateGerman(input.proposedDate)} ${input.proposedTime} zu verschieben. Akzeptieren: ${acceptUrl} | Ablehnen: ${declineUrl}`,
          html,
          bcc: process.env.ADMIN_EMAIL ? [process.env.ADMIN_EMAIL] : undefined,
        }).catch(() => {});
      }

      return { success: true, token: res.token };
    }),

  // Customer accepts reschedule via token
  acceptReschedule: os
    .input(z.object({ token: z.string() }))
    .handler(async ({ input }) => {
      const proposal = await queryClient.cancellation.getRescheduleProposal({ token: input.token });
      const booking = await kv.getItem(proposal.booking.id);
      if (!booking) throw new Error("Booking not found");
      if (booking.status !== "confirmed") throw new Error("Buchung ist nicht mehr in bestätigtem Zustand.");

      const duration = booking.treatments && booking.treatments.length > 0
        ? booking.treatments.reduce((sum, t) => sum + t.duration, 0)
        : (booking.bookedDurationMinutes || 60);

      // Re-validate slot to ensure still available
      await validateBookingAgainstRules(proposal.proposed.date, proposal.proposed.time, duration);
      await checkBookingConflicts(proposal.proposed.date, proposal.proposed.time, duration, booking.id);

      const updated = { ...booking, appointmentDate: proposal.proposed.date, appointmentTime: proposal.proposed.time } as typeof booking;
      await kv.setItem(updated.id, updated);

      // Remove token
      await queryClient.cancellation.removeRescheduleToken({ token: input.token });

      // Send confirmation to customer (no BCC to avoid duplicate admin emails)
      if (updated.customerEmail) {
        const bookingAccessToken = await queryClient.cancellation.createToken({ bookingId: updated.id });
        const html = await renderBookingConfirmedHTML({
          name: updated.customerName,
          date: updated.appointmentDate,
          time: updated.appointmentTime,
          cancellationUrl: generateUrl(`/booking/${bookingAccessToken.token}`),
          reviewUrl: generateUrl(`/review/${bookingAccessToken.token}`),
          treatments: updated.treatments,
        });
        
        const treatmentsText = updated.treatments.map(t => `- ${t.name} (${t.duration} Min, ${(t.price / 100).toFixed(2)} €)`).join('\n');
        const totalPrice = updated.treatments.reduce((sum, t) => sum + (t.price / 100), 0);
        
        await sendEmailWithAGBAndCalendar({
          to: updated.customerEmail,
          subject: "Terminänderung bestätigt",
          text: `Hallo ${updated.customerName}, dein neuer Termin ist am ${formatDateGerman(updated.appointmentDate)} um ${updated.appointmentTime}.\n\nBehandlungen:\n${treatmentsText}\n\nGesamt: ${duration} Min, ${totalPrice.toFixed(2)} €`,
          html,
        }, {
          date: updated.appointmentDate,
          time: updated.appointmentTime,
          customerName: updated.customerName,
          customerEmail: updated.customerEmail,
          treatments: updated.treatments,
        }).catch(() => {});
      }

      if (process.env.ADMIN_EMAIL) {
        const treatmentName = updated.treatments && updated.treatments.length > 0
          ? updated.treatments.map(t => t.name).join(', ')
          : "Behandlung";
        const adminHtml = await renderAdminRescheduleAcceptedHTML({
          customerName: updated.customerName,
          originalDate: proposal.original.date,
          originalTime: proposal.original.time,
          newDate: updated.appointmentDate,
          newTime: updated.appointmentTime,
          treatmentName: treatmentName,
        });
        await sendEmail({
          to: process.env.ADMIN_EMAIL,
          subject: `Reschedule akzeptiert - ${updated.customerName}`,
          text: `Reschedule akzeptiert: ${updated.customerName} von ${formatDateGerman(proposal.original.date)} ${proposal.original.time} auf ${formatDateGerman(updated.appointmentDate)} ${updated.appointmentTime}.`,
          html: adminHtml,
        }).catch(() => {});
      }

      return { success: true, message: `Termin auf ${formatDateGerman(updated.appointmentDate)} um ${updated.appointmentTime} aktualisiert.` };
    }),

  // Customer declines reschedule via token
  declineReschedule: os
    .input(z.object({ token: z.string() }))
    .handler(async ({ input }) => {
      const proposal = await queryClient.cancellation.getRescheduleProposal({ token: input.token });
      const booking = await kv.getItem(proposal.booking.id);
      if (!booking) throw new Error("Booking not found");

      // Remove token
      await queryClient.cancellation.removeRescheduleToken({ token: input.token });

      // Notify customer that original stays
      if (booking.customerEmail) {
        const bookingAccessToken = await queryClient.cancellation.createToken({ bookingId: booking.id });
        
        const treatmentsText = booking.treatments.map(t => `- ${t.name} (${t.duration} Min, ${(t.price / 100).toFixed(2)} €)`).join('\n');
        const totalDuration = booking.treatments.reduce((sum, t) => sum + t.duration, 0);
        const totalPrice = booking.treatments.reduce((sum, t) => sum + (t.price / 100), 0);
        
        await sendEmail({
          to: booking.customerEmail,
          subject: "Terminänderung abgelehnt",
          text: `Du hast den Vorschlag zur Terminänderung abgelehnt. Dein ursprünglicher Termin am ${formatDateGerman(booking.appointmentDate)} um ${booking.appointmentTime} bleibt bestehen.\n\nBehandlungen:\n${treatmentsText}\n\nGesamt: ${totalDuration} Min, ${totalPrice.toFixed(2)} €`,
          html: await renderBookingConfirmedHTML({ 
            name: booking.customerName, 
            date: booking.appointmentDate, 
            time: booking.appointmentTime, 
            cancellationUrl: generateUrl(`/booking/${bookingAccessToken.token}`), 
            reviewUrl: generateUrl(`/review/${bookingAccessToken.token}`),
            treatments: booking.treatments
          }),
        }).catch(() => {});
      }

      // Notify admin
      if (process.env.ADMIN_EMAIL) {
        const treatmentName = booking.treatments && booking.treatments.length > 0
          ? booking.treatments.map(t => t.name).join(', ')
          : "Behandlung";
        const html = await renderAdminRescheduleDeclinedHTML({
          customerName: booking.customerName,
          originalDate: proposal.original.date,
          originalTime: proposal.original.time,
          proposedDate: proposal.proposed.date!,
          proposedTime: proposal.proposed.time!,
          treatmentName: treatmentName,
          customerEmail: booking.customerEmail,
          customerPhone: booking.customerPhone,
        });
        await sendEmail({
          to: process.env.ADMIN_EMAIL,
          subject: `Reschedule abgelehnt - ${booking.customerName}`,
          text: `Abgelehnt: ${booking.customerName}. Ursprünglich: ${formatDateGerman(proposal.original.date)} ${proposal.original.time}. Vorschlag: ${formatDateGerman(proposal.proposed.date!)} ${proposal.proposed.time!}.`,
          html,
        }).catch(() => {});
      }

      return { success: true, message: "Du hast den Vorschlag abgelehnt. Dein ursprünglicher Termin bleibt bestehen." };
    }),

  // CalDAV Token für Admin generieren
  generateCalDAVToken: os
    .input(z.object({ sessionId: z.string() }))
    .handler(async ({ input }) => {
      await assertOwner(input.sessionId);
      
      // Generiere einen sicheren Token für CalDAV-Zugriff
      const token = randomUUID();
      
      // Hole Session-Daten für Token-Erstellung
      const session = await sessionsKV.getItem(input.sessionId);
      if (!session) throw new Error("Session nicht gefunden");
      
      // Speichere Token mit Ablaufzeit (24 Stunden)
      const tokenData = {
        id: token,
        sessionId: input.sessionId,
        userId: session.userId, // Benötigt für Session-Typ
        expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), // 24 Stunden
        createdAt: new Date().toISOString(),
      };
      
      // Verwende den sessionsKV Store für Token-Speicherung
      await sessionsKV.setItem(token, tokenData);
      
      const domain = process.env.DOMAIN || 'localhost:3000';
      const protocol = domain.includes('localhost') ? 'http' : 'https';
      const caldavUrl = `${protocol}://${domain}/caldav/calendar/events.ics?token=${token}`;
      
      return {
        token,
        caldavUrl,
        expiresAt: tokenData.expiresAt,
        instructions: {
          title: "CalDAV-Kalender abonnieren",
          steps: [
            "Kopiere die CalDAV-URL unten",
            "Füge sie in deiner Kalender-App als Abonnement hinzu:",
            "- Outlook: Datei → Konto hinzufügen → Internetkalender",
            "- Google Calendar: Andere Kalender hinzufügen → Von URL",
            "- Apple Calendar: Abonnement → Neue Abonnements",
            "- Thunderbird: Kalender hinzufügen → Im Netzwerk",
            "Der Kalender wird automatisch aktualisiert"
          ],
          note: "Dieser Token ist 24 Stunden gültig. Bei Bedarf kannst du einen neuen Token generieren."
        }
      };
    }),

  // Admin sendet Nachricht an Kunden
  sendCustomerMessage: os
    .input(z.object({
      sessionId: z.string(),
      bookingId: z.string(),
      message: z.string().min(1, "Nachricht darf nicht leer sein").max(5000, "Nachricht ist zu lang (max. 5000 Zeichen)"),
    }))
    .handler(async ({ input }) => {
      await assertOwner(input.sessionId);
      
      const booking = await kv.getItem(input.bookingId);
      if (!booking) throw new Error("Buchung nicht gefunden");
      
      // Check if booking has customer email
      if (!booking.customerEmail) {
        throw new Error("Diese Buchung hat keine E-Mail-Adresse. Bitte kontaktiere den Kunden telefonisch.");
      }
      
      // Check if booking is in the future
      const today = new Date().toISOString().split("T")[0];
      const bookingDate = booking.appointmentDate;
      if (bookingDate < today) {
        throw new Error("Nachrichten können nur für zukünftige Termine gesendet werden.");
      }
      
      // Get treatment name for context
      const treatmentName = booking.treatments && booking.treatments.length > 0
        ? booking.treatments.map(t => t.name).join(', ')
        : "Behandlung";
      
      // Prepare email with Reply-To header
      const ownerName = process.env.OWNER_NAME || "Stargirlnails Kiel";
      const emailFrom = process.env.EMAIL_FROM || "Stargirlnails <no-reply@stargirlnails.de>";
      const replyToEmail = process.env.ADMIN_EMAIL;
      
      const formattedDate = formatDateGerman(bookingDate);
      
      const html = await renderCustomerMessageHTML({
        customerName: booking.customerName,
        message: input.message,
        appointmentDate: bookingDate,
        appointmentTime: booking.appointmentTime,
        treatmentName: treatmentName,
      });
      
      const textContent = `Hallo ${booking.customerName},\n\nZu deinem Termin:\nBehandlung: ${treatmentName}\nDatum: ${formattedDate}\nUhrzeit: ${booking.appointmentTime}\n\nNachricht von ${ownerName}:\n${input.message}\n\nBei Fragen oder Anliegen kannst du einfach auf diese E-Mail antworten – wir helfen dir gerne weiter!\n\nLiebe Grüße,\n${ownerName}`;
      
      // Send email with BCC to admin for monitoring
      // Note: Not using explicit 'from' or 'replyTo' to match behavior of other system emails
      console.log(`Sending customer message to ${booking.customerEmail} for booking ${input.bookingId}`);
      console.log(`Email config: from=${emailFrom}, replyTo=${replyToEmail}, bcc=${replyToEmail}`);
      const emailResult = await sendEmail({
        to: booking.customerEmail,
        subject: `Nachricht zu deinem Termin am ${formattedDate}`,
        text: textContent,
        html: html,
        bcc: replyToEmail ? [replyToEmail] : undefined,
      });
      
      if (!emailResult.success) {
        console.error(`Failed to send customer message to ${booking.customerEmail}`);
        throw new Error("E-Mail konnte nicht versendet werden. Bitte überprüfe die E-Mail-Konfiguration oder versuche es später erneut.");
      }
      
      console.log(`Successfully sent customer message to ${booking.customerEmail}`);
      return { 
        success: true, 
        message: `Nachricht wurde erfolgreich an ${booking.customerEmail} gesendet.`
      };
    }),
};