/**
 * Recurring report schedules. Each row pre-computes `next_run_at` so the
 * BullMQ scheduler (P3) can poll `enabled=true AND next_run_at <= now()`
 * efficiently. Cadence is an enum at v1; cron strings can layer later
 * without a schema change (just add a parser branch in `nextRunFor`).
 *
 * Multi-tenant safe: every query carries `port_id = ctx.portId`. Recipient
 * email + name lists travel as JSONB so reps can edit without per-row
 * relational overhead — small lists, no need for a `schedule_recipients`
 * relation in v1.
 */

import { and, asc, eq } from 'drizzle-orm';

import { db } from '@/lib/db';
import {
  reportSchedules,
  reportTemplates,
  type ReportSchedule,
  type NewReportSchedule,
} from '@/lib/db/schema/reports';
import { createAuditLog, type AuditMeta } from '@/lib/audit';
import { NotFoundError } from '@/lib/errors';
import type {
  CreateReportScheduleInput,
  ListReportSchedulesInput,
  ReportScheduleCadence,
  UpdateReportScheduleInput,
} from '@/lib/validators/reports';

export interface ListReportSchedulesResult {
  data: ReportSchedule[];
  total: number;
  hasMore: boolean;
}

export async function listReportSchedules(
  portId: string,
  query: ListReportSchedulesInput,
): Promise<ListReportSchedulesResult> {
  const conditions = [eq(reportSchedules.portId, portId)];
  if (query.enabled !== undefined) conditions.push(eq(reportSchedules.enabled, query.enabled));
  if (query.templateId) conditions.push(eq(reportSchedules.templateId, query.templateId));

  const where = and(...conditions);
  const offset = (query.page - 1) * query.pageSize;

  const [rows, total] = await Promise.all([
    db
      .select()
      .from(reportSchedules)
      .where(where)
      .orderBy(asc(reportSchedules.nextRunAt))
      .limit(query.pageSize)
      .offset(offset),
    db.$count(reportSchedules, where),
  ]);

  return {
    data: rows,
    total: Number(total),
    hasMore: offset + rows.length < Number(total),
  };
}

export async function getReportSchedule(id: string, portId: string): Promise<ReportSchedule> {
  const row = await db.query.reportSchedules.findFirst({
    where: and(eq(reportSchedules.id, id), eq(reportSchedules.portId, portId)),
  });
  if (!row) throw new NotFoundError('Report schedule');
  return row;
}

/**
 * Compute the next absolute fire-time for a cadence. Anchored to UTC for
 * consistency; per-port timezone display happens in the render worker
 * when it composes the actual email.
 *
 * - `weekly_monday_9` — next Monday 09:00 UTC
 * - `monthly_first_9` — 1st of next month 09:00 UTC (or this month 1st if
 *   we're invoked before 09:00 today on the 1st)
 * - `quarterly_first_9` — 1st of next quarter (Jan/Apr/Jul/Oct) 09:00 UTC
 */
export function nextRunFor(cadence: ReportScheduleCadence, now: Date = new Date()): Date {
  const TARGET_HOUR_UTC = 9;
  const out = new Date(
    Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(), TARGET_HOUR_UTC, 0, 0, 0),
  );
  if (cadence === 'weekly_monday_9') {
    // 1 = Monday. Roll forward until we land on Monday > now.
    while (out.getUTCDay() !== 1 || out.getTime() <= now.getTime()) {
      out.setUTCDate(out.getUTCDate() + 1);
    }
    return out;
  }
  if (cadence === 'monthly_first_9') {
    out.setUTCDate(1);
    if (out.getTime() <= now.getTime()) {
      out.setUTCMonth(out.getUTCMonth() + 1);
    }
    return out;
  }
  // quarterly_first_9
  out.setUTCDate(1);
  const month = out.getUTCMonth();
  const quarterStart = month - (month % 3); // 0 / 3 / 6 / 9
  out.setUTCMonth(quarterStart);
  if (out.getTime() <= now.getTime()) {
    out.setUTCMonth(quarterStart + 3);
  }
  return out;
}

export interface CreateReportScheduleOptions {
  portId: string;
  meta: AuditMeta;
}

