import { and, eq, inArray, isNotNull, sql } from 'drizzle-orm';

import { db } from '@/lib/db';
import {
  documents,
  documentSigners,
  documentEvents,
  documentTemplates,
} from '@/lib/db/schema/documents';
import { ports } from '@/lib/db/schema/ports';
import { sendReminder as documensoRemind } from '@/lib/services/documenso-client';
import { logger } from '@/lib/logger';

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

function getCurrentHourInTimezone(timezone: string): number {
  const now = new Date();
  const formatter = new Intl.DateTimeFormat('en-US', {
    timeZone: timezone,
    hour: 'numeric',
    hour12: false,
  });
  return parseInt(formatter.format(now), 10);
}

interface ReminderEligibilityArgs {
  status: string;
  documensoId: string | null;
  remindersDisabled: boolean;
  reminderCadenceOverride: number | null;
  templateCadenceDays: number | null;
  lastReminderAt: Date | null;
  now?: Date;
}

/**
 * Pure cadence/disable check used by the cron path. Auto-mode reminders
 * additionally enforce the 9–16 port-timezone window, which lives in the
 * caller below.
 */
export function isReminderDue(args: ReminderEligibilityArgs): boolean {
  const now = args.now ?? new Date();
  if (!['sent', 'partially_signed'].includes(args.status)) return false;
  if (args.documensoId == null) return false;
  if (args.remindersDisabled) return false;

  const cadence = args.reminderCadenceOverride ?? args.templateCadenceDays;
  if (cadence === null) return false;

  if (args.lastReminderAt == null) return true;
  const elapsedMs = now.getTime() - args.lastReminderAt.getTime();
  return elapsedMs >= cadence * 24 * 60 * 60 * 1000;
}

// ─── sendReminderIfAllowed ───────────────────────────────────────────────────

export interface SendReminderOptions {
  /** true = cron auto-fire, enforces 9-16 window + cadence cooldown.
   *  false (default) = manual UI action, bypasses both. */
  auto?: boolean;
  /** Optional - target a specific pending signer (parallel mode), or
   *  bypass the lowest-pending default in sequential mode (must still be the
   *  next pending signer in that case). */
  signerId?: string;
}

export interface SendReminderResult {
  sent: boolean;
  reason?: string;
  signerId?: string;
}

