/**
 * Appointment Reminder Service
 *
 * Sends a WhatsApp reminder 24 hours before every upcoming booking across all
 * organizations. Triggered by a daily morning cron (see
 * /api/cron/appointment-reminders and /api/dev/run-reminders).
 *
 * Design:
 * - A single daily run finds every booking whose appointmentDate falls inside
 *   the "tomorrow" window in Doha time.
 * - Skips anything already reminded (reminderSentAt != null) so we're safe to
 *   run multiple times per day if the cron retries.
 * - Uses the org's own Twilio credentials (same path as the inbound webhook)
 *   so outbound messages appear from the clinic's WhatsApp number.
 * - Picks the customer's last known conversation language (Qatari Arabic if
 *   set) and renders the template accordingly; falls back to English.
 *
 * Reply-to-rearrange:
 * - No new inbound logic required. The reminder lands in the same WhatsApp
 *   thread the customer already has; when they reply, the existing webhook AI
 *   engine handles reschedule/cancel via its `amend_appointment` intent.
 */

import { logger } from "@/lib/logger";
import { convexQuery, convexMutation } from "@/lib/convex-client";
import { twilioWhatsAppService } from "@/lib/services/twilio-whatsapp.service";
import { qatariDialectService } from "@/lib/services/qatari-dialect.service";

// ── Types ──────────────────────────────────────────────────────────────

interface BookingRow {
  _id: string;
  prismaId?: string;
  organizationId?: string;
  userId: string;
  customerName: string;
  customerPhone: string;
  service: string;
  appointmentDate: number;
  appointmentTime: string;
  status: string;
  source: string;
  reminderSentAt?: number;
  reminderStatus?: string;
}

interface OrgConversation {
  _id: string;
  userPhone: string;
  language?: string;
  userName?: string;
}

export interface ReminderResult {
  processedCount: number;
  sentCount: number;
  failedCount: number;
  skippedCount: number;
  details: Array<{
    bookingId: string;
    customerPhone: string;
    status: "sent" | "failed" | "skipped";
    error?: string;
  }>;
}

// ── Time helpers (Doha / Qatar = UTC+3, no DST) ────────────────────────

const DOHA_OFFSET_MS = 3 * 60 * 60 * 1000;

/**
 * Return [startMs, endMs] covering the full day 24 hours from now in Doha
 * time. Example: if run at 09:00 Doha on 2026-04-11, returns the range
 * 2026-04-12 00:00 to 2026-04-12 23:59:59 Doha (converted to UTC ms).
 */
export function getTomorrowWindowDoha(nowMs: number = Date.now()): {
  startMs: number;
  endMs: number;
  label: string;
} {
  // Shift to Doha local time, add 24h, then snap to start of day
  const dohaNow = new Date(nowMs + DOHA_OFFSET_MS);
  const dohaTomorrow = new Date(dohaNow.getTime() + 24 * 60 * 60 * 1000);
  const y = dohaTomorrow.getUTCFullYear();
  const m = dohaTomorrow.getUTCMonth();
  const d = dohaTomorrow.getUTCDate();
  // Start/end of the Doha "tomorrow" day expressed in UTC ms
  const startUtc = Date.UTC(y, m, d, 0, 0, 0) - DOHA_OFFSET_MS;
  const endUtc = Date.UTC(y, m, d, 23, 59, 59, 999) - DOHA_OFFSET_MS;
  const label = `${y}-${String(m + 1).padStart(2, "0")}-${String(d).padStart(2, "0")}`;
  return { startMs: startUtc, endMs: endUtc, label };
}

