/**
 * PII redactor for the support AI agent.
 *
 * All text handed to Gemini must be passed through `redactForPrompt` first.
 * The redaction map is the single chokepoint — only `redactor.ts` touches raw PII.
 */

export interface UserPII {
  id: string;
  email?: string | null;
  phone?: string | null;
}

export interface VendorPII {
  id: string;
  ownerEmail?: string | null;
  ownerPhone?: string | null;
}

export interface RedactionMap {
  /** Raw PII string → "{user_a}" pseudo-id */
  forward: Map<string, string>;
  /** "{user_a}" → raw PII string (for substituting back into AI output) */
  reverse: Map<string, string>;
}

// ─── Helpers ──────────────────────────────────────────────────────────────────

/** Convert a number to a lowercase alphabetic suffix: 0→a, 1→b, … 25→z, 26→aa, … */
function toAlpha(n: number): string {
  let result = '';
  let num = n;
  do {
    result = String.fromCharCode(97 + (num % 26)) + result;
    num = Math.floor(num / 26) - 1;
  } while (num >= 0);
  return result;
}

/**
 * Normalize a phone number to its raw digit string (strip +, spaces, dashes, parentheses).
 * Used for matching phone literals in text.
 */
function normalizePhone(phone: string): string {
  return phone.replace(/[\s\-().+]/g, '');
}

/**
 * Build a redaction map from a set of user + vendor PII.
 *
 * Pseudo-id assignment is deterministic: sorted by entity id (ascending),
 * users first, then vendors. Each entity gets one pseudo-id that covers both
 * its email and phone (whichever exists), so the same suffix represents the
 * same person across fields.
 */
export function buildRedactionMap(
  users: Map<string, UserPII>,
  vendors: Map<string, VendorPII>,
): RedactionMap {
  const forward = new Map<string, string>();
  const reverse = new Map<string, string>();

  // Sort by id for determinism
  const sortedUsers = [...users.values()].sort((a, b) => a.id.localeCompare(b.id));
  const sortedVendors = [...vendors.values()].sort((a, b) => a.id.localeCompare(b.id));

  let userIdx = 0;
  for (const u of sortedUsers) {
    const pseudoBase = `{user_${toAlpha(userIdx)}}`;
    let added = false;
    if (u.email) {
      forward.set(u.email, pseudoBase);
      if (!added) {
        reverse.set(pseudoBase, u.email);
        added = true;
      }
    }
    if (u.phone) {
      const normalized = normalizePhone(u.phone);
      forward.set(normalized, pseudoBase);
      if (!added) {
        reverse.set(pseudoBase, normalized);
      }
    }
    userIdx++;
  }

  let vendorIdx = 0;
  for (const v of sortedVendors) {
    const pseudoBase = `{vendor_${toAlpha(vendorIdx)}}`;
    let added = false;
    if (v.ownerEmail) {
      forward.set(v.ownerEmail, pseudoBase);
      if (!added) {
        reverse.set(pseudoBase, v.ownerEmail);
        added = true;
      }
    }
    if (v.ownerPhone) {
      const normalized = normalizePhone(v.ownerPhone);
      forward.set(normalized, pseudoBase);
      if (!added) {
        reverse.set(pseudoBase, normalized);
      }
    }
    vendorIdx++;
  }

  return { forward, reverse };
}

/**
 * Redact all known PII from `text`, replacing each occurrence with its pseudo-id.
 *
 * The replacement is:
 * - Emails: matched literally (case-sensitive, as stored).
 * - Phones: the text is first normalized to digits-only before matching,
 *   but we also match the original formatted form by scanning the forward map
 *   for numeric keys and replacing all digit-normalized occurrences in text.
 *
 * Implementation strategy:
 * 1. Sort forward keys longest-first to avoid substring collisions.
 * 2. For email keys: do a global literal replacement.
 * 3. For phone (digit-only) keys: normalize text segments and replace.
 */
export function redactForPrompt(text: string, map: RedactionMap): string {
  if (map.forward.size === 0) return text;

  // Separate email keys from phone (digit-only) keys
  const emailKeys: string[] = [];
  const phoneKeys: string[] = [];

  for (const key of map.forward.keys()) {
    // Phone keys contain only digits (after normalization)
    if (/^\d+$/.test(key)) {
      phoneKeys.push(key);
    } else {
      emailKeys.push(key);
    }
  }

  // Sort longest first to avoid partial matches
  emailKeys.sort((a, b) => b.length - a.length);
  phoneKeys.sort((a, b) => b.length - a.length);

  let result = text;

  // Replace email literals
  for (const email of emailKeys) {
    const pseudoId = map.forward.get(email)!;
    // Escape special regex chars in email
    const escaped = email.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
    result = result.replace(new RegExp(escaped, 'g'), pseudoId);
  }

  // Replace phone numbers: the forward map has digit-only keys (post-normalization).
  // We match these in text by:
  //   1. Building a regex from the digits with optional separators between each digit.
  //   2. Allowing an optional leading `+` before the first digit.
  for (const digits of phoneKeys) {
    const pseudoId = map.forward.get(digits)!;
    // Build pattern: optional leading + then digits with optional separators between each
    const digitParts = digits
      .split('')
      .map((d) => d.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
      .join('[\\s\\-().]*');
    const phoneRegex = new RegExp(`\\+?${digitParts}`, 'g');
    result = result.replace(phoneRegex, pseudoId);
  }

  return result;
}

/**
 * Substitute pseudo-ids back to their original PII values.
 *
 * Used when AI output contains pseudo-ids that need to be de-anonymized
 * before storage or display to privileged users.
 */
export function substitutePseudoIds(text: string, map: RedactionMap): string {
  if (map.reverse.size === 0) return text;

  let result = text;

  for (const [pseudoId, original] of map.reverse.entries()) {
    // Escape curly braces for regex
    const escaped = pseudoId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
    result = result.replace(new RegExp(escaped, 'g'), original);
  }

  return result;
}
