import { query } from "../_generated/server";
import { v } from "convex/values";

/**
 * Get all bookings for a user (paginated)
 * Phase 2: Bookings
 */
export const getBookings = query({
  args: {
    userId: v.string(),
    status: v.optional(v.string()),
    limit: v.optional(v.number()),
  },
  handler: async (ctx, args) => {
    const limit = args.limit ?? 50;
    const q = ctx.db
      .query("bookings")
      .withIndex("by_userId", (q) => q.eq("userId", args.userId));

    return await q.order("desc").take(limit);
  },
});

/**
 * Get booking by ID
 * Phase 2: Bookings
 */
export const getById = query({
  args: { id: v.id("bookings") },
  handler: async (ctx, args) => {
    return await ctx.db.get(args.id);
  },
});

/**
 * Get bookings by date range
 * Phase 2: Bookings
 */
export const getByDate = query({
  args: {
    userId: v.string(),
    startDate: v.number(),
    endDate: v.number(),
  },
  handler: async (ctx, args) => {
    return await ctx.db
      .query("bookings")
      .withIndex("by_userId_appointmentDate", (q) =>
        q.eq("userId", args.userId),
      )
      .filter((q) =>
        q.and(
          q.gte(q.field("appointmentDate"), args.startDate),
          q.lte(q.field("appointmentDate"), args.endDate),
        ),
      )
      .collect();
  },
});

/**
 * Get bookings by customer phone
 * Phase 2: Bookings
 */
export const getByCustomer = query({
  args: {
    userId: v.string(),
    customerPhone: v.string(),
  },
  handler: async (ctx, args) => {
    return await ctx.db
      .query("bookings")
      .withIndex("by_userId", (q) => q.eq("userId", args.userId))
      .filter((q) => q.eq(q.field("customerPhone"), args.customerPhone))
      .collect();
  },
});

/**
 * Find bookings that would conflict with a proposed time slot.
 *
 * Used by lib/services/whatsapp-availability.service.ts `isSlotAvailable()`.
 * Returns all active (non-cancelled, non-no_show) bookings for the org whose
 * appointment interval overlaps [startMs, endMs). Callers can pass
 * `excludeBookingId` to filter out a booking being rescheduled so it isn't
 * reported as a conflict with itself.
 *
 * Overlap rule: `booking.appointmentDate < endMs && (booking.appointmentDate + booking.duration*60000) > startMs`.
 */
export const getConflictingBookings = query({
  args: {
    organizationId: v.string(),
    startMs: v.number(),
    endMs: v.number(),
    excludeBookingId: v.optional(v.string()),
    staffMember: v.optional(v.string()),
  },
  handler: async (ctx, args) => {
    // Use the by_organizationId_appointmentDate index to bound the scan; grab
    // a generous window (+/- 1 day) around the proposed slot so we catch any
    // long-duration booking whose start time is outside the slot window but
    // whose end time reaches into it.
    const dayMs = 24 * 60 * 60 * 1000;
    const windowStart = args.startMs - dayMs;
    const windowEnd = args.endMs + dayMs;

    const rows = await ctx.db
      .query("bookings")
      .withIndex("by_organizationId_appointmentDate", (q) =>
        q
          .eq("organizationId", args.organizationId)
          .gte("appointmentDate", windowStart)
          .lte("appointmentDate", windowEnd),
      )
      .collect();

    const conflicts = rows.filter((r) => {
      if (r.status === "cancelled" || r.status === "no_show") return false;
      if (args.excludeBookingId && r._id === args.excludeBookingId) return false;
      if (args.excludeBookingId && r.prismaId === args.excludeBookingId)
        return false;
      if (
        args.staffMember !== undefined &&
        r.staffMember !== undefined &&
        r.staffMember !== args.staffMember
      ) {
        return false;
      }
      const bookingDuration = (r.duration ?? 30) * 60 * 1000;
      const bookingStart = r.appointmentDate;
      const bookingEnd = bookingStart + bookingDuration;
      // Intervals overlap iff start1 < end2 && end1 > start2
      return bookingStart < args.endMs && bookingEnd > args.startMs;
    });

    return conflicts.map((r) => ({
      id: r._id,
      prismaId: r.prismaId ?? null,
      appointmentDate: r.appointmentDate,
      appointmentTime: r.appointmentTime,
      duration: r.duration ?? 30,
      status: r.status,
      staffMember: r.staffMember ?? null,
    }));
  },
});

