/**
 * DO alarm arm/disarm helpers.
 *
 * Canonical pattern from md-server-dev §7:
 *   await armDealAlarm(env, id, endsAt);
 *   await disarmDealAlarm(env, id);
 *
 * All query-layer create/update functions with a deadline call arm*.
 * All cancel/delete functions call disarm*.
 *
 * DO instances are keyed by entity id via idFromName(id) for UUID-keyed
 * entities, or idFromName(page) for page-slug-keyed ScheduledPublishDO.
 *
 * GroupDealDO accepts an optional `kind` discriminator:
 *   'deadline'         — default, fires at windowEnd / extendedDeadline
 *   'partial_decision' — fires at partialDecisionDeadline
 */

import type { MultidealEnv } from './env.js';
import { captureCaught } from './observability/capture.server.js';

// ─── Deal ──────────────────────────────────────────────────────────────────────

export async function armDealAlarm(env: MultidealEnv, dealId: string, at: Date): Promise<void> {
  const stub = env.DEAL_DO.get(env.DEAL_DO.idFromName(dealId));
  await stub.fetch('https://do/arm', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ at: at.getTime(), entityId: dealId }),
  });
}

export async function disarmDealAlarm(env: MultidealEnv, dealId: string): Promise<void> {
  const stub = env.DEAL_DO.get(env.DEAL_DO.idFromName(dealId));
  await stub.fetch('https://do/disarm', { method: 'POST' });
}

// ─── Gold window ───────────────────────────────────────────────────────────────

export async function armGoldWindowAlarm(
  env: MultidealEnv,
  dealId: string,
  at: Date,
): Promise<void> {
  const stub = env.GOLD_WINDOW_DO.get(env.GOLD_WINDOW_DO.idFromName(dealId));
  await stub.fetch('https://do/arm', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ at: at.getTime(), entityId: dealId }),
  });
}

export async function disarmGoldWindowAlarm(env: MultidealEnv, dealId: string): Promise<void> {
  const stub = env.GOLD_WINDOW_DO.get(env.GOLD_WINDOW_DO.idFromName(dealId));
  await stub.fetch('https://do/disarm', { method: 'POST' });
}

// ─── Group deal ────────────────────────────────────────────────────────────────

export async function armGroupDealAlarm(
  env: MultidealEnv,
  groupDealId: string,
  at: Date,
  kind: 'deadline' | 'partial_decision' = 'deadline',
): Promise<void> {
  const stub = env.GROUP_DEAL_DO.get(env.GROUP_DEAL_DO.idFromName(groupDealId));
  await stub.fetch('https://do/arm', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ at: at.getTime(), kind, entityId: groupDealId }),
  });
}

export async function disarmGroupDealAlarm(env: MultidealEnv, groupDealId: string): Promise<void> {
  const stub = env.GROUP_DEAL_DO.get(env.GROUP_DEAL_DO.idFromName(groupDealId));
  await stub.fetch('https://do/disarm', { method: 'POST' });
}

// ─── Personal offer ────────────────────────────────────────────────────────────

export async function armPersonalOfferAlarm(
  env: MultidealEnv,
  requestId: string,
  at: Date,
): Promise<void> {
  const stub = env.PERSONAL_OFFER_DO.get(env.PERSONAL_OFFER_DO.idFromName(requestId));
  await stub.fetch('https://do/arm', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ at: at.getTime(), entityId: requestId }),
  });
}

export async function disarmPersonalOfferAlarm(
  env: MultidealEnv,
  requestId: string,
): Promise<void> {
  const stub = env.PERSONAL_OFFER_DO.get(env.PERSONAL_OFFER_DO.idFromName(requestId));
  await stub.fetch('https://do/disarm', { method: 'POST' });
}

// ─── Scheduled publish ─────────────────────────────────────────────────────────

export async function armScheduledPublishAlarm(
  env: MultidealEnv,
  page: string,
  at: Date,
): Promise<void> {
  const stub = env.SCHEDULED_PUBLISH_DO.get(env.SCHEDULED_PUBLISH_DO.idFromName(page));
  await stub.fetch('https://do/arm', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ at: at.getTime(), entityId: page }),
  });
}

