import { GetObjectCommand } from '@aws-sdk/client-s3';
import axios from 'axios';
import { randomBytes } from 'crypto';
import { R2Client } from '@/shared/constants/r2service.constant';
import config from '@/shared/config/config';
import { EmailAttachment } from './email.interfaces';

const CRLF = '\r\n';
const MAX_ATTACHMENT_BYTES = 9 * 1024 * 1024; // stay under SES ~10 MB message limit headroom

const R2_VHOST_SUFFIX = '.r2.cloudflarestorage.com';

/** Virtual-hosted: https://{bucket}.{accountId}.r2.cloudflarestorage.com/{key} */
const parseR2VirtualHostedKey = (urlStr: string): string | null => {
  try {
    const u = new URL(urlStr);
    const host = u.hostname.toLowerCase();
    if (!host.endsWith(R2_VHOST_SUFFIX)) return null;
    const without = host.slice(0, -R2_VHOST_SUFFIX.length);
    const firstDot = without.indexOf('.');
    if (firstDot < 1) return null;
    const bucket = without.slice(0, firstDot);
    const accountId = without.slice(firstDot + 1);
    if (bucket !== config.r2.bucketName) return null;
    if (accountId.toLowerCase() !== config.r2.accountId.toLowerCase()) return null;
    const key = decodeURIComponent(u.pathname.replace(/^\/+/, ''));
    return key || null;
  } catch {
    return null;
  }
};

/** Path-style: https://{accountId}.r2.cloudflarestorage.com/{bucket}/{key} */
const parseR2PathStyleKey = (urlStr: string): string | null => {
  try {
    const u = new URL(urlStr);
    const expectedHost = `${config.r2.accountId.toLowerCase()}${R2_VHOST_SUFFIX}`;
    if (u.hostname.toLowerCase() !== expectedHost) return null;
    const segments = u.pathname.split('/').filter(Boolean);
    if (segments[0] !== config.r2.bucketName) return null;
    return decodeURIComponent(segments.slice(1).join('/')) || null;
  } catch {
    return null;
  }
};

/** Public URL under R2_PUBLIC_BASE_URL → object key */
const parseR2PublicBaseKey = (urlStr: string): string | null => {
  const base = config.r2.publicBaseUrl.replace(/\/$/, '');
  if (!urlStr.startsWith(base)) return null;
  const rest = urlStr.slice(base.length).replace(/^\/+/, '');
  return rest ? decodeURIComponent(rest) : null;
};

const fetchR2ObjectByKey = async (
  key: string,
): Promise<{ data: Buffer; contentType?: string }> => {
  const response = await R2Client.send(
    new GetObjectCommand({
      Bucket: config.r2.bucketName,
      Key: key,
    }),
  );
  if (!response.Body) {
    throw new Error(`R2 object has empty body: ${key}`);
  }
  const byteArray = await response.Body.transformToByteArray();
  const data = Buffer.from(byteArray);
  if (data.length > MAX_ATTACHMENT_BYTES) {
    throw new Error(`R2 object exceeds size limit: ${key}`);
  }
  return {
    data,
    contentType: response.ContentType?.split(';')[0]?.trim(),
  };
};

/**
 * Load bytes for an attachment URL. R2 URLs are fetched with API credentials
 * (anonymous HTTP GET to r2.cloudflarestorage.com often returns 400 for private buckets).
 */
const fetchAttachmentUrlBytes = async (
  urlStr: string,
): Promise<{ data: Buffer; contentType?: string }> => {
  const r2Key =
    parseR2VirtualHostedKey(urlStr) ||
    parseR2PathStyleKey(urlStr) ||
    parseR2PublicBaseKey(urlStr);
  if (r2Key) {
    return fetchR2ObjectByKey(r2Key);
  }

  const res = await axios.get<ArrayBuffer>(urlStr, {
    responseType: 'arraybuffer',
    maxContentLength: MAX_ATTACHMENT_BYTES,
    maxBodyLength: MAX_ATTACHMENT_BYTES,
    validateStatus: (s) => s >= 200 && s < 300,
    headers: {
      Accept: '*/*',
      'Accept-Encoding': 'identity',
    },
  });
  return {
    data: Buffer.from(res.data),
    contentType: res.headers['content-type']?.split(';')[0]?.trim(),
  };
};

export type SesRawMimeOptions = {
  fromName?: string;
  fromEmail: string;
  to: string[];
  cc?: string[];
  subject: string;
  htmlContent?: string;
  textContent?: string;
  attachments: EmailAttachment[];
};

const guessMimeFromFilename = (name: string): string => {
  const ext = name.split('.').pop()?.toLowerCase();
  const map: Record<string, string> = {
    pdf: 'application/pdf',
    png: 'image/png',
    jpg: 'image/jpeg',
    jpeg: 'image/jpeg',
    gif: 'image/gif',
    webp: 'image/webp',
    doc: 'application/msword',
    docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
    xls: 'application/vnd.ms-excel',
    xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
    zip: 'application/zip',
    txt: 'text/plain',
  };
  return (ext && map[ext]) || 'application/octet-stream';
};

