/**
 * EML/MIME evidence parser for the phishing-triage pipeline (Phase 16).
 *
 * Turns a raw RFC822/MIME `.eml` buffer (an attacker-controlled email a
 * user reported as phishing/spam) into a normalized, structured
 * `NormalizedMessage` — headers, structured SPF/DKIM/DMARC verdicts,
 * URLs, and attachment metadata — using `mailparser` for MIME parsing
 * and a small hand-rolled RFC 8601 tokenizer for Authentication-Results.
 *
 * Hard invariant (SC#3 / EVID-04 / T-16-03): this module must never fetch
 * or execute anything found in a message. It never dereferences an
 * extracted URL, never renders `mail.html`, and never performs any
 * outbound network call while parsing. This is test-enforced with a
 * `global.fetch` spy in eml-parser.test.ts.
 *
 * Also implements `selectOriginalMessage` (EVID-02) — a pure, I/O-free
 * decision over ticket attachment metadata already in hand, choosing the
 * originally-reported message from a ticket's attachment list.
 */

import { simpleParser } from 'mailparser';
import type { AddressObject, HeaderValue, ParsedMail } from 'mailparser';
import { linkifyit } from 'linkify-it';
import type { Attachment } from '@/lib/types/autotask';

/** Basename of an attachment's filename, lowercased, for tier matching. */
function attachmentName(att: Attachment): string {
  const raw = att.fullPath || att.title || '';
  const base = raw.split('/').pop() || raw;
  return base.toLowerCase();
}

function isMessageRfc822(att: Attachment): boolean {
  return (att.contentType || '').toLowerCase() === 'message/rfc822';
}

/**
 * Three-tier `.eml` attachment selection, empirically validated against 15
 * real phishing tickets (see 16-RESEARCH.md Pitfall 1):
 *
 *   1. An attachment named exactly `rfc.eml` (case-insensitive) among
 *      `message/rfc822` attachments — the Microsoft "Report Message" flow.
 *   2. Else, among `message/rfc822` attachments, exclude any named exactly
 *      `OriginatingEmail.eml` (case-insensitive) — if exactly one candidate
 *      remains, select it (covers KnowBe4's versioned filenames, e.g.
 *      `phish_alert_sp2_2.0.0.0.eml`).
 *   3. Else (0 or 2+ ambiguous candidates after step 2) — fall back to
 *      `OriginatingEmail.eml` if present; otherwise return null.
 */
export function selectOriginalMessage(attachments: Attachment[]): Attachment | null {
  const rfc822Attachments = attachments.filter(isMessageRfc822);

  const exactRfcEml = rfc822Attachments.find((att) => attachmentName(att) === 'rfc.eml');
  if (exactRfcEml) return exactRfcEml;

  const nonOriginatingCandidates = rfc822Attachments.filter(
    (att) => attachmentName(att) !== 'originatingemail.eml'
  );
  if (nonOriginatingCandidates.length === 1) return nonOriginatingCandidates[0];

  const originatingFallback = rfc822Attachments.find(
    (att) => attachmentName(att) === 'originatingemail.eml'
  );
  return originatingFallback ?? null;
}

// ---------------------------------------------------------------------------
// parseEml + auth-results + URLs + body preview (EVID-03, EVID-04, D-06)
// ---------------------------------------------------------------------------

/** Hard cap on bytes this module will hand to `simpleParser`. Below B2's 25 MB cap (T-16-01). */
export const MAX_EML_BYTES = 10 * 1024 * 1024; // 10 MB

/** Max length of the derived plain-text body preview (EVID-04). */
const MAX_BODY_PREVIEW_LENGTH = 500;

export type AuthVerdict = 'pass' | 'fail' | 'softfail' | 'neutral' | 'none' | 'temperror' | 'permerror';

export interface AuthResults {
  spf?: AuthVerdict;
  dkim?: AuthVerdict;
  dmarc?: AuthVerdict;
}

