/**
 * Booking Refund Service
 *
 * Processes refunds when a customer cancels an appointment via WhatsApp.
 * Applies the org's cancellation policy (from sector-registry):
 *   - Cancelled >= policy.fullRefundHours before appointment → full refund
 *   - Cancelled inside the window                            → no refund
 *
 * Triggered by lib/services/whatsapp-ai-engine.service.ts after a
 * customer confirms cancellation. The caller then formats a
 * bilingual WhatsApp reply based on the returned RefundOutcome.
 *
 * Idempotent: if the deposit has already been fully refunded (e.g. via
 * the staff dashboard), skip the Stripe call and return 'succeeded'.
 */

import { logger } from "@/lib/logger";
import { convexQuery, convexMutation } from "@/lib/convex-client";
import { createRefund, listRefunds } from "@/lib/stripe/refunds";
import { getSectorConfigForIndustry } from "@/lib/config/sector-registry";

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

export type RefundOutcome =
  | { status: "succeeded"; refundId: string; amount: number }
  | {
      status: "not_eligible";
      reason: "within_cancellation_window";
      windowHours: number;
    }
  | { status: "not_applicable"; reason: "no_deposit" | "no_payment_intent" }
  | { status: "failed"; error: string };

interface BookingRecord {
  _id: string;
  prismaId?: string;
  organizationId?: string;
  depositPaid?: boolean;
  depositAmount?: number;
  depositPaymentId?: string;
  appointmentDate?: number;
  refundStatus?: string;
  refundId?: string;
  status?: string;
}

interface OrgRecord {
  industry?: string;
}

const DEFAULT_FULL_REFUND_HOURS = 4;

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

class BookingRefundService {
  /**
   * Process a refund for a cancelled booking.
   *
   * Call this AFTER the booking has already been marked status='cancelled'.
   */
  async processCancelRefund(bookingId: string): Promise<RefundOutcome> {
    // 1. Load the booking
    const booking = await this.loadBooking(bookingId);
    if (!booking) {
      return { status: "failed", error: "booking not found" };
    }

    // 2. Early exits for bookings with no deposit
    if (!booking.depositPaid) {
      logger.info("[BookingRefund] Skipping — no deposit paid", { bookingId });
      await this.mark(bookingId, "not_applicable");
      return { status: "not_applicable", reason: "no_deposit" };
    }
    if (!booking.depositPaymentId) {
      logger.warn(
        "[BookingRefund] Skipping — depositPaid=true but no depositPaymentId",
        { bookingId },
      );
      await this.mark(bookingId, "not_applicable", {
        error: "no payment intent on booking",
      });
      return { status: "not_applicable", reason: "no_payment_intent" };
    }

    // 3. Policy decision — is the cancellation within the refund window?
    const fullRefundHours = await this.resolveRefundWindow(
      booking.organizationId,
    );
    const now = Date.now();
    const hoursUntilAppointment =
      booking.appointmentDate !== undefined
        ? (booking.appointmentDate - now) / (60 * 60 * 1000)
        : Number.POSITIVE_INFINITY;

    if (hoursUntilAppointment < fullRefundHours) {
      logger.info(
        "[BookingRefund] Not eligible — inside cancellation window",
        {
          bookingId,
          hoursUntilAppointment: Math.round(hoursUntilAppointment * 10) / 10,
          fullRefundHours,
        },
      );
      await this.mark(bookingId, "not_eligible");
      return {
        status: "not_eligible",
        reason: "within_cancellation_window",
        windowHours: fullRefundHours,
      };
    }

    // 4. Idempotency: has this payment already been refunded?
    const paymentIntentId = booking.depositPaymentId;
    try {
      const existingRefunds = await listRefunds({ paymentIntentId });
      const totalRefunded = existingRefunds.reduce(
        (sum, r) => sum + (r.amount || 0),
        0,
      );
      const depositCents = Math.round((booking.depositAmount ?? 0) * 100);
      if (totalRefunded >= depositCents && depositCents > 0) {
        // Already fully refunded (probably by staff)
        const last = existingRefunds[0];
        logger.info("[BookingRefund] Already refunded externally", {
          bookingId,
          totalRefunded,
          depositCents,
        });
        await this.mark(bookingId, "succeeded", {
          refundId: last?.id,
          refundAmount: totalRefunded / 100,
        });
        return {
          status: "succeeded",
          refundId: last?.id ?? "unknown",
          amount: totalRefunded / 100,
        };
      }
    } catch (err) {
      logger.warn(
        "[BookingRefund] listRefunds failed, continuing with refund attempt",
        {
          bookingId,
          error: err instanceof Error ? err.message : String(err),
        },
      );
    }

    // 5. Create the refund
    try {
      const amountCents = Math.round((booking.depositAmount ?? 0) * 100);
      const refund = await createRefund({
        paymentIntentId,
        amount: amountCents > 0 ? amountCents : undefined,
        reason: "requested_by_customer",
        metadata: {
          booking_id: bookingId,
          cancelled_via: "whatsapp",
        },
      });

      logger.info("[BookingRefund] Stripe refund created", {
        bookingId,
        refundId: refund.id,
        amount: refund.amount,
        status: refund.status ?? "unknown",
      });

      await this.mark(bookingId, "succeeded", {
        refundId: refund.id,
        refundAmount: (refund.amount ?? 0) / 100,
      });

      return {
        status: "succeeded",
        refundId: refund.id,
        amount: (refund.amount ?? 0) / 100,
      };
    } catch (err) {
      const errorMessage =
        err instanceof Error ? err.message : "unknown refund error";
      logger.error("[BookingRefund] Stripe refund failed", err, {
        bookingId,
      });
      await this.mark(bookingId, "failed", { error: errorMessage });
      return { status: "failed", error: errorMessage };
    }
  }

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