const chunkBase64 = (b64: string): string =>
  (b64.match(/.{1,76}/g) ?? [b64]).join(CRLF);

const encodeSubject = (subject: string): string => {
  if (/^[\x20-\x7E]*$/.test(subject)) return subject;
  return `=?UTF-8?B?${Buffer.from(subject, 'utf8').toString('base64')}?=`;
};

const formatFrom = (name: string | undefined, email: string): string => {
  if (!name?.trim()) return email;
  const n = name.trim();
  if (/^[\x20-\x7E]*$/.test(n)) {
    const escaped = n.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
    return `"${escaped}" <${email}>`;
  }
  return `=?UTF-8?B?${Buffer.from(n, 'utf8').toString('base64')}?= <${email}>`;
};

const sanitizeFilename = (name: string): string => name.replace(/[\r\n"]/g, '_');

const resolveAttachment = async (
  att: EmailAttachment,
): Promise<{ filename: string; contentType: string; data: Buffer }> => {
  const filename = att.name || 'attachment';
  let contentType = att.contentType;
  let data: Buffer;

  if (att.url) {
    const fetched = await fetchAttachmentUrlBytes(att.url);
    data = fetched.data;
    if (!contentType && fetched.contentType) contentType = fetched.contentType;
  } else if (att.content !== undefined) {
    data = Buffer.isBuffer(att.content)
      ? att.content
      : Buffer.from(att.content, 'utf8');
  } else {
    throw new Error(`Attachment "${filename}" needs url or content`);
  }

  if (!contentType) contentType = guessMimeFromFilename(filename);

  if (data.length > MAX_ATTACHMENT_BYTES) {
    throw new Error(`Attachment "${filename}" exceeds size limit`);
  }

  return { filename, contentType, data };
};

/**
 * Build an RFC 5322 / MIME multipart message for SES SendRawEmail (mixed: alternative body + attachments).
 */
export const buildRawSesMimeMessage = async (
  opts: SesRawMimeOptions,
): Promise<Buffer> => {
  const resolved = await Promise.all(opts.attachments.map(resolveAttachment));

  const mixedBoundary = `mix_${randomBytes(12).toString('hex')}`;
  const altBoundary = `alt_${randomBytes(12).toString('hex')}`;

  const headerLines: string[] = [
    `From: ${formatFrom(opts.fromName, opts.fromEmail)}`,
    `To: ${opts.to.join(', ')}`,
  ];
  if (opts.cc?.length) headerLines.push(`Cc: ${opts.cc.join(', ')}`);
  headerLines.push(
    `Subject: ${encodeSubject(opts.subject)}`,
    'MIME-Version: 1.0',
    `Content-Type: multipart/mixed; boundary="${mixedBoundary}"`,
  );

  let body = '';

  body += `--${mixedBoundary}${CRLF}`;
  body += `Content-Type: multipart/alternative; boundary="${altBoundary}"${CRLF}${CRLF}`;

  if (opts.textContent) {
    body += `--${altBoundary}${CRLF}`;
    body += `Content-Type: text/plain; charset=UTF-8${CRLF}`;
    body += `Content-Transfer-Encoding: base64${CRLF}${CRLF}`;
    body += `${chunkBase64(Buffer.from(opts.textContent, 'utf8').toString('base64'))}${CRLF}`;
  }
  if (opts.htmlContent) {
    body += `--${altBoundary}${CRLF}`;
    body += `Content-Type: text/html; charset=UTF-8${CRLF}`;
    body += `Content-Transfer-Encoding: base64${CRLF}${CRLF}`;
    body += `${chunkBase64(Buffer.from(opts.htmlContent, 'utf8').toString('base64'))}${CRLF}`;
  }
  if (!opts.textContent && !opts.htmlContent) {
    body += `--${altBoundary}${CRLF}`;
    body += `Content-Type: text/plain; charset=UTF-8${CRLF}`;
    body += `Content-Transfer-Encoding: base64${CRLF}${CRLF}`;
    body += `${chunkBase64(Buffer.from('', 'utf8').toString('base64'))}${CRLF}`;
  }

  body += `--${altBoundary}--${CRLF}`;

  for (const att of resolved) {
    const safe = sanitizeFilename(att.filename);
    body += `--${mixedBoundary}${CRLF}`;
    body += `Content-Type: ${att.contentType}; name="${safe}"${CRLF}`;
    body += `Content-Disposition: attachment; filename="${safe}"${CRLF}`;
    body += `Content-Transfer-Encoding: base64${CRLF}${CRLF}`;
    body += `${chunkBase64(att.data.toString('base64'))}${CRLF}`;
  }

  body += `--${mixedBoundary}--${CRLF}`;

  const raw = headerLines.join(CRLF) + CRLF + CRLF + body;
  return Buffer.from(raw, 'utf8');
};
