import { mutation } from "../_generated/server";
import { v } from "convex/values";
import { sanitizeString, sanitizeOptionalString } from "../helpers/sanitize";

/**
 * Create a new booking
 * Phase 2: Bookings
 */
export const create = mutation({
  args: {
    prismaId: v.optional(v.string()),
    userId: v.string(),
    organizationId: v.optional(v.string()),
    customerName: v.string(),
    customerPhone: v.string(),
    customerEmail: v.optional(v.string()),
    service: v.string(),
    appointmentDate: v.number(),
    appointmentTime: v.string(),
    duration: v.optional(v.number()),
    staffMember: v.optional(v.string()),
    totalAmount: v.number(),
    notes: v.optional(v.string()),
    source: v.optional(v.string()),
    sectorMetadata: v.optional(v.any()),
    itemType: v.optional(v.string()),
    depositAmount: v.optional(v.float64()),
    conversationId: v.optional(v.string()),
  },
  handler: async (ctx, args) => {
    const now = Date.now();
    const bookingId = await ctx.db.insert("bookings", {
      ...(args.prismaId ? { prismaId: args.prismaId } : {}),
      userId: args.userId,
      organizationId: args.organizationId,
      customerName: sanitizeString(args.customerName, 200),
      customerPhone: sanitizeString(args.customerPhone, 30),
      customerEmail: args.customerEmail
        ? sanitizeString(args.customerEmail, 254)
        : args.customerEmail,
      service: sanitizeString(args.service, 500),
      appointmentDate: args.appointmentDate,
      appointmentTime: args.appointmentTime,
      duration: args.duration ?? 30,
      staffMember: sanitizeOptionalString(args.staffMember, 200),
      totalAmount: args.totalAmount,
      notes: sanitizeOptionalString(args.notes, 5000),
      source:
        (args.source as
          | "whatsapp"
          | "manual"
          | "demo_booking"
          | "widget"
          | "phone_call"
          | "walk_in") ?? "manual",
      sectorMetadata: args.sectorMetadata,
      itemType: args.itemType,
      ...(args.depositAmount !== undefined ? { depositAmount: args.depositAmount } : {}),
      ...(args.conversationId ? { conversationId: args.conversationId } : {}),
      status: "pending",
      depositPaid: false,
      depositReminderCount: 0,
      createdAt: now,
      updatedAt: now,
    });
    return bookingId;
  },
});

/**
 * Update booking status (confirm, cancel, complete, no_show)
 * Phase 2: Bookings
 */
export const updateStatus = mutation({
  args: {
    id: v.id("bookings"),
    status: v.string(),
  },
  handler: async (ctx, args) => {
    const existing = await ctx.db.get(args.id);
    if (!existing) {
      throw new Error(`Booking ${args.id} not found`);
    }
    await ctx.db.patch(args.id, {
      status: args.status as
        | "pending"
        | "confirmed"
        | "cancelled"
        | "completed"
        | "no_show",
      updatedAt: Date.now(),
    });
    return args.id;
  },
});

/**
 * Add a note to a booking
 * Phase 2: Bookings
 */
export const addNote = mutation({
  args: {
    bookingId: v.string(),
    authorId: v.string(),
    authorName: v.string(),
    content: v.string(),
  },
  handler: async (ctx, args) => {
    const now = Date.now();
    const noteId = await ctx.db.insert("booking_notes", {
      bookingId: args.bookingId,
      authorId: args.authorId,
      authorName: sanitizeString(args.authorName, 200),
      content: sanitizeString(args.content, 5000),
      createdAt: now,
      updatedAt: now,
    });
    return noteId;
  },
});

/**
 * Cancel a booking with optional reason
 * Phase 2: Bookings
 */
export const cancel = mutation({
  args: {
    id: v.id("bookings"),
    reason: v.optional(v.string()),
    cancelledVia: v.optional(v.string()),
  },
  handler: async (ctx, args) => {
    const existing = await ctx.db.get(args.id);
    if (!existing) {
      throw new Error(`Booking ${args.id} not found`);
    }
    const updates: Record<string, unknown> = {
      status: "cancelled" as const,
      cancelledAt: Date.now(),
      updatedAt: Date.now(),
    };
    if (args.cancelledVia !== undefined) {
      updates.cancelledVia = args.cancelledVia;
    }
    if (args.reason) {
      const sanitizedReason = sanitizeString(args.reason, 2000);
      updates.notes = existing.notes
        ? `${existing.notes}\n[Cancelled]: ${sanitizedReason}`
        : `[Cancelled]: ${sanitizedReason}`;
    }
    await ctx.db.patch(args.id, updates);
    return args.id;
  },
});