/**
 * Get upcoming bookings for a customer phone under a specific organization.
 * Used by the WhatsApp AI engine's amend/cancel flow as the primary booking
 * resolver (the Prisma-Convex adapter can't translate a Prisma-style
 * multi-field findMany for bookings because the generic bookings listFn is
 * scoped by userId, not organizationId).
 */
export const getUpcomingByOrgPhone = query({
  args: {
    organizationId: v.string(),
    customerPhone: v.string(),
    limit: v.optional(v.number()),
    includePast: v.optional(v.boolean()),
  },
  handler: async (ctx, args) => {
    const limit = args.limit ?? 10;
    // No compound index on (organizationId, customerPhone); use the existing
    // by_organizationId index then filter client-side. Clinic volume is low
    // enough that this is fine.
    const rows = await ctx.db
      .query("bookings")
      .withIndex("by_organizationId", (q) =>
        q.eq("organizationId", args.organizationId),
      )
      .take(200);

    const now = Date.now();
    const filtered = rows.filter((r) => {
      if (r.customerPhone !== args.customerPhone) return false;
      if (r.status === "cancelled" || r.status === "no_show") return false;
      if (!args.includePast && r.appointmentDate < now) return false;
      return true;
    });
    filtered.sort((a, b) => a.appointmentDate - b.appointmentDate);
    return filtered.slice(0, limit);
  },
});

/**
 * Get upcoming bookings linked to a WhatsApp conversation.
 * Used by the amend/cancel flow in whatsapp-ai-engine.service.ts to
 * resolve "which booking is the customer asking about" when the
 * conversationId is present on the booking.
 */
export const getByConversationId = query({
  args: {
    conversationId: v.string(),
    includePast: v.optional(v.boolean()),
    limit: v.optional(v.number()),
  },
  handler: async (ctx, args) => {
    const limit = args.limit ?? 20;
    const rows = await ctx.db
      .query("bookings")
      .withIndex("by_conversationId", (q) =>
        q.eq("conversationId", args.conversationId),
      )
      .take(limit * 2); // take extra so we can filter client-side

    const now = Date.now();
    const filtered = rows.filter((r) => {
      if (r.status === "cancelled" || r.status === "no_show") return false;
      if (!args.includePast && r.appointmentDate < now) return false;
      return true;
    });
    // Order by soonest appointment first
    filtered.sort((a, b) => a.appointmentDate - b.appointmentDate);
    return filtered.slice(0, limit);
  },
});

/**
 * Get upcoming bookings (confirmed/pending, future dates)
 * Phase 2: Bookings
 */
export const getUpcoming = query({
  args: {
    userId: v.string(),
    limit: v.optional(v.number()),
  },
  handler: async (ctx, args) => {
    const limit = args.limit ?? 10;
    const now = Date.now();
    return await ctx.db
      .query("bookings")
      .withIndex("by_userId_appointmentDate", (q) =>
        q.eq("userId", args.userId),
      )
      .filter((q) =>
        q.and(
          q.gte(q.field("appointmentDate"), now),
          q.or(
            q.eq(q.field("status"), "pending"),
            q.eq(q.field("status"), "confirmed"),
          ),
        ),
      )
      .take(limit);
  },
});

/**
 * Get bookings due for a 24-hour reminder.
 *
 * Returns confirmed/pending bookings whose appointmentDate falls inside the
 * provided window and which have not already had a reminder sent. Used by the
 * daily cron in lib/services/appointment-reminder.service.ts to locate
 * tomorrow's appointments across all orgs.
 */