export async function sendReminderIfAllowed(
  documentId: string,
  portId: string,
  options: SendReminderOptions = {},
): Promise<SendReminderResult> {
  const { auto = false, signerId } = options;

  const doc = await db.query.documents.findFirst({
    where: and(eq(documents.id, documentId), eq(documents.portId, portId)),
  });

  if (!doc) return { sent: false, reason: 'Document not found' };
  if (!doc.documensoId) return { sent: false, reason: 'Document has no Documenso id' };
  if (!['sent', 'partially_signed'].includes(doc.status)) {
    return { sent: false, reason: `Document is ${doc.status}` };
  }
  if (doc.remindersDisabled) {
    return { sent: false, reason: 'Reminders disabled for this document' };
  }

  // Resolve effective cadence (override → template → null/disabled)
  let templateCadenceDays: number | null = null;
  if (auto) {
    // Auto path needs a cadence to fire at all; manual sends bypass cadence.
    // We still load the template cadence so `isReminderDue` has the input.
    const templateRow = doc.fileId
      ? await db.query.documentTemplates.findFirst({
          // Fallback: look up by document type if no explicit template_id.
          where: and(
            eq(documentTemplates.portId, portId),
            eq(documentTemplates.templateType, doc.documentType),
          ),
        })
      : null;
    templateCadenceDays = templateRow?.reminderCadenceDays ?? null;

    const lastReminder = await db.query.documentEvents.findFirst({
      where: and(
        eq(documentEvents.documentId, documentId),
        eq(documentEvents.eventType, 'reminder_sent'),
      ),
      orderBy: (de, { desc }) => [desc(de.createdAt)],
    });

    const due = isReminderDue({
      status: doc.status,
      documensoId: doc.documensoId,
      remindersDisabled: doc.remindersDisabled,
      reminderCadenceOverride: doc.reminderCadenceOverride,
      templateCadenceDays,
      lastReminderAt: lastReminder?.createdAt ?? null,
    });
    if (!due) return { sent: false, reason: 'Cadence cooldown' };

    // Auto only: 9-16 window in port timezone
    const port = await db.query.ports.findFirst({ where: eq(ports.id, portId) });
    const timezone = port?.timezone ?? 'UTC';
    const currentHour = getCurrentHourInTimezone(timezone);
    if (currentHour < 9 || currentHour >= 16) {
      return { sent: false, reason: 'Outside 9-16 port-timezone window' };
    }
  }

  // Pick the signer to nudge.
  const pendingSigners = await db
    .select()
    .from(documentSigners)
    .where(and(eq(documentSigners.documentId, documentId), eq(documentSigners.status, 'pending')))
    .orderBy(sql`${documentSigners.signingOrder} ASC`);
  if (pendingSigners.length === 0) {
    return { sent: false, reason: 'No pending signers' };
  }

  let target = pendingSigners[0]!;

  if (signerId) {
    const requested = pendingSigners.find((s) => s.id === signerId);
    if (!requested) {
      return { sent: false, reason: 'Signer is not pending' };
    }
    // Sequential mode: require the lowest pending order.
    if (requested.signingOrder !== pendingSigners[0]!.signingOrder) {
      return { sent: false, reason: 'Signer is not next in sequence' };
    }
    target = requested;
  }

  try {
    await documensoRemind(doc.documensoId, target.id, portId);
  } catch (err) {
    logger.error({ err, documentId, signerId: target.id }, 'Documenso reminder failed');
    return { sent: false, reason: 'Documenso reminder failed' };
  }

  await db.insert(documentEvents).values({
    documentId,
    eventType: 'reminder_sent',
    signerId: target.id,
    eventData: {
      signerEmail: target.signerEmail,
      signerRole: target.signerRole,
      auto,
    },
  });

  return { sent: true, signerId: target.id };
}

// ─── processReminderQueue ────────────────────────────────────────────────────

/**
 * Cron entry point. Selects in-flight documents whose effective cadence
 * (override or template) is set, then attempts auto-fire on each.
 * `interests.reminderEnabled` is no longer part of the gating - per-doc
 * `remindersDisabled` is the kill switch instead.
 *
 * Performance: the pre-bulk version called `sendReminderIfAllowed` per
 * doc, which re-fetched the port row (invariant), the template-by-type
 * map (repeats heavily), the last reminder event, and the pending
 * signers - 5×N round trips per cron tick. This implementation hoists
 * the invariants out of the loop and turns the per-row queries into
 * grouped scans (one per dimension), so a port with 500 in-flight docs
 * is now ~7 round trips total instead of ~2500.
 */