/**
 * Reschedule a booking to a new date/time
 * Phase 2: Bookings
 */
export const reschedule = mutation({
  args: {
    id: v.id("bookings"),
    appointmentDate: v.number(),
    appointmentTime: v.string(),
    staffMember: v.optional(v.string()),
  },
  handler: async (ctx, args) => {
    const existing = await ctx.db.get(args.id);
    if (!existing) {
      throw new Error(`Booking ${args.id} not found`);
    }
    const updates: Record<string, unknown> = {
      appointmentDate: args.appointmentDate,
      appointmentTime: args.appointmentTime,
      updatedAt: Date.now(),
    };
    if (args.staffMember !== undefined) {
      updates.staffMember = sanitizeOptionalString(args.staffMember, 200);
    }
    // Clear reminder-sent state so the 24h cron re-fires for the new date
    updates.reminderSentAt = undefined;
    updates.reminderStatus = undefined;
    updates.reminderError = undefined;
    await ctx.db.patch(args.id, updates);
    return args.id;
  },
});

/**
 * Create a new demo booking
 * Group G: Demo Booking Migration
 */
export const createDemoBooking = mutation({
  args: {
    fullName: v.string(),
    email: v.string(),
    telephone: v.string(),
    appointmentDate: v.number(),
    appointmentTime: v.string(),
    duration: v.string(),
    specialRequirements: v.optional(v.string()),
    language: v.string(),
    status: v.optional(v.string()),
    webhookStatus: v.optional(v.string()),
    webhookRetryCount: v.optional(v.number()),
    customerTimezone: v.optional(v.string()),
  },
  handler: async (ctx, args) => {
    const now = Date.now();
    const id = await ctx.db.insert("demo_bookings", {
      fullName: sanitizeString(args.fullName, 200),
      email: sanitizeString(args.email, 254),
      telephone: sanitizeString(args.telephone, 30),
      appointmentDate: args.appointmentDate,
      appointmentTime: args.appointmentTime,
      duration: args.duration,
      specialRequirements: sanitizeOptionalString(
        args.specialRequirements,
        5000,
      ),
      language: args.language,
      status:
        (args.status as
          | "pending"
          | "confirmed"
          | "cancelled"
          | "completed"
          | "no_show") ?? "pending",
      webhookStatus:
        (args.webhookStatus as "pending" | "sent" | "failed") ?? "pending",
      webhookRetryCount: args.webhookRetryCount ?? 0,
      customerTimezone: args.customerTimezone,
      reminderEmailSent: false,
      reminderWhatsAppSent: false,
      createdAt: now,
      updatedAt: now,
    });
    return id;
  },
});

/**
 * Update demo booking with Google Calendar metadata
 * Group G: Demo Booking Migration
 */
export const updateDemoBookingCalendarData = mutation({
  args: {
    id: v.id("demo_bookings"),
    googleCalendarEventId: v.optional(v.string()),
    googleConferenceId: v.optional(v.string()),
    googleJoinUrl: v.optional(v.string()),
    meetingLink: v.optional(v.string()),
  },
  handler: async (ctx, args) => {
    const { id, ...updates } = args;
    const patch: Record<string, unknown> = { updatedAt: Date.now() };
    if (updates.googleCalendarEventId !== undefined)
      patch.googleCalendarEventId = updates.googleCalendarEventId;
    if (updates.googleConferenceId !== undefined)
      patch.googleConferenceId = updates.googleConferenceId;
    if (updates.googleJoinUrl !== undefined)
      patch.googleJoinUrl = updates.googleJoinUrl;
    if (updates.meetingLink !== undefined)
      patch.meetingLink = updates.meetingLink;
    await ctx.db.patch(id, patch);
    return id;
  },
});

/**
 * Confirm a demo booking (set status to "confirmed")
 * Group G: Demo Booking Migration
 */
