import { os } from "@orpc/server";
import { z } from "zod";
import { createKV } from "../lib/create-kv.js";
import { createKV as createAvailabilityKV } from "../lib/create-kv.js";
import { randomUUID } from "crypto";
// Schema for booking access token (used for both status viewing and cancellation)
const BookingAccessTokenSchema = z.object({
    id: z.string(),
    bookingId: z.string(),
    token: z.string(),
    expiresAt: z.string(),
    createdAt: z.string(),
    purpose: z.enum(["booking_access", "reschedule_proposal"]), // Extended for reschedule proposals
    // Optional metadata for reschedule proposals
    proposedDate: z.string().optional(),
    proposedTime: z.string().optional(),
    originalDate: z.string().optional(),
    originalTime: z.string().optional(),
});
const cancellationKV = createKV("cancellation_tokens");
const bookingsKV = createKV("bookings");
const availabilityKV = createAvailabilityKV("availability");
// Helper function to convert date from yyyy-mm-dd to dd.mm.yyyy
function formatDateGerman(dateString) {
    const [year, month, day] = dateString.split('-');
    return `${day}.${month}.${year}`;
}
// Helper to invalidate all reschedule tokens for a specific booking
async function invalidateRescheduleTokensForBooking(bookingId) {
    const tokens = await cancellationKV.getAllItems();
    const related = tokens.filter(t => t.bookingId === bookingId && t.purpose === "reschedule_proposal");
    for (const tok of related) {
        await cancellationKV.removeItem(tok.id);
    }
}
// Create cancellation token for a booking
const createToken = os
    .input(z.object({ bookingId: z.string() }))
    .handler(async ({ input }) => {
    const booking = await bookingsKV.getItem(input.bookingId);
    if (!booking) {
        throw new Error("Booking not found");
    }
    if (booking.status === "cancelled") {
        throw new Error("Booking is already cancelled");
    }
    // Create token that expires in 30 days
    const expiresAt = new Date();
    expiresAt.setDate(expiresAt.getDate() + 30);
    const token = randomUUID();
    const cancellationToken = {
        id: randomUUID(),
        bookingId: input.bookingId,
        token,
        expiresAt: expiresAt.toISOString(),
        createdAt: new Date().toISOString(),
        purpose: "booking_access",
    };
    await cancellationKV.setItem(cancellationToken.id, cancellationToken);
    return { token, expiresAt: expiresAt.toISOString() };
});
// Get booking details by token
const getBookingByToken = os
    .input(z.object({ token: z.string() }))
    .handler(async ({ input }) => {
    const tokens = await cancellationKV.getAllItems();
    const validToken = tokens.find(t => t.token === input.token &&
        new Date(t.expiresAt) > new Date() &&
        t.purpose === 'booking_access');
    if (!validToken) {
        throw new Error("Invalid or expired cancellation token");
    }
    const booking = await bookingsKV.getItem(validToken.bookingId);
    if (!booking) {
        throw new Error("Booking not found");
    }
    // Get treatment details
    const treatmentsKV = createKV("treatments");
    const treatment = await treatmentsKV.getItem(booking.treatmentId);
    // Calculate if cancellation is still possible
    const minStornoTimespan = parseInt(process.env.MIN_STORNO_TIMESPAN || "24");
    const appointmentDateTime = new Date(`${booking.appointmentDate}T${booking.appointmentTime}:00`);
    const now = new Date();
    const timeDifferenceHours = (appointmentDateTime.getTime() - now.getTime()) / (1000 * 60 * 60);
    const canCancel = timeDifferenceHours >= minStornoTimespan &&
        booking.status !== "cancelled" &&
        booking.status !== "completed";
    return {
        id: booking.id,
        customerName: booking.customerName,
        customerEmail: booking.customerEmail,
        customerPhone: booking.customerPhone,
        appointmentDate: booking.appointmentDate,
        appointmentTime: booking.appointmentTime,
        treatmentId: booking.treatmentId,
        treatmentName: treatment?.name || "Unbekannte Behandlung",
        treatmentDuration: treatment?.duration || 60,
        treatmentPrice: treatment?.price || 0,
        status: booking.status,
        notes: booking.notes,
        formattedDate: formatDateGerman(booking.appointmentDate),
        createdAt: booking.createdAt,
        canCancel,
        hoursUntilAppointment: Math.max(0, Math.round(timeDifferenceHours)),
    };
});
// Cancel booking by token
const cancelByToken = os
    .input(z.object({ token: z.string() }))
    .handler(async ({ input }) => {
    const tokens = await cancellationKV.getAllItems();
    const validToken = tokens.find(t => t.token === input.token &&
        new Date(t.expiresAt) > new Date());
    if (!validToken) {
        throw new Error("Invalid or expired cancellation token");
    }
    const booking = await bookingsKV.getItem(validToken.bookingId);
    if (!booking) {
        throw new Error("Booking not found");
    }
    // Check if booking is already cancelled
    if (booking.status === "cancelled") {
        throw new Error("Booking is already cancelled");
    }
    // Check minimum cancellation timespan from environment variable
    const minStornoTimespan = parseInt(process.env.MIN_STORNO_TIMESPAN || "24"); // Default: 24 hours
    const appointmentDateTime = new Date(`${booking.appointmentDate}T${booking.appointmentTime}:00`);
    const now = new Date();
    const timeDifferenceHours = (appointmentDateTime.getTime() - now.getTime()) / (1000 * 60 * 60);
    if (timeDifferenceHours < minStornoTimespan) {
        throw new Error(`Stornierung ist nur bis ${minStornoTimespan} Stunden vor dem Termin möglich. Der Termin liegt nur noch ${Math.round(timeDifferenceHours)} Stunden in der Zukunft.`);
    }
    // Check if booking is in the past (additional safety check)
    const today = new Date().toISOString().split("T")[0];
    if (booking.appointmentDate < today) {
        throw new Error("Cannot cancel past bookings");
    }
    // For today's bookings, check if the time is not in the past
    if (booking.appointmentDate === today) {
        const currentTime = `${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}`;
        if (booking.appointmentTime <= currentTime) {
            throw new Error("Cannot cancel bookings that have already started");
        }
    }
    // Update booking status
    const updatedBooking = { ...booking, status: "cancelled" };
    await bookingsKV.setItem(booking.id, updatedBooking);
    // Free the slot if it exists
    if (booking.slotId) {
        const slot = await availabilityKV.getItem(booking.slotId);
        if (slot) {
            const updatedSlot = {
                ...slot,
                status: "free",
                reservedByBookingId: undefined,
            };
            await availabilityKV.setItem(slot.id, updatedSlot);
        }
    }
    // Invalidate the token
    await cancellationKV.removeItem(validToken.id);
    return {
        success: true,
        message: "Booking cancelled successfully",
        formattedDate: formatDateGerman(booking.appointmentDate),
    };
});
export const router = {
    createToken,
    getBookingByToken,
    cancelByToken,
    // Create a reschedule proposal token (48h expiry)
    createRescheduleToken: os
        .input(z.object({ bookingId: z.string(), proposedDate: z.string(), proposedTime: z.string() }))
        .handler(async ({ input }) => {
        const booking = await bookingsKV.getItem(input.bookingId);
        if (!booking) {
            throw new Error("Booking not found");
        }
        if (booking.status === "cancelled" || booking.status === "completed") {
            throw new Error("Reschedule not allowed for this booking");
        }
        // Invalidate existing reschedule proposals for this booking
        await invalidateRescheduleTokensForBooking(input.bookingId);
        // Create token that expires in 48 hours
        const expiresAt = new Date();
        expiresAt.setHours(expiresAt.getHours() + 48);
        const token = randomUUID();
        const rescheduleToken = {
            id: randomUUID(),
            bookingId: input.bookingId,
            token,
            expiresAt: expiresAt.toISOString(),
            createdAt: new Date().toISOString(),
            purpose: "reschedule_proposal",
            proposedDate: input.proposedDate,
            proposedTime: input.proposedTime,
            originalDate: booking.appointmentDate,
            originalTime: booking.appointmentTime,
        };
        await cancellationKV.setItem(rescheduleToken.id, rescheduleToken);
        return { token, expiresAt: expiresAt.toISOString() };
    }),
    // Get reschedule proposal details by token
    getRescheduleProposal: os
        .input(z.object({ token: z.string() }))
        .handler(async ({ input }) => {
        const tokens = await cancellationKV.getAllItems();
        const proposal = tokens.find(t => t.token === input.token && t.purpose === "reschedule_proposal");
        if (!proposal) {
            throw new Error("Ungültiger Reschedule-Token");
        }
        const booking = await bookingsKV.getItem(proposal.bookingId);
        if (!booking) {
            throw new Error("Booking not found");
        }
        const treatmentsKV = createKV("treatments");
        const treatment = await treatmentsKV.getItem(booking.treatmentId);
        const now = new Date();
        const isExpired = new Date(proposal.expiresAt) <= now;
        const hoursUntilExpiry = isExpired ? 0 : Math.max(0, Math.round((new Date(proposal.expiresAt).getTime() - now.getTime()) / (1000 * 60 * 60)));
        return {
            booking: {
                id: booking.id,
                customerName: booking.customerName,
                customerEmail: booking.customerEmail,
                customerPhone: booking.customerPhone,
                status: booking.status,
                treatmentId: booking.treatmentId,
                treatmentName: treatment?.name || "Unbekannte Behandlung",
            },
            original: {
                date: proposal.originalDate || booking.appointmentDate,
                time: proposal.originalTime || booking.appointmentTime,
            },
            proposed: {
                date: proposal.proposedDate,
                time: proposal.proposedTime,
            },
            expiresAt: proposal.expiresAt,
            hoursUntilExpiry,
            isExpired,
            canRespond: booking.status === "confirmed" && !isExpired,
        };
    }),
    // Helper endpoint to remove a reschedule token by value (used after accept/decline)
    removeRescheduleToken: os
        .input(z.object({ token: z.string() }))
        .handler(async ({ input }) => {
        const tokens = await cancellationKV.getAllItems();
        const proposal = tokens.find(t => t.token === input.token && t.purpose === "reschedule_proposal");
        if (proposal) {
            await cancellationKV.removeItem(proposal.id);
        }
        return { success: true };
    }),
    // Clean up expired reschedule proposals and notify admin
    sweepExpiredRescheduleProposals: os
        .handler(async () => {
        const tokens = await cancellationKV.getAllItems();
        const now = new Date();
        const expiredProposals = tokens.filter(t => t.purpose === "reschedule_proposal" &&
            new Date(t.expiresAt) <= now);
        if (expiredProposals.length === 0) {
            return { success: true, expiredCount: 0 };
        }
        // Get booking details for each expired proposal
        const expiredDetails = [];
        for (const proposal of expiredProposals) {
            const booking = await bookingsKV.getItem(proposal.bookingId);
            if (booking) {
                const treatmentsKV = createKV("treatments");
                const treatment = await treatmentsKV.getItem(booking.treatmentId);
                expiredDetails.push({
                    customerName: booking.customerName,
                    originalDate: proposal.originalDate || booking.appointmentDate,
                    originalTime: proposal.originalTime || booking.appointmentTime,
                    proposedDate: proposal.proposedDate,
                    proposedTime: proposal.proposedTime,
                    treatmentName: treatment?.name || "Unbekannte Behandlung",
                    customerEmail: booking.customerEmail,
                    customerPhone: booking.customerPhone,
                    expiredAt: proposal.expiresAt,
                });
            }
            // Remove the expired token
            await cancellationKV.removeItem(proposal.id);
        }
        // Notify admin if there are expired proposals
        if (expiredDetails.length > 0 && process.env.ADMIN_EMAIL) {
            try {
                const { renderAdminRescheduleExpiredHTML } = await import("../lib/email-templates.js");
                const { sendEmail } = await import("../lib/email.js");
                const html = await renderAdminRescheduleExpiredHTML({
                    expiredProposals: expiredDetails,
                });
                await sendEmail({
                    to: process.env.ADMIN_EMAIL,
                    subject: `${expiredDetails.length} abgelaufene Terminänderungsvorschläge`,
                    text: `Es sind ${expiredDetails.length} Terminänderungsvorschläge abgelaufen. Details in der HTML-Version.`,
                    html,
                });
            }
            catch (error) {
                console.error("Failed to send admin notification for expired proposals:", error);
            }
        }
        return { success: true, expiredCount: expiredDetails.length };
    }),
};