  private async loadBooking(bookingId: string): Promise<BookingRecord | null> {
    try {
      const row = await convexQuery<BookingRecord>(
        "bookings/queries:getById",
        { id: bookingId },
      );
      return row ?? null;
    } catch (err) {
      logger.warn("[BookingRefund] Failed to load booking", {
        bookingId,
        error: err instanceof Error ? err.message : String(err),
      });
      return null;
    }
  }

  private async resolveRefundWindow(
    organizationId?: string,
  ): Promise<number> {
    if (!organizationId) return DEFAULT_FULL_REFUND_HOURS;
    try {
      const org = await convexQuery<OrgRecord>(
        "organizations/queries:getOrganizationByAnyId",
        { organizationId },
      );
      const sectorConfig = getSectorConfigForIndustry(org?.industry ?? "");
      // Agent B adds cancellationPolicy to SectorConfig; read it defensively
      // so we don't blow up if the field hasn't been wired yet.
      const fromConfig = (
        sectorConfig as unknown as {
          cancellationPolicy?: { fullRefundHours?: number };
        }
      ).cancellationPolicy?.fullRefundHours;
      return typeof fromConfig === "number" && fromConfig >= 0
        ? fromConfig
        : DEFAULT_FULL_REFUND_HOURS;
    } catch {
      return DEFAULT_FULL_REFUND_HOURS;
    }
  }

  private async mark(
    bookingId: string,
    refundStatus: "succeeded" | "failed" | "not_eligible" | "not_applicable",
    extra?: { refundId?: string; refundAmount?: number; error?: string },
  ): Promise<void> {
    try {
      await convexMutation("bookings/mutations:markRefundIssued", {
        id: bookingId,
        refundStatus,
        ...(extra?.refundId ? { refundId: extra.refundId } : {}),
        ...(extra?.refundAmount !== undefined
          ? { refundAmount: extra.refundAmount }
          : {}),
        ...(extra?.error ? { refundError: extra.error } : {}),
      });
    } catch (err) {
      logger.error("[BookingRefund] Failed to mark booking", err, {
        bookingId,
        refundStatus,
      });
    }
  }
}

export const bookingRefundService = new BookingRefundService();
