// Copyright (c) 2026 Hellmai Ltd
// SPDX-License-Identifier: AGPL-3.0-only

/**
 * WU-2731 (INIT-060 phase 3, ADR-013 §5 Identity):
 * Pure inbound-channel ingress sanitizer.
 *
 * ADR-013 §5 rules, enforced here:
 *
 * - The authoritative `from` value for an inbound phone POST comes from the
 *   authenticated token subject (`{workspace_id}:phone:{device_id}`). Cloud
 *   MUST NOT trust a `from` field supplied in the request body.
 *
 * - Body-supplied `from` (and any alias under `metadata.from`) is stripped
 *   before the envelope reaches domain code. The surface that calls this
 *   module is expected to pass the resolved `from` separately; attempting to
 *   ingest a body-only envelope (no authoritative from) fails closed.
 *
 * The module is HTTP-independent: both the kernel tool-api surface and the
 * future control-plane adapter (WU-2737) call the same sanitizer so the
 * ignore-body-from rule is enforced in one place.
 */

const ENVELOPE_BODY_FIELD_FROM = 'from' as const;
const ENVELOPE_METADATA_KEY = 'metadata' as const;
const ENVELOPE_FROM_SOURCE_TOKEN = 'token' as const;

export interface PhoneChannelIngressInput {
  /**
   * Raw request body as parsed from JSON. Opaque record; the sanitizer does
   * not inspect `body`, only the `from` attribution fields.
   */
  body: Readonly<Record<string, unknown>>;
  /**
   * Authoritative identity claim resolved from the bearer token. Required:
   * without a token subject, cloud cannot attribute the message, and the
   * ingress path rejects the request.
   */
  authoritativeFrom: string;
}

export interface PhoneChannelIngressEnvelope {
  /**
   * Sanitised request body with any body-level `from` stripped. Everything
   * else the caller supplied is preserved so downstream code (channel.send,
   * routing) can consume arbitrary payload fields.
   */
  body: Record<string, unknown>;
  /** Authoritative attribution string (`{workspace_id}:phone:{device_id}`). */
  from: string;
  /** Always `'token'` — cloud-safe invariant per ADR-013 §5. */
  from_source: typeof ENVELOPE_FROM_SOURCE_TOKEN;
}

export class PhoneChannelIngressError extends Error {
  readonly statusCode: number;

  constructor(message: string, statusCode: number) {
    super(message);
    this.name = 'PhoneChannelIngressError';
    this.statusCode = statusCode;
  }
}

/**
 * Sanitize an inbound phone-channel request. Returns a cleaned envelope with
 * the authoritative `from` baked in. Throws `PhoneChannelIngressError` (HTTP
 * 400) when the authoritative `from` is missing — the surface translates the
 * error to a 400/403 response.
 */
export function sanitizePhoneChannelIngress(
  input: PhoneChannelIngressInput,
): PhoneChannelIngressEnvelope {
  if (typeof input.authoritativeFrom !== 'string' || input.authoritativeFrom.length === 0) {
    throw new PhoneChannelIngressError(
      'Inbound phone channel requires authoritative from (token subject); body-only attribution is rejected.',
      400,
    );
  }

  const sanitizedBody = stripKey(input.body, ENVELOPE_BODY_FIELD_FROM);
  const rawMetadata = sanitizedBody[ENVELOPE_METADATA_KEY];
  if (
    rawMetadata !== null &&
    typeof rawMetadata === 'object' &&
    !Array.isArray(rawMetadata) &&
    ENVELOPE_BODY_FIELD_FROM in (rawMetadata as Record<string, unknown>)
  ) {
    sanitizedBody[ENVELOPE_METADATA_KEY] = stripKey(
      rawMetadata as Record<string, unknown>,
      ENVELOPE_BODY_FIELD_FROM,
    );
  }

  return {
    body: sanitizedBody,
    from: input.authoritativeFrom,
    from_source: ENVELOPE_FROM_SOURCE_TOKEN,
  };
}

/**
 * Copy `record` omitting the given key. Implemented with a filtered
 * `Object.entries` rather than `delete` to satisfy the linter's ban on
 * dynamic-delete (which would otherwise be a no-op for literal keys but
 * fails the rule uniformly).
 */
function stripKey(record: Readonly<Record<string, unknown>>, key: string): Record<string, unknown> {
  const result: Record<string, unknown> = {};
  for (const [entryKey, entryValue] of Object.entries(record)) {
    if (entryKey !== key) {
      result[entryKey] = entryValue;
    }
  }
  return result;
}

/**
 * Convenience: reports whether the given `from` subject matches the
 * phone-device grammar (`{workspace_id}:phone:{device_id}`). Useful to
 * tell workspace-scoped actors from phone-device actors in the audit log
 * without reaching for the enrollment parser.
 */
export function isPhoneSubject(subject: string): boolean {
  const parts = subject.split(':');
  if (parts.length !== 3) {
    return false;
  }
  const [workspace, kind, device] = parts;
  return (
    kind === 'phone' &&
    typeof workspace === 'string' &&
    workspace.length > 0 &&
    typeof device === 'string' &&
    device.length > 0
  );
}
