/**
 * V3.9 — Rich file metadata helpers.
 *
 * `StreamFileMetadata` rides inside `stream-init` plaintext and is therefore
 * E2EE; consumers that opt into thumbnails ship the preview as a *separate*
 * stream (id `${main}.thumb`) keyed independently. The format-hardening
 * rules (size + MIME allowlist) are enforced *symmetrically* on sender and
 * receiver so a hostile peer cannot bypass them by forging
 * `fileMetadata` — the receiver re-validates before rendering.
 */

import { ValidationError } from '@shade/core';
import {
  THUMBNAIL_MAX_BYTES,
  THUMBNAIL_MIME_ALLOWLIST,
  type StreamFileMetadata,
  type ThumbnailMime,
} from './types.js';

/** Suffix used for the companion thumbnail-stream id. */
export const THUMBNAIL_STREAM_ID_SUFFIX = '.thumb';

/** Compute the thumbnail-stream id given the main streamId. */
export function thumbnailStreamIdFor(mainStreamId: string): string {
  return `${mainStreamId}${THUMBNAIL_STREAM_ID_SUFFIX}`;
}

/** Inverse: given a streamId, peel `.thumb`. Returns null if not a thumb. */
export function mainStreamIdForThumbnail(streamId: string): string | null {
  if (!streamId.endsWith(THUMBNAIL_STREAM_ID_SUFFIX)) return null;
  return streamId.slice(0, -THUMBNAIL_STREAM_ID_SUFFIX.length);
}

/** Type guard for the MIME allowlist. */
export function isAllowedThumbnailMime(mime: string): mime is ThumbnailMime {
  return (THUMBNAIL_MIME_ALLOWLIST as readonly string[]).includes(mime);
}

/**
 * Validate a `StreamFileMetadata` value. Throws `ValidationError` on a
 * shape mismatch. All fields are optional, so an empty object is valid.
 *
 * The same function runs on the sender (before encoding init) and the
 * receiver (after decoding) — single source of truth for the format
 * rules.
 */
export function validateFileMetadata(meta: StreamFileMetadata): void {
  if (typeof meta !== 'object' || meta === null) {
    throw new ValidationError('fileMetadata must be an object', 'fileMetadata');
  }
  if (meta.filename !== undefined) {
    if (typeof meta.filename !== 'string') {
      throw new ValidationError('filename must be a string', 'fileMetadata.filename');
    }
    if (meta.filename.length > 1024) {
      throw new ValidationError(
        'filename exceeds 1024 chars',
        'fileMetadata.filename',
      );
    }
    // Strip control chars + path separators — these have no place in a
    // displayable filename and a hostile sender shouldn't be able to make
    // a UI render `\r\n` or `../`.
    if (/[\x00-\x1f\x7f]/.test(meta.filename)) {
      throw new ValidationError(
        'filename contains control characters',
        'fileMetadata.filename',
      );
    }
  }
  if (meta.mimeType !== undefined) {
    if (typeof meta.mimeType !== 'string' || meta.mimeType.length > 256) {
      throw new ValidationError(
        'mimeType must be a non-empty string ≤ 256 chars',
        'fileMetadata.mimeType',
      );
    }
    if (!/^[a-zA-Z0-9!#$&^_.+-]+\/[a-zA-Z0-9!#$&^_.+-]+$/.test(meta.mimeType)) {
      throw new ValidationError(
        'mimeType is not a well-formed media-type token',
        'fileMetadata.mimeType',
      );
    }
  }
  if (meta.thumbnailMime !== undefined && !isAllowedThumbnailMime(meta.thumbnailMime)) {
    throw new ValidationError(
      `thumbnailMime ${meta.thumbnailMime} not in allowlist`,
      'fileMetadata.thumbnailMime',
    );
  }
  if (meta.thumbnailBytes !== undefined) {
    if (
      !Number.isInteger(meta.thumbnailBytes) ||
      meta.thumbnailBytes < 0 ||
      meta.thumbnailBytes > THUMBNAIL_MAX_BYTES
    ) {
      throw new ValidationError(
        `thumbnailBytes out of range [0, ${THUMBNAIL_MAX_BYTES}]`,
        'fileMetadata.thumbnailBytes',
      );
    }
  }
  if (meta.thumbnailHash !== undefined) {
    if (typeof meta.thumbnailHash !== 'string' || meta.thumbnailHash.length === 0) {
      throw new ValidationError(
        'thumbnailHash must be a non-empty base64 string',
        'fileMetadata.thumbnailHash',
      );
    }
  }
  if (meta.thumbnailStreamId !== undefined) {
    if (
      typeof meta.thumbnailStreamId !== 'string' ||
      meta.thumbnailStreamId.length === 0 ||
      meta.thumbnailStreamId.length > 64
    ) {
      throw new ValidationError(
        'thumbnailStreamId must be a non-empty short string',
        'fileMetadata.thumbnailStreamId',
      );
    }
  }
  // Cross-field check: hash without bytes (or vice versa) is suspicious.
  // We allow `thumbnailHash` alone for backwards compatibility with future
  // shapes, but require bytes when MIME is declared.
  if (meta.thumbnailMime !== undefined && meta.thumbnailBytes === undefined) {
    throw new ValidationError(
      'thumbnailMime declared without thumbnailBytes',
      'fileMetadata',
    );
  }
}

/**
 * Return true when the metadata declares a thumbnail-stream that the
 * receiver should fetch.
 */
export function declaresThumbnail(meta: StreamFileMetadata | undefined): boolean {
  return (
    meta !== undefined &&
    meta.thumbnailHash !== undefined &&
    meta.thumbnailMime !== undefined &&
    meta.thumbnailBytes !== undefined &&
    meta.thumbnailStreamId !== undefined
  );
}