export async function disarmScheduledPublishAlarm(env: MultidealEnv, page: string): Promise<void> {
  const stub = env.SCHEDULED_PUBLISH_DO.get(env.SCHEDULED_PUBLISH_DO.idFromName(page));
  await stub.fetch('https://do/disarm', { method: 'POST' });
}

// ─── Support Ticket ────────────────────────────────────────────────────────────

export async function armSupportTicketAlarm(
  env: MultidealEnv,
  ticketId: string,
  at: Date,
): Promise<void> {
  const stub = env.SUPPORT_TICKET_DO.get(env.SUPPORT_TICKET_DO.idFromName(ticketId));
  await stub.fetch('https://do/arm', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ at: at.getTime(), entityId: ticketId }),
  });
}

export async function disarmSupportTicketAlarm(env: MultidealEnv, ticketId: string): Promise<void> {
  const stub = env.SUPPORT_TICKET_DO.get(env.SUPPORT_TICKET_DO.idFromName(ticketId));
  await stub.fetch('https://do/disarm', { method: 'POST' });
}

// ─── Support Case ──────────────────────────────────────────────────────────────

export type CaseAlarmKind =
  | 'vendor_window_50pct'
  | 'vendor_window_expires_at'
  | 'sla_ai_due_at'
  | 'sla_human_due_at'
  | 'autoclose_at';

/**
 * Arms a named alarm on the SupportCaseDO.
 * The DO stores a map of { kind → at } so multiple concurrent alarms can fire.
 */
export async function armSupportCaseAlarm(
  env: MultidealEnv,
  caseId: string,
  kind: CaseAlarmKind,
  at: Date,
): Promise<void> {
  const stub = env.SUPPORT_CASE_DO.get(env.SUPPORT_CASE_DO.idFromName(caseId));
  await stub.fetch('https://do/arm', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ kind, at: at.getTime(), entityId: caseId }),
  });
}

export async function disarmSupportCaseAlarms(env: MultidealEnv, caseId: string): Promise<void> {
  const stub = env.SUPPORT_CASE_DO.get(env.SUPPORT_CASE_DO.idFromName(caseId));
  await stub.fetch('https://do/disarm', { method: 'POST' });
}

// ─── Cache epoch ───────────────────────────────────────────────────────────────

/** Read the current global catalog epoch from CacheEpochDO. Returns 0 on any failure. */
export async function doGetCacheEpoch(env: MultidealEnv): Promise<number> {
  const stub = env.CACHE_EPOCH_DO.get(env.CACHE_EPOCH_DO.idFromName('catalog'));
  const res = await stub.fetch(new Request('https://do/get', { method: 'GET' }));
  const body = (await res.json()) as { epoch: number };
  return body.epoch;
}

/** Bump the global catalog epoch. Returns the new epoch. */
export async function doBumpCacheEpoch(env: MultidealEnv): Promise<number> {
  const stub = env.CACHE_EPOCH_DO.get(env.CACHE_EPOCH_DO.idFromName('catalog'));
  const res = await stub.fetch(new Request('https://do/bump', { method: 'POST' }));
  const body = (await res.json()) as { epoch: number };
  return body.epoch;
}

// ─── Password hashing (PasswordHashDO) ───────────────────────────────────────

/**
 * Raised when the argon2id hashing DO is unreachable, unconfigured, or returns a
 * non-2xx response. Callers MUST surface this as a 503 (service unavailable) and
 * MUST NOT treat it as a wrong-password — there is no weak fallback. A DO outage
 * must never silently accept a bad password nor lock out a valid one.
 */
export class PasswordHashDoError extends Error {
  constructor(message: string) {
    super(message);
    this.name = 'PasswordHashDoError';
  }
}

/** True when the DO binding + gate secret are both present. */
export function isPasswordHashDoAvailable(env: MultidealEnv): boolean {
  return Boolean(env.PASSWORD_HASH_DO && env.HASH_DO_SECRET);
}