function formatAppointment(
  dateMs: number,
  time: string,
  lang: "en" | "ar",
): { dateStr: string; timeStr: string } {
  // Format date in Doha local time
  const d = new Date(dateMs + DOHA_OFFSET_MS);
  const dayNamesEn = [
    "Sunday",
    "Monday",
    "Tuesday",
    "Wednesday",
    "Thursday",
    "Friday",
    "Saturday",
  ];
  const dayNamesAr = [
    "الأحد",
    "الإثنين",
    "الثلاثاء",
    "الأربعاء",
    "الخميس",
    "الجمعة",
    "السبت",
  ];
  const monthNamesEn = [
    "January",
    "February",
    "March",
    "April",
    "May",
    "June",
    "July",
    "August",
    "September",
    "October",
    "November",
    "December",
  ];
  const monthNamesAr = [
    "يناير",
    "فبراير",
    "مارس",
    "أبريل",
    "مايو",
    "يونيو",
    "يوليو",
    "أغسطس",
    "سبتمبر",
    "أكتوبر",
    "نوفمبر",
    "ديسمبر",
  ];
  const dow = d.getUTCDay();
  const day = d.getUTCDate();
  const month = d.getUTCMonth();
  const dateStr =
    lang === "ar"
      ? `${dayNamesAr[dow]} ${day} ${monthNamesAr[month]}`
      : `${dayNamesEn[dow]}, ${day} ${monthNamesEn[month]}`;
  return { dateStr, timeStr: time };
}

/**
 * Compose a human-readable "street, city, country" string plus a
 * Google Maps search URL from loose address components. Returns `null`
 * if nothing usable is available so the template can omit the line.
 */
export function buildAddressBlock(parts: {
  address?: string | null;
  city?: string | null;
  country?: string | null;
}): { display: string; mapsUrl: string } | null {
  const clean = (s?: string | null) => (s || "").trim();
  const address = clean(parts.address);
  const city = clean(parts.city);
  const country = clean(parts.country);
  // Accept the short ISO country code ("QA") only if we have something else.
  const meaningful = [address, city, country].filter((p) => p.length > 1);
  if (!meaningful.length) return null;
  // A bare country code with nothing else is not a real address.
  if (meaningful.length === 1 && meaningful[0].length <= 3) return null;
  // De-duplicate country vs address if the address already ends with the country.
  const display = [address, city, country]
    .filter((p, i, arr) => p && arr.indexOf(p) === i)
    .join(", ");
  const mapsUrl = `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(display)}`;
  return { display, mapsUrl };
}

function renderEnglishTemplate(
  businessName: string,
  customerName: string,
  service: string,
  dateStr: string,
  timeStr: string,
  address: { display: string; mapsUrl: string } | null,
): string {
  const greeting = customerName ? `Hi ${customerName.split(" ")[0]},` : "Hi,";
  const lines = [
    `${greeting} this is a friendly reminder from *${businessName}*.`,
    "",
    `You have an appointment tomorrow:`,
    `• Service: ${service}`,
    `• Date: ${dateStr}`,
    `• Time: ${timeStr}`,
  ];
  if (address) {
    lines.push(`• Location: ${address.display}`);
    lines.push(`📍 ${address.mapsUrl}`);
  }
  lines.push(
    "",
    `If you need to reschedule or cancel, just reply to this message and let us know — we're happy to help.`,
    "",
    `See you soon! 🌿`,
  );
  return lines.join("\n");
}

function renderArabicTemplate(
  businessName: string,
  customerName: string,
  service: string,
  dateStr: string,
  timeStr: string,
  address: { display: string; mapsUrl: string } | null,
): string {
  const greeting = customerName
    ? `هلا ${customerName.split(" ")[0]}،`
    : "هلا،";
  const lines = [
    `${greeting} تذكير من *${businessName}*.`,
    "",
    `عندك موعد باچر:`,
    `• الخدمة: ${service}`,
    `• التاريخ: ${dateStr}`,
    `• الوقت: ${timeStr}`,
  ];
  if (address) {
    lines.push(`• الموقع: ${address.display}`);
    lines.push(`📍 ${address.mapsUrl}`);
  }
  lines.push(
    "",
    `إذا تبي تغير الموعد أو تلغيه، فقط رد على هالرسالة وقلنا — وإحنا بخدمتك.`,
    "",
    `نشوفك باچر! 🌿`,
  );
  return lines.join("\n");
}

// ── Service ─────────────────────────────────────────────────────────────

