/**
 * Shared contact-form relay wiring for HTML export.
 *
 * Any form component that wants to deliver submissions by email (ContactForm,
 * FormContainer, ...) emits the SAME marker/placeholder/honeypot shape so the
 * WHP publish step (`fs_rewrite_contact_forms`) can provision a token, rewrite
 * the action, and strip the marker. Keeping this in one place means the two
 * components can't drift apart (a drift would leak the recipient into published
 * HTML — see PR #47 review).
 */

import { escapeAttr, safeUrl, scopeId } from './escape';

/**
 * Where a form's submissions go. Optional on every call site: a form that
 * passes nothing here (or `type: 'email'`) emits the LEGACY marker, byte for
 * byte -- see `relayFormWiring` below.
 */
export interface FormDestination {
  /** 'webhook' (case-insensitive) selects the webhook path; anything else = email. */
  type?: string;
  /** Absolute https URL the relay POSTs to. Validated at publish time. */
  url?: string;
  /** Opaque id minted by /api/form-webhook-secret.php. NEVER the raw secret. */
  secretId?: string;
  /** 'bearer' or 'signature' (HMAC, the default). */
  authMode?: string;
}

/** The two destination types this emitter knows how to describe. */
const DESTINATION_TYPES = ['email', 'webhook'] as const;
/** The two auth modes the relay implements (FormRelayProvisioner::upsertWebhookToken). */
const AUTH_MODES = ['signature', 'bearer'] as const;

/**
 * Allowlist a destination type / auth mode rather than escaping it.
 *
 * Same reasoning as `sanitizeInputType`/`sanitizeFormMethod` in ./escape: these
 * props are declared as unions in TS but arrive raw from a deserialized saved
 * state or the AI `update_props` path, and the only legitimate values are a
 * fixed pair. Narrowing here also stops a case-drifted `"Bearer"` from being
 * silently downgraded to `signature` by the publish step (which compares
 * `=== 'bearer'` exactly) -- the customer would see unsigned deliveries with
 * nothing in the UI to explain it.
 */
function allowlist<T extends string>(value: unknown, allowed: readonly T[], fallback: T): T {
  const v = (typeof value === 'string' ? value : '').trim().toLowerCase();
  return (allowed as readonly string[]).includes(v) ? (v as T) : fallback;
}

/**
 * Emit one ` name="value"` marker attribute.
 *
 * THE SINGLE ESCAPING SITE for every optional marker attribute. The publish-time
 * strip (whp: web-files/libs/FormRelayRewrite.php) has been hardened six times
 * over exactly this: an unescaped `<`, `>` or a literal `-->` inside a marker
 * attribute value truncates the strip mid-marker and leaks the customer's
 * recipient address into their public page source, or trips the post-condition
 * and refuses the publish outright. `escapeAttr` removing `"` is also what keeps
 * each value inside the `[^"]*` the publish-side parser expects.
 */
function markerAttr(name: string, value: string): string {
  return ` ${name}="${escapeAttr(value)}"`;
}

export interface RelayWiring {
  /** true when a recipient is set (relay path); false = legacy formAction fallback */
  useRelay: boolean;
  /** the `<!--WHP-FORM ...-->` marker (stripped at publish); '' when not relay */
  marker: string;
  /** value for the form's `action` attribute (placeholder when relay, else the escaped fallback) */
  actionAttr: string;
  /** hidden honeypot `<input>` to render as the form's first child; '' when not relay */
  honeypot: string;
}

/**
 * @param recipientEmail the "Send submissions to" address (empty/undefined = no relay)
 * @param thankYouUrl    optional post-submit redirect (blank = hosted thank-you page)
 * @param fallbackAction the form's existing action to use when no recipient is set
 * @param nodeId         the Craft node id of the calling form component, used to scope
 *                        the marker/placeholder id deterministically and uniquely --
 *                        see `scopeId` in ./escape. Falls back to a stable hash of the
 *                        recipient/thankYouUrl/fallbackAction when omitted (never random).
 * @param destination    optional destination descriptor. Omitted, or `type: 'email'`,
 *                        yields the LEGACY narrow marker byte for byte -- every
 *                        already-published site depends on that shape continuing to
 *                        provision an email endpoint.
 *
 * The wide (webhook) marker keeps the optional attributes BETWEEN `id` and
 * `recipient`, which is where the publish-side parser looks for them:
 *
 *   /<!--WHP-FORM id="([^"]+)"((?: [a-z]+="[^"]*")*) recipient="([^"]*)" thankyou="([^"]*)"-->/
 *
 * (FormRelayRewrite.php). Attribute NAMES must therefore be lowercase, and every
 * VALUE must be free of `"` -- both guaranteed here, the latter by `markerAttr`.
 *
 * A webhook marker is emitted whenever the customer selected webhook, even with a
 * blank URL: the publish step then refuses that one endpoint and logs it (the form
 * publishes inert). Falling back to the email path instead would deliver mail to a
 * customer who configured a webhook, with nothing anywhere to explain it -- the
 * exact silent degradation the publish-side `type` normalisation exists to stop.
 */
export function relayFormWiring(
  recipientEmail: string | undefined,
  thankYouUrl: string | undefined,
  fallbackAction: string | undefined,
  nodeId?: string,
  destination?: FormDestination,
): RelayWiring {
  const destType = allowlist(destination?.type, DESTINATION_TYPES, 'email');
  const isWebhook = destType === 'webhook';

  // No destination at all: nothing to deliver to, so no relay (unchanged).
  if (!recipientEmail && !isWebhook) {
    return { useRelay: false, marker: '', actionAttr: escapeAttr(safeUrl(fallbackAction || '#')), honeypot: '' };
  }

  // Webhook config participates in the fallback seed so two webhook forms with
  // no node id and no recipient don't collide on one fid. Appended only in the
  // webhook branch, so the legacy seed -- and therefore every legacy fid -- is
  // unchanged.
  const seed = `${recipientEmail || ''}::${thankYouUrl || ''}::${fallbackAction || ''}`
    + (isWebhook ? `::webhook::${destination?.url || ''}::${destination?.secretId || ''}` : '');
  const fid = scopeId(nodeId, seed, 'F');

  // The `url` value is NOT routed through `safeUrl`: it is never a live sink
  // (it lands in an HTML comment that the publish step strips), and blanking it
  // here would silently turn a mistyped destination into an inert form with no
  // log line. The publish step validates it properly -- absolute https, no
  // control characters -- and refuses loudly when it doesn't hold.
  const extraAttrs = isWebhook
    ? markerAttr('type', 'webhook')
      + markerAttr('url', destination?.url || '')
      + markerAttr('secret', destination?.secretId || '')
      + markerAttr('authmode', allowlist(destination?.authMode, AUTH_MODES, 'signature'))
    : '';

  return {
    useRelay: true,
    marker: `<!--WHP-FORM id="${fid}"${extraAttrs} recipient="${escapeAttr(recipientEmail || '')}" thankyou="${escapeAttr(thankYouUrl || '')}"-->`,
    actionAttr: `__WHP_FORM_ACTION__${fid}__`,
    honeypot: `<input type="text" name="_gotcha" tabindex="-1" autocomplete="off" style="position:absolute;left:-9999px" aria-hidden="true">`,
  };
}