export interface AttachmentMeta {
  filename: string | null;
  contentType: string | null;
  size: number;
  checksum: string | null;
  related: boolean;
}

export interface NormalizedMessage {
  from: { displayName: string | null; email: string | null; domain: string | null };
  replyTo: string | null;
  returnPath: string | null;
  to: string[];
  cc: string[];
  subject: string | null;
  date: string | null; // ISO 8601 or null
  messageId: string | null;
  receivedChain: string[]; // ordered, outermost-first as encountered in headerLines
  authResults: AuthResults; // parsed from the primary Authentication-Results header
  authResultsOriginal: AuthResults | null; // parsed from Authentication-Results-Original if present
  urls: string[]; // deduped, from text + html parts
  attachments: AttachmentMeta[]; // includes inline/related, distinguished by `related`
  bodyPreview: string; // truncated plain text, distinct from raw body
}

/**
 * Hand-rolled RFC 8601 Authentication-Results tokenizer (D-06). Deliberately
 * NOT delegated to `mailauth` — that package's public API performs live
 * DNS/HTTP verification (SC#3 violation); this only re-reads the verdict a
 * receiving mail server already stamped on the header.
 *
 * Grammar (simplified): `authserv-id; method1=result1 (comment); method2=result2 ...`
 */
export function parseAuthResults(headerValue: string): AuthResults {
  const result: AuthResults = {};
  for (const clause of headerValue.split(';')) {
    const match = clause.trim().match(/^(spf|dkim|dmarc)=(\w+)/i);
    if (!match) continue;
    const method = match[1].toLowerCase() as 'spf' | 'dkim' | 'dmarc';
    const verdict = match[2].toLowerCase() as AuthVerdict;
    result[method] = verdict;
  }
  return result;
}

// fuzzyLink enables scheme-less `www.`-prefixed URL detection (phishing URLs
// frequently omit the scheme). Never used to fetch/dereference — string
// matching only.
const linkify = linkifyit({ fuzzyLink: true });

/**
 * Extracts and dedupes http(s)/www URLs found in the text and html parts of
 * a message. Never fetches or dereferences any extracted URL (SC#3 / T-16-03)
 * — this is pure string matching via `linkify-it`.
 */
export function extractUrls(
  text: string | null | undefined,
  html: string | null | undefined
): string[] {
  const urls = new Set<string>();
  for (const source of [text, html]) {
    if (!source) continue;
    const matches = linkify.match(source) ?? [];
    for (const match of matches) {
      if (match.schema === 'mailto:') continue;
      urls.add(match.url);
    }
  }
  return Array.from(urls);
}