export const confirmDemoBooking = mutation({
  args: {
    id: v.id("demo_bookings"),
  },
  handler: async (ctx, args) => {
    const existing = await ctx.db.get(args.id);
    if (!existing) {
      throw new Error(`Demo booking ${args.id} not found`);
    }
    await ctx.db.patch(args.id, {
      status: "confirmed" as const,
      updatedAt: Date.now(),
    });
    return args.id;
  },
});

/**
 * Update demo booking status with optional contact/meeting metadata
 * Group G: Demo Booking Migration
 */
export const updateDemoBookingStatus = mutation({
  args: {
    id: v.id("demo_bookings"),
    status: v.string(),
    contactedAt: v.optional(v.number()),
    contactedBy: v.optional(v.string()),
    internalNotes: v.optional(v.string()),
    meetingLink: v.optional(v.string()),
  },
  handler: async (ctx, args) => {
    const existing = await ctx.db.get(args.id);
    if (!existing) {
      throw new Error(`Demo booking ${args.id} not found`);
    }
    const patch: Record<string, unknown> = {
      status: args.status as
        | "pending"
        | "confirmed"
        | "cancelled"
        | "completed"
        | "no_show",
      updatedAt: Date.now(),
    };
    if (args.contactedAt !== undefined) patch.contactedAt = args.contactedAt;
    if (args.contactedBy !== undefined)
      patch.contactedBy = sanitizeOptionalString(args.contactedBy, 200);
    if (args.internalNotes !== undefined)
      patch.internalNotes = sanitizeOptionalString(args.internalNotes, 5000);
    if (args.meetingLink !== undefined) patch.meetingLink = args.meetingLink;
    await ctx.db.patch(args.id, patch);
    return args.id;
  },
});

/**
 * Generic update for a demo booking — patches any provided fields
 * Group G: Demo Booking Migration
 */
export const updateDemoBooking = mutation({
  args: {
    id: v.id("demo_bookings"),
    fullName: v.optional(v.string()),
    email: v.optional(v.string()),
    telephone: v.optional(v.string()),
    appointmentDate: v.optional(v.number()),
    appointmentTime: v.optional(v.string()),
    duration: v.optional(v.string()),
    specialRequirements: v.optional(v.string()),
    language: v.optional(v.string()),
    status: v.optional(v.string()),
    webhookStatus: v.optional(v.string()),
    webhookRetryCount: v.optional(v.number()),
    webhookLastAttempt: v.optional(v.number()),
    webhookResponse: v.optional(v.string()),
    assignedTo: v.optional(v.string()),
    contactedAt: v.optional(v.number()),
    contactedBy: v.optional(v.string()),
    customerTimezone: v.optional(v.string()),
    googleCalendarEventId: v.optional(v.string()),
    googleConferenceId: v.optional(v.string()),
    googleJoinUrl: v.optional(v.string()),
    internalNotes: v.optional(v.string()),
    meetingLink: v.optional(v.string()),
    reminderEmailSent: v.optional(v.boolean()),
    reminderError: v.optional(v.string()),
    reminderSentAt: v.optional(v.number()),
    reminderStatus: v.optional(v.string()),
    reminderWhatsAppSent: v.optional(v.boolean()),
  },
  handler: async (ctx, args) => {
    const { id, ...fields } = args;
    const existing = await ctx.db.get(id);
    if (!existing) {
      throw new Error(`Demo booking ${id} not found`);
    }
    // Text fields that should be sanitized
    const textFieldLimits: Record<string, number> = {
      fullName: 200,
      email: 254,
      telephone: 30,
      specialRequirements: 5000,
      internalNotes: 5000,
      webhookResponse: 5000,
      reminderError: 2000,
      reminderStatus: 100,
    };

    const patch: Record<string, unknown> = { updatedAt: Date.now() };
    for (const [key, value] of Object.entries(fields)) {
      if (value !== undefined) {
        if (typeof value === "string" && key in textFieldLimits) {
          patch[key] = sanitizeString(value, textFieldLimits[key]);
        } else {
          patch[key] = value;
        }
      }
    }
    await ctx.db.patch(id, patch);
    return id;
  },
});

/**
 * Record the outcome of a 24-hour appointment reminder send attempt.
 *
 * Called by lib/services/appointment-reminder.service.ts after each Twilio
 * send. On success sets `reminderSentAt` (so the query filter skips it next
 * run). On failure, records the error + status without `reminderSentAt` so
 * the next cron run can retry.
 */
