/**
 * WhatsApp nurturing schedule (server-side authority).
 *
 * The client computes the same schedule to preview the message count in the
 * Generate with AI popup, but the count that actually drives generation is
 * settled here: `runWhatsAppNurturingPipeline` produces exactly one message per
 * sequence-plan entry, so the plan length IS the message count. Reconciling the
 * plan against a server-recomputed schedule means a stale, malformed or tampered
 * client payload cannot make the pipeline generate the wrong number of messages.
 *
 * Mirror of `src/frontend/src/modules/sales/whatsapp-nurturing/utils/schedule.ts`
 * — the two workspaces share no code (the frequency union is likewise declared in
 * both `models/WhatsAppCampaign.ts` and the frontend's `types/entities.ts`).
 * Keep the interval table and the walk in step with that file.
 *
 * All arithmetic is UTC: a local-time `new Date('2026-08-01')` shifts a day
 * backwards west of UTC, which would drop or duplicate the first send date.
 */

import type { MessageFrequency } from '../../models/WhatsAppCampaign';

/** Days between consecutive messages, per existing frequency value. */
export const FREQUENCY_INTERVAL_DAYS: Record<MessageFrequency, number> = {
  'daily': 1,
  'alternate-day': 2,
  'every-3-days': 3,
  'weekly': 7,
};

/**
 * Hard ceiling on generated messages, matching the Campaign schema's existing
 * `sequenceDuration` bound (`min: 1, max: 90`).
 */
export const MAX_SCHEDULED_MESSAGES = 90;

export interface ScheduleSlot {
  index: number;
  /** Day offset from the start date, 1-based — what `message.day` holds. */
  day: number;
  /** Send date as `YYYY-MM-DD`. */
  date: string;
}

export interface ScheduleResult {
  slots: ScheduleSlot[];
  count: number;
  truncated: boolean;
  spanDays: number;
}

const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
const DAY_MS = 24 * 60 * 60 * 1000;

/**
 * Normalise anything date-like to `YYYY-MM-DD`.
 *
 * Accepts the plain date strings the popup sends, plus `Date` objects and full
 * ISO timestamps, since a campaign loaded from Mongo carries real Dates.
 */
export function toIsoDateString(value: unknown): string | null {
  if (value instanceof Date) {
    return isNaN(value.getTime()) ? null : value.toISOString().slice(0, 10);
  }
  if (typeof value !== 'string') return null;
  const trimmed = value.trim();
  if (!trimmed) return null;
  const datePart = trimmed.length > 10 && DATE_RE.test(trimmed.slice(0, 10))
    ? trimmed.slice(0, 10)
    : trimmed;
  return DATE_RE.test(datePart) ? datePart : null;
}

/** Parse `YYYY-MM-DD` to a UTC timestamp, rejecting impossible calendar dates. */
function parseUtc(value: unknown): number | null {
  const iso = toIsoDateString(value);
  if (!iso) return null;
  const [y, m, d] = iso.split('-').map(Number);
  const ts = Date.UTC(y, m - 1, d);
  const back = new Date(ts);
  if (back.getUTCFullYear() !== y || back.getUTCMonth() !== m - 1 || back.getUTCDate() !== d) {
    return null;
  }
  return ts;
}

export type ScheduleValidationError =
  | 'start-required'
  | 'end-required'
  | 'frequency-required'
  | 'invalid-date'
  | 'end-before-start';

export const SCHEDULE_ERROR_MESSAGES: Record<ScheduleValidationError, string> = {
  'start-required': 'Start date is required',
  'end-required': 'End date is required',
  'frequency-required': 'A valid message frequency is required',
  'invalid-date': 'Start and end dates must be valid dates',
  'end-before-start': 'End date cannot be earlier than the start date',
};

/** Validate the three inputs. Returns null when the range is usable. */
export function validateScheduleInputs(
  startDate: unknown,
  endDate: unknown,
  frequency: unknown,
): ScheduleValidationError | null {
  if (startDate === undefined || startDate === null || startDate === '') return 'start-required';
  if (endDate === undefined || endDate === null || endDate === '') return 'end-required';
  if (typeof frequency !== 'string' || !(frequency in FREQUENCY_INTERVAL_DAYS)) {
    return 'frequency-required';
  }

  const start = parseUtc(startDate);
  const end = parseUtc(endDate);
  if (start === null || end === null) return 'invalid-date';
  if (end < start) return 'end-before-start';

  return null;
}