function getPasswordHashStub(env: MultidealEnv, userId: string) {
  if (!env.PASSWORD_HASH_DO || !env.HASH_DO_SECRET) {
    throw new PasswordHashDoError('PasswordHashDO binding or HASH_DO_SECRET missing');
  }
  // idFromName(userId): distribute load across users; serial per-user is a free
  // per-user rate-limit on the (expensive) argon2id derivation.
  return {
    stub: env.PASSWORD_HASH_DO.get(env.PASSWORD_HASH_DO.idFromName(userId)),
    secret: env.HASH_DO_SECRET,
  };
}

/**
 * Hash a password via PasswordHashDO. Returns the PHC-style argon2id hash string
 * (`argon2id$v=19$m=...,t=...,p=...$<saltB64>$<hmacB64>`). THROWS PasswordHashDoError
 * on any DO failure — never returns a fallback hash.
 */
export async function hashPasswordViaDO(
  env: MultidealEnv,
  userId: string,
  password: string,
): Promise<string> {
  const { stub, secret } = getPasswordHashStub(env, userId);
  let res: Response;
  try {
    res = await stub.fetch(
      new Request('https://do/hash', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', 'x-md-hash-do': secret },
        body: JSON.stringify({ userId, password }),
      }),
    );
  } catch (err) {
    // Transport error / DO unreachable. Surface as outage (503), never a fallback.
    // captureCaught scrubs PII; the transport error carries no secret/password.
    captureCaught(err, { scope: 'do-client.hashPasswordViaDO.fetch' });
    throw new PasswordHashDoError('PasswordHashDO /hash transport error');
  }
  if (!res.ok) {
    throw new PasswordHashDoError(`PasswordHashDO /hash returned ${res.status}`);
  }
  let body: { hash?: unknown };
  try {
    body = (await res.json()) as { hash?: unknown };
  } catch (err) {
    // Malformed / non-JSON response body. Treat as outage, never a fallback.
    captureCaught(err, { scope: 'do-client.hashPasswordViaDO.parse' });
    throw new PasswordHashDoError('PasswordHashDO /hash returned unparseable body');
  }
  if (typeof body.hash !== 'string') {
    throw new PasswordHashDoError('PasswordHashDO /hash returned no hash');
  }
  return body.hash;
}

/**
 * Verify a password against a stored argon2id PHC hash via PasswordHashDO.
 * THROWS PasswordHashDoError on any DO failure — never coerces an outage into
 * `false` (which would reject valid users) nor `true`.
 */
export async function verifyPasswordViaDO(
  env: MultidealEnv,
  userId: string,
  password: string,
  stored: string,
): Promise<boolean> {
  const { stub, secret } = getPasswordHashStub(env, userId);
  let res: Response;
  try {
    res = await stub.fetch(
      new Request('https://do/verify', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', 'x-md-hash-do': secret },
        body: JSON.stringify({ userId, password, stored }),
      }),
    );
  } catch (err) {
    // Transport error / DO unreachable. Surface as outage (503); never coerce to
    // false (rejects valid users / triggers lockout) nor true.
    captureCaught(err, { scope: 'do-client.verifyPasswordViaDO.fetch' });
    throw new PasswordHashDoError('PasswordHashDO /verify transport error');
  }
  if (!res.ok) {
    throw new PasswordHashDoError(`PasswordHashDO /verify returned ${res.status}`);
  }
  let body: { ok?: unknown };
  try {
    body = (await res.json()) as { ok?: unknown };
  } catch (err) {
    // Malformed / non-JSON response body. Treat as outage, never a verdict.
    captureCaught(err, { scope: 'do-client.verifyPasswordViaDO.parse' });
    throw new PasswordHashDoError('PasswordHashDO /verify returned unparseable body');
  }
  if (typeof body.ok !== 'boolean') {
    throw new PasswordHashDoError('PasswordHashDO /verify returned no ok flag');
  }
  return body.ok;
}