export const markBookingReminderSent = mutation({
  args: {
    id: v.string(),
    status: v.string(), // 'sent' | 'failed' | 'skipped'
    error: v.optional(v.string()),
    channel: v.optional(v.string()),
  },
  handler: async (ctx, args) => {
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    let doc: any = null;
    try {
      doc = await ctx.db.get(args.id as any);
    } catch {
      /* not a valid Convex id */
    }
    if (!doc) {
      doc = await ctx.db
        .query("bookings")
        .filter((q: any) => q.eq(q.field("prismaId"), args.id))
        .first();
    }
    if (!doc) return null;

    const patch: Record<string, unknown> = {
      reminderStatus: args.status as
        | "pending"
        | "sent"
        | "partial"
        | "failed"
        | "skipped",
      updatedAt: Date.now(),
    };
    if (args.status === "sent") {
      patch.reminderSentAt = Date.now();
      patch.reminderError = undefined;
    }
    if (args.status === "pending") {
      // Explicit reset so the next cron run picks the booking up again
      patch.reminderSentAt = undefined;
      patch.reminderError = undefined;
    }
    if (args.error) patch.reminderError = args.error;
    if (args.channel) patch.reminderChannel = args.channel;

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await ctx.db.patch(doc._id, patch as any);
    return doc._id;
  },
});

/**
 * Update deposit info after Stripe checkout session created.
 */
export const updateDepositInfo = mutation({
  args: {
    id: v.string(),
    depositAmount: v.optional(v.number()),
    depositPaymentId: v.optional(v.string()),
    stripeCheckoutSessionId: v.optional(v.string()),
    status: v.optional(v.string()),
  },
  handler: async (ctx, args) => {
    const { id, ...fields } = args;
    let doc: any;
    try { doc = await ctx.db.get(id as any); } catch {}
    if (!doc) {
      const byPrismaId = await ctx.db.query("bookings").filter((q: any) => q.eq(q.field("prismaId"), id)).first();
      doc = byPrismaId;
    }
    if (!doc) return null;
    const updates: Record<string, unknown> = { updatedAt: Date.now() };
    if (fields.depositAmount !== undefined) updates.depositAmount = fields.depositAmount;
    if (fields.depositPaymentId !== undefined) updates.depositPaymentId = fields.depositPaymentId;
    if (fields.stripeCheckoutSessionId !== undefined) updates.stripeCheckoutSessionId = fields.stripeCheckoutSessionId;
    if (fields.status !== undefined) updates.status = fields.status;
    // Auto-mark depositPaid when status is confirmed
    if (fields.status === "confirmed" || (fields.depositPaymentId && fields.status !== "cancelled")) {
      updates.depositPaid = true;
      updates.depositPaidAt = Date.now();
    }
    await ctx.db.patch(doc._id, updates as any);
    return doc._id;
  },
});

/**
 * Record the outcome of a Stripe refund attempt on a cancelled booking.
 *
 * Called by lib/services/booking-refund.service.ts. On success sets
 * refundId, refundAmount, refundedAt. On failure records refundError.
 * For 'not_eligible' (too late to refund) records only the status so
 * the customer-facing message can explain why.
 */
export const markRefundIssued = mutation({
  args: {
    id: v.string(),
    refundStatus: v.string(),
    refundId: v.optional(v.string()),
    refundAmount: v.optional(v.number()),
    refundError: v.optional(v.string()),
  },
  handler: async (ctx, args) => {
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    let doc: any = null;
    try {
      doc = await ctx.db.get(args.id as any);
    } catch {
      /* not a valid Convex id */
    }
    if (!doc) {
      doc = await ctx.db
        .query("bookings")
        .filter((q: any) => q.eq(q.field("prismaId"), args.id))
        .first();
    }
    if (!doc) return null;

    const patch: Record<string, unknown> = {
      refundStatus: args.refundStatus,
      updatedAt: Date.now(),
    };
    if (args.refundStatus === "succeeded") {
      patch.refundedAt = Date.now();
      patch.refundError = undefined;
    }
    if (args.refundId !== undefined) patch.refundId = args.refundId;
    if (args.refundAmount !== undefined) patch.refundAmount = args.refundAmount;
    if (args.refundError !== undefined) patch.refundError = args.refundError;

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await ctx.db.patch(doc._id, patch as any);
    return doc._id;
  },
});