export async function processReminderQueue(portId: string): Promise<void> {
  const activeDocs = await db
    .select({
      id: documents.id,
      documentType: documents.documentType,
      documensoId: documents.documensoId,
      status: documents.status,
      remindersDisabled: documents.remindersDisabled,
      reminderCadenceOverride: documents.reminderCadenceOverride,
      fileId: documents.fileId,
    })
    .from(documents)
    // CRITICAL: scope the join to the same port - `documentTemplates.templateType`
    // is not unique across ports, so a leftJoin without `portId` produces a
    // cartesian explosion (one output row per template-type match across
    // every port). The downstream loop fires `documensoRemind` per row,
    // which means the same signer in port A would receive N reminders on
    // a single cron tick (once per port that defined a template of the
    // same type). Audit follow-up after Tier 3 ship.
    .leftJoin(
      documentTemplates,
      and(
        eq(documentTemplates.templateType, documents.documentType),
        eq(documentTemplates.portId, portId),
      ),
    )
    .where(
      and(
        eq(documents.portId, portId),
        inArray(documents.status, ['sent', 'partially_signed']),
        isNotNull(documents.documensoId),
        eq(documents.remindersDisabled, false),
        sql`COALESCE(${documents.reminderCadenceOverride}, ${documentTemplates.reminderCadenceDays}) IS NOT NULL`,
      ),
    );

  if (activeDocs.length === 0) return;

  // Hoist invariants out of the per-doc loop ────────────────────────────────

  // (1) Port row (timezone) - invariant across the whole batch.
  const port = await db.query.ports.findFirst({ where: eq(ports.id, portId) });
  const timezone = port?.timezone ?? 'UTC';
  const currentHour = getCurrentHourInTimezone(timezone);
  if (currentHour < 9 || currentHour >= 16) {
    // Outside the 9-16 window - nothing to do this tick.
    return;
  }

  // (2) Per-type template cadence map - repeats per documentType.
  const distinctTypes = Array.from(new Set(activeDocs.map((d) => d.documentType)));
  const templateRows = await db
    .select({
      templateType: documentTemplates.templateType,
      reminderCadenceDays: documentTemplates.reminderCadenceDays,
    })
    .from(documentTemplates)
    .where(
      and(
        eq(documentTemplates.portId, portId),
        inArray(documentTemplates.templateType, distinctTypes),
      ),
    );
  const templateCadenceByType = new Map(
    templateRows.map((r) => [r.templateType, r.reminderCadenceDays ?? null]),
  );

  // (3) Latest reminder_sent event per doc - one grouped query.
  const docIds = activeDocs.map((d) => d.id);
  const lastReminderRows = await db
    .select({
      documentId: documentEvents.documentId,
      lastAt: sql<Date>`max(${documentEvents.createdAt})`,
    })
    .from(documentEvents)
    .where(
      and(
        inArray(documentEvents.documentId, docIds),
        eq(documentEvents.eventType, 'reminder_sent'),
      ),
    )
    .groupBy(documentEvents.documentId);
  const lastReminderByDoc = new Map(lastReminderRows.map((r) => [r.documentId, r.lastAt]));

  // (4) Pending signers per doc - one inArray scan.
  const pendingSignerRows = await db
    .select()
    .from(documentSigners)
    .where(and(inArray(documentSigners.documentId, docIds), eq(documentSigners.status, 'pending')))
    .orderBy(sql`${documentSigners.signingOrder} ASC`);
  const pendingByDoc = new Map<string, typeof pendingSignerRows>();
  for (const row of pendingSignerRows) {
    const arr = pendingByDoc.get(row.documentId) ?? [];
    arr.push(row);
    pendingByDoc.set(row.documentId, arr);
  }

  // Per-doc fire - at this point every per-row query is a Map.get.
  for (const doc of activeDocs) {
    try {
      const due = isReminderDue({
        status: doc.status,
        documensoId: doc.documensoId,
        remindersDisabled: doc.remindersDisabled,
        reminderCadenceOverride: doc.reminderCadenceOverride,
        templateCadenceDays: templateCadenceByType.get(doc.documentType) ?? null,
        lastReminderAt: lastReminderByDoc.get(doc.id) ?? null,
      });
      if (!due) continue;

      const pending = pendingByDoc.get(doc.id) ?? [];
      const target = pending[0];
      if (!target || !doc.documensoId) continue;

      await documensoRemind(doc.documensoId, target.id, portId);
      await db.insert(documentEvents).values({
        documentId: doc.id,
        eventType: 'reminder_sent',
        signerId: target.id,
        eventData: {
          signerEmail: target.signerEmail,
          signerRole: target.signerRole,
          auto: true,
        },
      });
    } catch (err) {
      logger.error({ err, documentId: doc.id, portId }, 'Reminder processing failed');
    }
  }
}