class AppointmentReminderService {
  /**
   * Main entry point called by the daily cron (and the dev manual trigger).
   */
  async processAppointmentReminders(opts?: {
    /** Override the "now" time — used in tests. */
    nowMs?: number;
    /** Dry run — find and render messages but don't actually send/mark. */
    dryRun?: boolean;
  }): Promise<ReminderResult> {
    const nowMs = opts?.nowMs ?? Date.now();
    const dryRun = opts?.dryRun === true;
    const { startMs, endMs, label } = getTomorrowWindowDoha(nowMs);

    logger.info("[AppointmentReminder] Starting run", {
      tomorrowDoha: label,
      startMs,
      endMs,
      dryRun,
    });

    const bookings = await convexQuery<BookingRow[]>(
      "bookings/queries:getBookingsDueForReminder",
      { startMs, endMs },
    );

    const rows = bookings ?? [];
    logger.info("[AppointmentReminder] Found candidates", {
      count: rows.length,
    });

    const result: ReminderResult = {
      processedCount: rows.length,
      sentCount: 0,
      failedCount: 0,
      skippedCount: 0,
      details: [],
    };

    for (const booking of rows) {
      const bookingIdForUpdate = booking.prismaId || booking._id;
      try {
        const outcome = await this.sendReminderForBooking(booking, dryRun);
        if (outcome.status === "sent") result.sentCount += 1;
        else if (outcome.status === "failed") result.failedCount += 1;
        else result.skippedCount += 1;

        result.details.push({
          bookingId: bookingIdForUpdate,
          customerPhone: booking.customerPhone,
          ...outcome,
        });
      } catch (err) {
        const errorMessage =
          err instanceof Error ? err.message : "unknown error";
        logger.error("[AppointmentReminder] Unexpected failure", {
          bookingId: bookingIdForUpdate,
          error: errorMessage,
        });
        result.failedCount += 1;
        result.details.push({
          bookingId: bookingIdForUpdate,
          customerPhone: booking.customerPhone,
          status: "failed",
          error: errorMessage,
        });
        if (!dryRun) {
          await this.markReminder(
            bookingIdForUpdate,
            "failed",
            errorMessage,
          );
        }
      }
    }

    logger.info("[AppointmentReminder] Run complete", {
      processed: result.processedCount,
      sent: result.sentCount,
      failed: result.failedCount,
      skipped: result.skippedCount,
    });

    return result;
  }

  // ── Per-booking send ─────────────────────────────────────────────────

