import { sql } from 'drizzle-orm';
import type { TxDrizzleClient } from '@/server/db/client.js';

export interface CampaignSendProvider {
  sendCampaign(campaignId: number): Promise<void>;
  reconcileCampaign(campaignId: number): Promise<CampaignReconciliation>;
}

export type CampaignReconciliation = 'accepted' | 'definitively_not_accepted' | 'unknown';

export class CampaignSendProviderError extends Error {
  constructor(
    message: string,
    readonly outcome: Exclude<CampaignReconciliation, 'accepted'>,
  ) {
    super(message);
  }
}

interface CampaignSendRow {
  id: string;
  idempotency_key: string;
  status: 'SENDING' | 'AMBIGUOUS' | 'ACCEPTED';
  updated_at: Date | string;
}

interface RawSqlResult {
  rows: unknown[];
}

export class CampaignSendConflictError extends Error {
  readonly code = 'IDEMPOTENCY_CONFLICT';
}

export class CampaignSendInProgressError extends Error {
  readonly code = 'SEND_IN_PROGRESS';
  constructor() {
    super('Campaign send is in progress');
  }
}

export class CampaignSendAmbiguousError extends Error {
  readonly code = 'SEND_AMBIGUOUS';
  constructor(cause?: unknown) {
    super('Provider acceptance is ambiguous; retry with the same idempotency key', { cause });
  }
}

export class CampaignSendRejectedError extends Error {
  readonly code = 'SEND_REJECTED';
}

async function markAccepted(
  db: TxDrizzleClient,
  rowId: string,
  providerStatus: string,
): Promise<void> {
  await db.execute(sql`
    UPDATE campaign_send_attempts
    SET status = 'ACCEPTED', provider_status = ${providerStatus},
        accepted_at = COALESCE(accepted_at, now()), updated_at = now(), last_error = NULL
    WHERE id = ${rowId}
  `);
}

async function performSend(
  db: TxDrizzleClient,
  provider: CampaignSendProvider,
  rowId: string,
  campaignId: number,
): Promise<{ accepted: true; reconciled: false }> {
  await db.execute(sql`
    UPDATE campaign_send_attempts
    SET send_count = send_count + 1, updated_at = now()
    WHERE id = ${rowId} AND status = 'SENDING'
  `);
  try {
    await provider.sendCampaign(campaignId);
  } catch (error) {
    const outcome =
      error instanceof CampaignSendProviderError ? error.outcome : ('unknown' as const);
    await db.execute(sql`
      UPDATE campaign_send_attempts
      SET status = 'AMBIGUOUS', last_error = ${String(error).slice(0, 1000)}, updated_at = now()
      WHERE id = ${rowId} AND status = 'SENDING'
    `);
    if (outcome === 'definitively_not_accepted') throw new CampaignSendRejectedError(String(error));
    throw new CampaignSendAmbiguousError(error);
  }
  await markAccepted(db, rowId, 'accepted');
  return { accepted: true, reconciled: false };
}

export async function sendCampaignOnce(
  db: TxDrizzleClient,
  provider: CampaignSendProvider,
  input: { providerCampaignId: number; idempotencyKey: string; adminUserId: string },
): Promise<{ accepted: true; reconciled: boolean }> {
  const inserted = (await db.execute(sql`
    INSERT INTO campaign_send_attempts
      (provider_campaign_id, idempotency_key, admin_user_id, status)
    VALUES (${input.providerCampaignId}, ${input.idempotencyKey}, ${input.adminUserId}, 'SENDING')
    ON CONFLICT (provider_campaign_id) DO NOTHING
    RETURNING id, idempotency_key, status, updated_at
  `)) as unknown as RawSqlResult;
  if (inserted.rows[0]) {
    return performSend(
      db,
      provider,
      (inserted.rows[0] as CampaignSendRow).id,
      input.providerCampaignId,
    );
  }

  const selected = (await db.execute(sql`
    SELECT id, idempotency_key, status, updated_at
    FROM campaign_send_attempts
    WHERE provider_campaign_id = ${input.providerCampaignId}
  `)) as unknown as RawSqlResult;
  const row = selected.rows[0] as CampaignSendRow | undefined;
  if (!row) throw new Error('Campaign send claim disappeared');
  if (row.idempotency_key !== input.idempotencyKey) throw new CampaignSendConflictError();
  if (row.status === 'ACCEPTED') return { accepted: true, reconciled: false };
  if (row.status === 'SENDING' && Date.now() - new Date(row.updated_at).getTime() < 5 * 60 * 1000) {
    throw new CampaignSendInProgressError();
  }

  let reconciliation: CampaignReconciliation;
  try {
    reconciliation = await provider.reconcileCampaign(input.providerCampaignId);
  } catch (error) {
    throw new CampaignSendAmbiguousError(error);
  }
  if (reconciliation === 'accepted') {
    await markAccepted(db, row.id, reconciliation);
    return { accepted: true, reconciled: true };
  }
  if (reconciliation === 'unknown') throw new CampaignSendAmbiguousError();

  const claimed = (await db.execute(sql`
    UPDATE campaign_send_attempts
    SET status = 'SENDING', provider_status = ${reconciliation}, updated_at = now()
    WHERE id = ${row.id}
      AND (status = 'AMBIGUOUS' OR (status = 'SENDING' AND updated_at < now() - interval '5 minutes'))
    RETURNING id
  `)) as unknown as RawSqlResult;
  if (!claimed.rows[0]) throw new CampaignSendInProgressError();
  return performSend(db, provider, row.id, input.providerCampaignId);
}
