import { Readable, Writable } from 'node:stream';
import { Splitter, Streamer } from '@zone-eu/mailsplit';
import { extractAddress, parseAddressList } from './mailer.js';

export async function parseInboundMessage(rawMessage, envelopeRecipients = []) {
  const sourceBuffer = Buffer.isBuffer(rawMessage) ? rawMessage : Buffer.from(String(rawMessage || ''));
  const source = sourceBuffer.toString('utf8');
  const textParts = await collectTextParts(sourceBuffer);
  const textBody = textParts.find((part) => part.contentType === 'text/plain')?.body || '';
  const htmlBody = textParts.find((part) => part.contentType === 'text/html')?.body || '';
  const recipients = normalizeRecipients(envelopeRecipients);
  const headerRecipients = [
    ...parseAddressList(extractHeader(source, 'to')),
    ...parseAddressList(extractHeader(source, 'cc'))
  ];

  return {
    sender: extractAddress(extractHeader(source, 'from')) || extractAddress(extractHeader(source, 'sender')),
    recipients: recipients.length ? recipients : normalizeRecipients(headerRecipients),
    subject: decodeMimeHeader(extractHeader(source, 'subject')) || '(no subject)',
    messageId: extractHeader(source, 'message-id'),
    rawMessage: source,
    textBody,
    htmlBody,
    preview: previewText(textBody || htmlToText(htmlBody))
  };
}

async function collectTextParts(rawMessage) {
  const sourceBuffer = Buffer.isBuffer(rawMessage) ? rawMessage : Buffer.from(String(rawMessage || ''));
  const source = sourceBuffer.toString('utf8');
  const parts = [];
  const splitter = new Splitter({ ignoreEmbedded: true });
  const streamer = new Streamer((node) => (
    ['text/plain', 'text/html'].includes(node.contentType) && node.disposition !== 'attachment'
  ));
  const drain = new Writable({
    objectMode: true,
    write(_chunk, _encoding, callback) {
      callback();
    }
  });

  streamer.on('node', (data) => {
    const chunks = [];
    data.decoder.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
    data.decoder.on('end', () => {
      parts.push({
        contentType: data.node.contentType,
        body: decodeText(Buffer.concat(chunks), data.node.charset)
      });
      data.done();
    });
    data.decoder.on('error', () => data.done());
  });

  await new Promise((resolve, reject) => {
    drain.on('finish', resolve);
    drain.on('error', reject);
    splitter.on('error', reject);
    streamer.on('error', reject);
    Readable.from([sourceBuffer]).pipe(splitter).pipe(streamer).pipe(drain);
  });

  const topLevelContentType = extractHeader(source, 'content-type').split(';', 1)[0].trim().toLowerCase();
  if (!parts.length && (!topLevelContentType || topLevelContentType === 'text/plain')) {
    const body = source.split(/\r?\n\r?\n/).slice(1).join('\n\n').trim();
    if (body) parts.push({ contentType: 'text/plain', body });
  }

  return parts;
}

function extractHeader(rawMessage, name) {
  const head = rawMessage.split(/\r?\n\r?\n/, 1)[0] || '';
  const lines = head.split(/\r?\n/);
  const headers = [];
  for (const line of lines) {
    if (/^[\t ]/.test(line) && headers.length) {
      headers[headers.length - 1].value += ` ${line.trim()}`;
      continue;
    }
    const index = line.indexOf(':');
    if (index === -1) continue;
    headers.push({
      name: line.slice(0, index).toLowerCase(),
      value: line.slice(index + 1).trim()
    });
  }
  return headers.find((header) => header.name === name.toLowerCase())?.value || '';
}

export function decodeMimeHeader(value) {
  const source = String(value || '');
  const expression = /=\?([^?]+)\?([bq])\?([^?]+)\?=/gi;
  let output = '';
  let cursor = 0;
  let previousWasEncoded = false;

  for (const match of source.matchAll(expression)) {
    const between = source.slice(cursor, match.index);
    const decoded = decodeEncodedWord(match[1], match[2], match[3]);
    if (decoded === null) {
      output += between + match[0];
      previousWasEncoded = false;
    } else {
      output += previousWasEncoded && /^[\t\r\n ]+$/.test(between) ? '' : between;
      output += decoded;
      previousWasEncoded = true;
    }
    cursor = match.index + match[0].length;
  }

  return output + source.slice(cursor);
}

function decodeEncodedWord(charset, encoding, encoded) {
  const buffer = encoding.toLowerCase() === 'b'
    ? decodeBase64Word(encoded)
    : decodeQuotedWord(encoded);
  if (!buffer) return null;
  const normalizedCharset = String(charset || '').trim().toLowerCase() === 'utf8'
    ? 'utf-8'
    : String(charset || '').trim().toLowerCase();
  try {
    return new TextDecoder(normalizedCharset, { fatal: true }).decode(buffer);
  } catch {
    return null;
  }
}

function decodeBase64Word(value) {
  const encoded = String(value || '');
  if (!/^[a-z0-9+/]+={0,2}$/i.test(encoded) || encoded.length % 4 === 1) return null;
  const unpadded = encoded.replace(/=+$/, '');
  const padded = unpadded.padEnd(Math.ceil(unpadded.length / 4) * 4, '=');
  return Buffer.from(padded, 'base64');
}

function decodeQuotedWord(value) {
  const encoded = String(value || '');
  const bytes = [];
  for (let index = 0; index < encoded.length; index += 1) {
    const character = encoded[index];
    if (character === '_') {
      bytes.push(0x20);
      continue;
    }
    if (character === '=') {
      const hex = encoded.slice(index + 1, index + 3);
      if (!/^[a-f0-9]{2}$/i.test(hex)) return null;
      bytes.push(Number.parseInt(hex, 16));
      index += 2;
      continue;
    }
    const code = character.charCodeAt(0);
    if (code < 0x21 || code > 0x7e) return null;
    bytes.push(code);
  }
  return Buffer.from(bytes);
}

function decodeText(buffer, charset) {
  const normalized = String(charset || 'utf-8').trim().toLowerCase();
  const decoded = ['iso-8859-1', 'latin1', 'latin-1'].includes(normalized)
    ? buffer.toString('latin1')
    : buffer.toString('utf8');
  return decoded.trim();
}

function normalizeRecipients(values) {
  return [...new Set((Array.isArray(values) ? values : [values]).map(extractAddress).filter(Boolean))];
}

function previewText(value) {
  return String(value || '').replace(/\s+/g, ' ').trim().slice(0, 240);
}

function htmlToText(value) {
  return String(value || '')
    .replace(/<style[\s\S]*?<\/style>/gi, ' ')
    .replace(/<script[\s\S]*?<\/script>/gi, ' ')
    .replace(/<[^>]+>/g, ' ')
    .replace(/&nbsp;/gi, ' ')
    .replace(/&amp;/gi, '&')
    .replace(/&lt;/gi, '<')
    .replace(/&gt;/gi, '>')
    .replace(/&quot;/gi, '"')
    .replace(/&#39;/g, "'");
}