/**
 * Walk from the start date to the end date in `frequency` steps, inclusive of
 * both ends where the interval lands on them.
 *
 *   01→07 Aug daily         → 01,02,03,04,05,06,07  (7)
 *   01→07 Aug alternate-day → 01,03,05,07           (4)
 *   01→14 Aug weekly        → 01,08                 (2)
 *
 * Returns an empty schedule for invalid input; callers validate first.
 */
export function buildSchedule(
  startDate: unknown,
  endDate: unknown,
  frequency: MessageFrequency,
): ScheduleResult {
  if (validateScheduleInputs(startDate, endDate, frequency)) {
    return { slots: [], count: 0, truncated: false, spanDays: 0 };
  }

  const start = parseUtc(startDate)!;
  const end = parseUtc(endDate)!;
  const interval = FREQUENCY_INTERVAL_DAYS[frequency];
  const spanDays = Math.round((end - start) / DAY_MS) + 1;

  const slots: ScheduleSlot[] = [];
  let truncated = false;
  for (let ts = start; ts <= end; ts += interval * DAY_MS) {
    if (slots.length >= MAX_SCHEDULED_MESSAGES) { truncated = true; break; }
    slots.push({
      index: slots.length + 1,
      day: Math.round((ts - start) / DAY_MS) + 1,
      date: new Date(ts).toISOString().slice(0, 10),
    });
  }

  return { slots, count: slots.length, truncated, spanDays };
}

/**
 * Force a sequence plan to match the schedule exactly, one entry per send date.
 *
 * The pipeline generates one message per plan entry, so this is the single point
 * that decides how many messages are produced. A plan that is too long is cut; a
 * plan that is too short is padded from the AI's own themes (cycled) so the
 * padded entries still read as part of the campaign rather than as placeholders.
 * Every entry is stamped with its `day` offset and `scheduledDate`.
 *
 * With an empty schedule the plan is returned untouched — that is the pre-existing
 * duration-based path (a campaign created before this change, or the wizard),
 * which must keep working exactly as it did.
 */
export function reconcilePlanToSchedule(
  plan: any[] | undefined | null,
  schedule: ScheduleResult,
): any[] {
  const source = Array.isArray(plan) ? plan.filter((p) => p && typeof p === 'object') : [];
  if (schedule.count === 0) return source;
  if (source.length === 0) return [];

  return schedule.slots.map((slot, i) => {
    const base = source[i] ?? source[i % source.length];
    return {
      ...base,
      day: slot.day,
      scheduledDate: slot.date,
    };
  });
}

/**
 * Stamp generated messages with their scheduled dates.
 *
 * The pipeline returns messages in plan order, so slot N belongs to message N.
 * Any surplus the model produced beyond the schedule is dropped — "do not
 * generate extra messages" is enforced here rather than trusted from the model.
 */
export function applyScheduleToMessages(
  messages: any[] | undefined | null,
  schedule: ScheduleResult,
): any[] {
  const list = Array.isArray(messages) ? messages : [];
  if (schedule.count === 0) return list;

  return list.slice(0, schedule.count).map((message, i) => {
    const slot = schedule.slots[i];
    return {
      ...message,
      day: slot.day,
      scheduledDate: slot.date,
    };
  });
}

/**
 * Reject a generation whose message count does not match the schedule.
 *
 * Over-generation is already handled — `applyScheduleToMessages` drops the
 * surplus. Under-generation cannot be: inventing filler messages to fill the
 * remaining dates would be worse than failing, so the caller surfaces this as a
 * generation error through the existing job-failure path rather than saving a
 * campaign with silently missing sends.
 *
 * Returns null when the count is correct (or when no schedule applies).
 */
export function scheduleCountMismatch(
  messages: any[] | undefined | null,
  schedule: ScheduleResult,
): string | null {
  if (schedule.count === 0) return null;
  const actual = Array.isArray(messages) ? messages.length : 0;
  if (actual === schedule.count) return null;
  return `Generation produced ${actual} message${actual === 1 ? '' : 's'} but the schedule requires ${schedule.count}. Please try generating again.`;
}