export const getBookingsDueForReminder = query({
  args: {
    startMs: v.number(),
    endMs: v.number(),
    limit: v.optional(v.number()),
  },
  handler: async (ctx, args) => {
    const limit = args.limit ?? 500;
    // Pull confirmed+pending bookings whose appointmentDate is inside the
    // window. Uses the by_status_appointmentDate index twice (once per status).
    const statuses: Array<"pending" | "confirmed"> = ["pending", "confirmed"];
    const results: Array<Record<string, unknown>> = [];
    for (const status of statuses) {
      const rows = await ctx.db
        .query("bookings")
        .withIndex("by_status_appointmentDate", (q) =>
          q
            .eq("status", status)
            .gte("appointmentDate", args.startMs)
            .lte("appointmentDate", args.endMs),
        )
        .take(limit);
      for (const r of rows) {
        // Skip if a successful reminder has already been sent.
        if (r.reminderSentAt) continue;
        // Skip permanent failures so we don't spam — can be retried manually.
        if (r.reminderStatus === "skipped") continue;
        results.push(r as unknown as Record<string, unknown>);
      }
    }
    return results;
  },
});

/**
 * Get demo bookings (all or by status)
 * Group G: Demo Booking Migration
 */
export const getDemoBookings = query({
  args: {
    status: v.optional(v.string()),
    limit: v.optional(v.number()),
    startDate: v.optional(v.number()),
    endDate: v.optional(v.number()),
  },
  handler: async (ctx, args) => {
    const limit = args.limit ?? 100;
    let q = ctx.db.query("demo_bookings").order("desc");

    if (args.status) {
      q = q.filter((f) => f.eq(f.field("status"), args.status));
    }
    if (args.startDate !== undefined) {
      q = q.filter((f) => f.gte(f.field("appointmentDate"), args.startDate!));
    }
    if (args.endDate !== undefined) {
      q = q.filter((f) => f.lte(f.field("appointmentDate"), args.endDate!));
    }

    return await q.take(limit);
  },
});

/**
 * Get demo bookings for a specific date range (for slot availability)
 * Group G: Demo Booking Migration
 */
export const getDemoBookingsByDate = query({
  args: {
    startDate: v.number(),
    endDate: v.number(),
  },
  handler: async (ctx, args) => {
    return await ctx.db
      .query("demo_bookings")
      .withIndex("by_appointmentDate", (q) =>
        q.gte("appointmentDate", args.startDate),
      )
      .filter((q) =>
        q.and(
          q.lte(q.field("appointmentDate"), args.endDate),
          q.or(
            q.eq(q.field("status"), "pending"),
            q.eq(q.field("status"), "confirmed"),
          ),
        ),
      )
      .collect();
  },
});

/**
 * Get a demo booking by its Convex ID
 * Group G: Demo Booking Migration
 */
export const getDemoBookingById = query({
  args: {
    id: v.id("demo_bookings"),
  },
  handler: async (ctx, args) => {
    return await ctx.db.get(args.id);
  },
});

/**
 * Get a demo booking by its legacy Prisma ID (prismaId field)
 * Used by API routes that still reference Prisma IDs during migration
 * Group G: Demo Booking Migration
 */
export const getDemoBookingByLegacyId = query({
  args: {
    legacyId: v.string(),
  },
  handler: async (ctx, args) => {
    return await ctx.db
      .query("demo_bookings")
      .withIndex("by_prismaId", (q) => q.eq("prismaId", args.legacyId))
      .unique();
  },
});

/**
 * Get all demo bookings matching a given email address
 * Group G: Demo Booking Migration
 */
export const getDemoBookingsByEmail = query({
  args: {
    email: v.string(),
  },
  handler: async (ctx, args) => {
    return await ctx.db
      .query("demo_bookings")
      .withIndex("by_email", (q) => q.eq("email", args.email))
      .collect();
  },
});

/**
 * Get a booking by its prismaId (cuid2 from legacy system).
 */
export const getByPrismaId = query({
  args: { id: v.string() },
  handler: async (ctx, args) => {
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    try { const d = await ctx.db.get(args.id as any); if (d) return d; } catch {}
    return await ctx.db
      .query("bookings")
      .filter((q: any) => q.eq(q.field("prismaId"), args.id))
      .first();
  },
});