/** Strips HTML tags/entities down to plain text. Never renders/executes the HTML. */
function stripHtmlToText(html: string): string {
  return html
    .replace(/<(script|style)[^>]*>[\s\S]*?<\/\1>/gi, ' ')
    .replace(/<[^>]+>/g, ' ')
    .replace(/&nbsp;/gi, ' ')
    .replace(/&amp;/gi, '&')
    .replace(/&lt;/gi, '<')
    .replace(/&gt;/gi, '>')
    .replace(/&quot;/gi, '"')
    .replace(/&#39;/gi, "'")
    .replace(/\s+/g, ' ')
    .trim();
}

/**
 * Builds a sanitized, truncated plain-text body preview — prefers the
 * already-safe `mail.text`, falling back to a stripped version of
 * `mail.html` when no plain-text part exists. Never returns raw HTML.
 */
export function buildBodyPreview(
  text: string | null | undefined,
  html: string | null | undefined
): string {
  const source = text && text.trim() ? text : html ? stripHtmlToText(html) : '';
  const normalized = source.replace(/\s+/g, ' ').trim();
  if (normalized.length <= MAX_BODY_PREVIEW_LENGTH) return normalized;
  return normalized.slice(0, MAX_BODY_PREVIEW_LENGTH).trimEnd() + '…';
}

/** Extracts a single email address string from a mailparser HeaderValue (used for Return-Path). */
function headerValueToAddress(value: HeaderValue | undefined): string | null {
  if (!value) return null;
  if (typeof value === 'string') {
    const cleaned = value.replace(/^</, '').replace(/>$/, '').trim();
    return cleaned || null;
  }
  if (Array.isArray(value)) return null;
  if (typeof value === 'object' && 'value' in value) {
    const addressObject = value as AddressObject;
    return addressObject.value?.[0]?.address ?? null;
  }
  return null;
}

/** Flattens one or more mailparser AddressObjects into a list of email address strings. */
function addressListToStrings(addr: AddressObject | AddressObject[] | undefined): string[] {
  if (!addr) return [];
  const objects = Array.isArray(addr) ? addr : [addr];
  const addresses: string[] = [];
  for (const obj of objects) {
    for (const entry of obj.value ?? []) {
      if (entry.address) addresses.push(entry.address);
    }
  }
  return addresses;
}

/** Returns the raw header text (post-colon) for the first headerLine matching `key`, or null. */
function findHeaderLineValue(mail: ParsedMail, key: string): string | null {
  const line = mail.headerLines.find((candidate) => candidate.key === key);
  if (!line) return null;
  const colonIndex = line.line.indexOf(':');
  return (colonIndex === -1 ? line.line : line.line.slice(colonIndex + 1)).trim();
}

/**
 * Parses a raw RFC822/MIME `.eml` buffer into a normalized, structured
 * `NormalizedMessage`. Enforces `MAX_EML_BYTES` BEFORE calling `simpleParser`
 * (DoS mitigation, T-16-01) and never performs any network I/O (SC#3).
 */
export async function parseEml(rawEmlBuffer: Buffer): Promise<NormalizedMessage> {
  if (rawEmlBuffer.byteLength > MAX_EML_BYTES) {
    throw new Error(
      `[EML-PARSER] Buffer size ${rawEmlBuffer.byteLength} exceeds MAX_EML_BYTES (${MAX_EML_BYTES}); refusing to parse`
    );
  }

  let mail: ParsedMail;
  try {
    mail = await simpleParser(rawEmlBuffer, { checksumAlgo: 'sha256' });
  } catch (error) {
    console.error('[EML-PARSER] Failed to parse .eml buffer', error);
    throw error;
  }

  const fromEntry = mail.from?.value?.[0];
  const fromEmail = fromEntry?.address ?? null;
  const fromDomain = fromEmail?.includes('@') ? fromEmail.split('@')[1] ?? null : null;

  const receivedChain: string[] = [];
  for (const line of mail.headerLines) {
    if (line.key === 'received') receivedChain.push(line.line);
  }

  const primaryAuthResultsHeader = findHeaderLineValue(mail, 'authentication-results');
  const originalAuthResultsHeader = findHeaderLineValue(mail, 'authentication-results-original');

  return {
    from: {
      displayName: fromEntry?.name || null,
      email: fromEmail,
      domain: fromDomain,
    },
    replyTo: mail.replyTo?.value?.[0]?.address ?? null,
    returnPath: headerValueToAddress(mail.headers.get('return-path')),
    to: addressListToStrings(mail.to),
    cc: addressListToStrings(mail.cc),
    subject: mail.subject ?? null,
    date: mail.date ? mail.date.toISOString() : null,
    messageId: mail.messageId ?? null,
    receivedChain,
    authResults: primaryAuthResultsHeader ? parseAuthResults(primaryAuthResultsHeader) : {},
    authResultsOriginal: originalAuthResultsHeader
      ? parseAuthResults(originalAuthResultsHeader)
      : null,
    urls: extractUrls(mail.text, mail.html || null),
    attachments: mail.attachments.map((att) => ({
      filename: att.filename ?? null,
      contentType: att.contentType ?? null,
      size: att.size,
      checksum: att.checksum ?? null,
      related: Boolean(att.related),
    })),
    bodyPreview: buildBodyPreview(mail.text, mail.html || null),
  };
}