  private async sendReminderForBooking(
    booking: BookingRow,
    dryRun: boolean,
  ): Promise<{ status: "sent" | "failed" | "skipped"; error?: string }> {
    const bookingIdForUpdate = booking.prismaId || booking._id;

    // 1. Must have an org with Twilio credentials
    if (!booking.organizationId) {
      if (!dryRun)
        await this.markReminder(
          bookingIdForUpdate,
          "skipped",
          "no organizationId",
        );
      return { status: "skipped", error: "no organizationId" };
    }

    const credentials = await twilioWhatsAppService.getCredentialsForOrg(
      booking.organizationId,
    );
    if (!credentials) {
      if (!dryRun)
        await this.markReminder(
          bookingIdForUpdate,
          "skipped",
          "no twilio credentials",
        );
      return { status: "skipped", error: "no twilio credentials" };
    }

    // 2. Must have a customer phone
    const phone = (booking.customerPhone || "").trim();
    if (!phone) {
      if (!dryRun)
        await this.markReminder(
          bookingIdForUpdate,
          "skipped",
          "no customer phone",
        );
      return { status: "skipped", error: "no customer phone" };
    }

    // 3. Resolve display name + language + address
    const { displayName, language, address } = await this.resolveOrgAndLanguage(
      booking.organizationId,
      phone,
    );

    // 4. Render template in the customer's language
    const { dateStr, timeStr } = formatAppointment(
      booking.appointmentDate,
      booking.appointmentTime,
      language,
    );
    let body =
      language === "ar"
        ? renderArabicTemplate(
            displayName,
            booking.customerName,
            booking.service,
            dateStr,
            timeStr,
            address,
          )
        : renderEnglishTemplate(
            displayName,
            booking.customerName,
            booking.service,
            dateStr,
            timeStr,
            address,
          );

    // Apply Qatari dialect polish for Arabic (same path the AI engine uses)
    if (language === "ar") {
      try {
        const translated = await qatariDialectService.translateToQatari(body);
        if (translated?.translation) body = translated.translation;
      } catch (err) {
        logger.warn("[AppointmentReminder] Qatari dialect polish failed", {
          error: err instanceof Error ? err.message : err,
        });
      }
    }

    if (dryRun) {
      logger.info("[AppointmentReminder] DRY RUN — would send", {
        bookingId: bookingIdForUpdate,
        phone,
        language,
        bodyPreview: body.slice(0, 200),
      });
      return { status: "sent" };
    }

    // 5. Send via Twilio
    const whatsappTo = phone.startsWith("whatsapp:")
      ? phone
      : `whatsapp:${phone}`;
    const sendResult = await twilioWhatsAppService.sendTextMessage(
      credentials,
      whatsappTo,
      body,
    );

    if (!sendResult.success) {
      const errorMessage = sendResult.error || "twilio send failed";
      await this.markReminder(bookingIdForUpdate, "failed", errorMessage);
      return { status: "failed", error: errorMessage };
    }

    // 6. Mark booking as reminded
    await this.markReminder(bookingIdForUpdate, "sent");
    logger.info("[AppointmentReminder] Sent", {
      bookingId: bookingIdForUpdate,
      phone: whatsappTo,
      messageSid: sendResult.messageSid,
    });
    return { status: "sent" };
  }

  // ── Helpers ───────────────────────────────────────────────────────────

  private async resolveOrgAndLanguage(
    organizationId: string,
    phone: string,
  ): Promise<{
    displayName: string;
    language: "en" | "ar";
    address: { display: string; mapsUrl: string } | null;
  }> {
    // Display name + address both come from the org owner's user record
    // (same pattern as the Twilio webhook's companyName lookup).
    let displayName = "";
    let address: { display: string; mapsUrl: string } | null = null;
    let ownerId: string | undefined;
    try {
      const orgInfo = await convexQuery<{ name?: string; ownerId?: string }>(
        "organizations/queries:getOrganizationByAnyId",
        { organizationId },
      );
      displayName = orgInfo?.name || "";
      ownerId = orgInfo?.ownerId;
    } catch {
      /* best-effort only */
    }
    if (ownerId) {
      try {
        const owner = await convexQuery<{
          companyName?: string;
          address?: string;
          city?: string;
          country?: string;
        } | null>("auth/queries:getUser", { id: ownerId });
        if (owner?.companyName) displayName = owner.companyName;
        address = buildAddressBlock({
          address: owner?.address,
          city: owner?.city,
          country: owner?.country,
        });
      } catch {
        /* best-effort only */
      }
    }
    if (!displayName) displayName = "your clinic";

    // Language: look up the most recent org_whatsapp_conversations row for
    // this phone in this org. If not present → English default.
    let language: "en" | "ar" = "en";
    try {
      const convs = await convexQuery<OrgConversation[]>(
        "whatsapp/queries:getOrgConversations",
        { organizationId, userPhone: phone, limit: 1 },
      );
      const lang = convs?.[0]?.language;
      if (lang === "ar") language = "ar";
    } catch {
      /* keep english default */
    }
    return { displayName, language, address };
  }

  private async markReminder(
    bookingId: string,
    status: "sent" | "failed" | "skipped",
    error?: string,
  ): Promise<void> {
    try {
      await convexMutation("bookings/mutations:markBookingReminderSent", {
        id: bookingId,
        status,
        ...(error ? { error } : {}),
        channel: "whatsapp",
      });
    } catch (err) {
      logger.error("[AppointmentReminder] Failed to mark booking", {
        bookingId,
        status,
        error: err instanceof Error ? err.message : err,
      });
    }
  }
}

export const appointmentReminderService = new AppointmentReminderService();
