'use strict';
//
// Validation + parsing for chat attachments. Frontend POSTs base64 data URLs
// in `body.attachments`; we parse, enforce caps, and produce in-memory
// ParsedAttachment objects suitable for both LLM content-block construction
// and disk persistence.
//
// Phase 1 scope: image only (image/png, image/jpeg, image/gif, image/webp).
// Phase 2 adds application/pdf. Phase 4 adds audio.

const PHASE1_ALLOWED = new Set([
  'image/png', 'image/jpeg', 'image/gif', 'image/webp',
]);

const PHASE2_ALLOWED = new Set([
  'image/png', 'image/jpeg', 'image/gif', 'image/webp',
  'application/pdf',
]);

const ALL_ALLOWED = new Set([
  'image/png', 'image/jpeg', 'image/gif', 'image/webp',
  'application/pdf',
  'audio/wav', 'audio/mp3', 'audio/mpeg', 'audio/m4a', 'audio/mp4', 'audio/webm', 'audio/ogg',
]);

const CAPS = Object.freeze({
  perImageBytes: 5 * 1024 * 1024,    // 5MB binary
  perPdfBytes: 32 * 1024 * 1024,     // 32MB (Anthropic doc cap)
  perAudioBytes: 25 * 1024 * 1024,   // 25MB
  totalBytes: 50 * 1024 * 1024,      // 50MB per turn
  maxCount: 10,                       // 10 attachments per turn
});

class AttachmentError extends Error {
  constructor(message, { code = 'INVALID_ATTACHMENT', status = 400 } = {}) {
    super(message);
    this.code = code;
    this.status = status;
  }
}

/**
 * Parse a `data:<mediaType>;base64,<payload>` URL into its parts. Returns
 * null if the input doesn't look like a base64 data URL.
 */
function parseDataUrl(dataUrl) {
  if (typeof dataUrl !== 'string') return null;
  const m = dataUrl.match(/^data:([a-zA-Z][a-zA-Z0-9!#$&^_+\-./]*?);base64,(.*)$/);
  if (!m) return null;
  return { mediaType: m[1].toLowerCase(), base64: m[2] };
}

function base64DecodedSize(b64) {
  if (!b64) return 0;
  const padding = (b64.endsWith('==') ? 2 : b64.endsWith('=') ? 1 : 0);
  return Math.floor((b64.length * 3) / 4) - padding;
}

/**
 * Validate + parse a raw attachment array from request body. Returns an
 * array of `ParsedAttachment` objects:
 *   { type: 'image'|'pdf'|'audio', name, mediaType, base64, byteSize }
 * Throws `AttachmentError` (with `.code` + `.status`) on any violation.
 *
 * @param {Array} raw - the user-submitted attachments array
 * @param {{ allowed?: Set<string> }} [opts] - phase gate; default = Phase 1 (images only)
 */
function parseAndValidateAttachments(raw, opts = {}) {
  if (raw == null) return [];
  if (!Array.isArray(raw)) {
    throw new AttachmentError('attachments must be an array', { code: 'INVALID_SHAPE' });
  }
  if (raw.length === 0) return [];
  if (raw.length > CAPS.maxCount) {
    throw new AttachmentError(
      `too many attachments (${raw.length} > ${CAPS.maxCount} max per turn)`,
      { code: 'TOO_MANY', status: 413 }
    );
  }

  const allowed = opts.allowed || PHASE1_ALLOWED;
  let totalBytes = 0;
  const parsed = [];

  for (let i = 0; i < raw.length; i++) {
    const a = raw[i];
    if (!a || typeof a !== 'object') {
      throw new AttachmentError(`attachment[${i}] is not an object`, { code: 'INVALID_SHAPE' });
    }
    const declaredType = a.type;
    const name = typeof a.name === 'string' ? a.name : '';
    const data = typeof a.data === 'string' ? a.data : '';

    const dataUrl = parseDataUrl(data);
    if (!dataUrl) {
      throw new AttachmentError(
        `attachment[${i}] data must be a base64 data URL (got ${data ? data.slice(0, 32) + '...' : 'empty'})`,
        { code: 'INVALID_DATA_URL' }
      );
    }
    const { mediaType, base64 } = dataUrl;

    if (!allowed.has(mediaType)) {
      throw new AttachmentError(
        `attachment[${i}] media type "${mediaType}" not allowed in this scope`,
        { code: 'UNSUPPORTED_MEDIA_TYPE', status: 415 }
      );
    }

    // Map declared type → kind. Trust the actual mediaType, fall back when
    // declaredType is missing or wrong.
    const kind = mediaType.startsWith('image/') ? 'image'
      : mediaType === 'application/pdf' ? 'pdf'
      : mediaType.startsWith('audio/') ? 'audio'
      : null;
    if (!kind) {
      throw new AttachmentError(
        `attachment[${i}] unsupported kind for media type ${mediaType}`,
        { code: 'UNSUPPORTED_MEDIA_TYPE', status: 415 }
      );
    }
    if (declaredType && declaredType !== kind) {
      // Frontend declared 'image' but media type is application/pdf — don't
      // try to be clever, reject as inconsistent.
      throw new AttachmentError(
        `attachment[${i}] declared type "${declaredType}" disagrees with media type "${mediaType}" (kind: ${kind})`,
        { code: 'TYPE_MISMATCH' }
      );
    }

    const byteSize = base64DecodedSize(base64);
    const perCap = kind === 'image' ? CAPS.perImageBytes
      : kind === 'pdf' ? CAPS.perPdfBytes
      : CAPS.perAudioBytes;
    if (byteSize > perCap) {
      throw new AttachmentError(
        `attachment[${i}] "${name || mediaType}" exceeds per-${kind} cap (${byteSize} > ${perCap} bytes)`,
        { code: 'TOO_LARGE', status: 413 }
      );
    }
    totalBytes += byteSize;
    if (totalBytes > CAPS.totalBytes) {
      throw new AttachmentError(
        `total attachment bytes ${totalBytes} exceed ${CAPS.totalBytes} per-turn cap`,
        { code: 'TOO_LARGE_TOTAL', status: 413 }
      );
    }

    parsed.push({
      type: kind,
      name: name || `attachment-${i + 1}.${mediaType.split('/')[1]}`,
      mediaType,
      base64,
      byteSize,
    });
  }

  return parsed;
}

/** Required model capability for a given attachment kind. */
function capabilityFor(attachment) {
  if (attachment.type === 'image') return 'vision';
  if (attachment.type === 'pdf') return 'pdf';
  if (attachment.type === 'audio') return 'audio';
  return null;
}

/**
 * Collect the full set of capabilities required to serve a list of
 * attachments. Returns a `Set<string>` (e.g. `Set { 'vision' }`).
 */
function requiredCapabilities(parsedAttachments) {
  const out = new Set();
  for (const a of parsedAttachments) {
    const c = capabilityFor(a);
    if (c) out.add(c);
  }
  return out;
}

module.exports = {
  ALL_ALLOWED,
  AttachmentError,
  CAPS,
  PHASE1_ALLOWED,
  PHASE2_ALLOWED,
  base64DecodedSize,
  capabilityFor,
  parseAndValidateAttachments,
  parseDataUrl,
  requiredCapabilities,
};