export async function createReportSchedule(
  input: CreateReportScheduleInput,
  options: CreateReportScheduleOptions,
): Promise<ReportSchedule> {
  // Verify the template lives in this port — same guard as
  // createReportRun. Without it the FK would catch the cross-port case
  // with an opaque error.
  const tmpl = await db.query.reportTemplates.findFirst({
    where: and(
      eq(reportTemplates.id, input.templateId),
      eq(reportTemplates.portId, options.portId),
    ),
    columns: { id: true },
  });
  if (!tmpl) throw new NotFoundError('Report template');

  const nextRunAt = nextRunFor(input.cadence);
  const values: NewReportSchedule = {
    portId: options.portId,
    templateId: input.templateId,
    cadence: input.cadence,
    recipients: input.recipients,
    outputFormat: input.outputFormat,
    enabled: input.enabled,
    nextRunAt,
    createdBy: options.meta.userId,
  };

  const [row] = await db.insert(reportSchedules).values(values).returning();
  if (!row) throw new Error('createReportSchedule: insert returned no row');

  void createAuditLog({
    portId: options.portId,
    userId: options.meta.userId,
    action: 'create',
    entityType: 'report_schedule',
    entityId: row.id,
    newValue: {
      templateId: row.templateId,
      cadence: row.cadence,
      enabled: row.enabled,
      recipientCount: input.recipients.length,
    },
    ipAddress: options.meta.ipAddress,
    userAgent: options.meta.userAgent,
  });

  return row;
}

export interface UpdateReportScheduleOptions {
  portId: string;
  meta: AuditMeta;
}

export async function updateReportSchedule(
  id: string,
  patch: UpdateReportScheduleInput,
  options: UpdateReportScheduleOptions,
): Promise<ReportSchedule> {
  const existing = await getReportSchedule(id, options.portId);
  const updates: Partial<NewReportSchedule> = { updatedAt: new Date() };
  if (patch.cadence !== undefined) updates.cadence = patch.cadence;
  if (patch.recipients !== undefined) updates.recipients = patch.recipients;
  if (patch.outputFormat !== undefined) updates.outputFormat = patch.outputFormat;
  if (patch.enabled !== undefined) updates.enabled = patch.enabled;

  // Recompute nextRunAt on cadence change or on re-enable; otherwise leave
  // it alone so a mid-cycle edit doesn't slip the next fire-time.
  if (patch.cadence !== undefined) {
    updates.nextRunAt = nextRunFor(patch.cadence);
  } else if (patch.enabled === true && existing.enabled === false) {
    updates.nextRunAt = nextRunFor(existing.cadence as ReportScheduleCadence);
  }

  const [row] = await db
    .update(reportSchedules)
    .set(updates)
    .where(and(eq(reportSchedules.id, id), eq(reportSchedules.portId, options.portId)))
    .returning();
  if (!row) throw new NotFoundError('Report schedule');

  void createAuditLog({
    portId: options.portId,
    userId: options.meta.userId,
    action: 'update',
    entityType: 'report_schedule',
    entityId: row.id,
    oldValue: {
      cadence: existing.cadence,
      enabled: existing.enabled,
      outputFormat: existing.outputFormat,
      recipientCount: existing.recipients.length,
    },
    newValue: {
      cadence: row.cadence,
      enabled: row.enabled,
      outputFormat: row.outputFormat,
      recipientCount: row.recipients.length,
    },
    ipAddress: options.meta.ipAddress,
    userAgent: options.meta.userAgent,
  });

  return row;
}

export async function deleteReportSchedule(
  id: string,
  portId: string,
  meta: AuditMeta,
): Promise<void> {
  const existing = await getReportSchedule(id, portId);
  await db
    .delete(reportSchedules)
    .where(and(eq(reportSchedules.id, id), eq(reportSchedules.portId, portId)));

  void createAuditLog({
    portId,
    userId: meta.userId,
    action: 'delete',
    entityType: 'report_schedule',
    entityId: id,
    oldValue: {
      templateId: existing.templateId,
      cadence: existing.cadence,
      enabled: existing.enabled,
    },
    ipAddress: meta.ipAddress,
    userAgent: meta.userAgent,
  });
}
