import { createCacheKey, stableHash } from '../storage/cache-key';
import { validateWaveformPeakData } from './peak-aggregation';
import { WaveformDataError } from './waveform-errors';
import {
  MAX_WAVEFORM_BUCKET_COUNT,
  WAVEFORM_PEAKS_VERSION,
  type PcmEncoding,
  type WaveformPeakData,
} from './waveform.types';

export const WAVEFORM_PEAKS_MEDIA_TYPE =
  'application/vnd.add-ideas.waveform-peaks';

const MAGIC = [0x57, 0x46, 0x50, 0x4b] as const; // WFPK
const HEADER_BYTES = 44;
const FLAG_RMS = 1;
const SUPPORTED_FLAGS = FLAG_RMS;

export interface WaveformCacheKeyOptions {
  readonly sourceSize: number;
  readonly sourceLastModified: number;
  readonly streamIndex: number;
  readonly sampleRate: number;
  readonly bucketCount: number;
  readonly encoding: PcmEncoding;
  readonly partialHash?: string;
}

/**
 * Produces a safe cache key containing all source and analysis identities
 * required to avoid reusing peaks for a different file or stream.
 */
export function createWaveformCacheKey(
  options: WaveformCacheKeyOptions
): string {
  validateCacheKeyOptions(options);
  const sourceFingerprint = stableHash({
    size: options.sourceSize,
    lastModified: options.sourceLastModified,
    partialHash: options.partialHash ?? null,
  });
  return createCacheKey({
    sourceFingerprint,
    operation: 'waveform-peaks',
    streamIndex: options.streamIndex,
    settings: {
      sampleRate: options.sampleRate,
      bucketCount: options.bucketCount,
      encoding: options.encoding,
    },
    version: WAVEFORM_PEAKS_VERSION,
  });
}

/**
 * Serializes peak arrays to a compact, explicitly little-endian, checksummed
 * format suitable for the derived-data cache.
 */
export function serializeWaveformPeaks(peaks: WaveformPeakData): Uint8Array {
  validateWaveformPeakData(peaks);
  const payloadBytes = peaks.bucketCount * 3 * Float32Array.BYTES_PER_ELEMENT;
  const result = new Uint8Array(HEADER_BYTES + payloadBytes);
  const view = new DataView(result.buffer);
  for (let index = 0; index < MAGIC.length; index += 1) {
    view.setUint8(index, MAGIC[index] ?? 0);
  }
  view.setUint16(4, WAVEFORM_PEAKS_VERSION, true);
  view.setUint16(6, HEADER_BYTES, true);
  view.setUint8(8, encodingToCode(peaks.sourceEncoding));
  view.setUint8(9, FLAG_RMS);
  view.setUint16(10, 0, true);
  view.setUint32(12, peaks.sampleRate, true);
  view.setUint32(16, peaks.bucketCount, true);
  view.setFloat64(20, peaks.sampleCount, true);
  view.setFloat64(28, peaks.durationSeconds, true);
  view.setUint32(36, payloadBytes, true);

  let offset = HEADER_BYTES;
  for (const values of [peaks.minimum, peaks.maximum, peaks.rms]) {
    for (const value of values) {
      view.setFloat32(offset, value, true);
      offset += Float32Array.BYTES_PER_ELEMENT;
    }
  }
  const checksum = checksumCacheEntry(result);
  view.setUint32(40, checksum, true);
  return result;
}

export function deserializeWaveformPeaks(
  source: ArrayBuffer | ArrayBufferView
): WaveformPeakData {
  const bytes = toByteView(source);
  if (bytes.byteLength < HEADER_BYTES) {
    throw new WaveformDataError('Waveform cache entry is too short');
  }
  const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
  for (let index = 0; index < MAGIC.length; index += 1) {
    if (view.getUint8(index) !== MAGIC[index]) {
      throw new WaveformDataError('Waveform cache entry has an invalid magic');
    }
  }
  const version = view.getUint16(4, true);
  if (version !== WAVEFORM_PEAKS_VERSION) {
    throw new WaveformDataError(
      `Unsupported waveform cache version: ${String(version)}`
    );
  }
  if (view.getUint16(6, true) !== HEADER_BYTES) {
    throw new WaveformDataError('Waveform cache header size is invalid');
  }
  const flags = view.getUint8(9);
  if ((flags & ~SUPPORTED_FLAGS) !== 0 || (flags & FLAG_RMS) === 0) {
    throw new WaveformDataError('Waveform cache flags are unsupported');
  }
  const bucketCount = view.getUint32(16, true);
  if (bucketCount > MAX_WAVEFORM_BUCKET_COUNT) {
    throw new WaveformDataError('Waveform cache has too many buckets');
  }
  const expectedPayloadBytes = bucketCount * 3 * Float32Array.BYTES_PER_ELEMENT;
  const payloadBytes = view.getUint32(36, true);
  if (
    payloadBytes !== expectedPayloadBytes ||
    bytes.byteLength !== HEADER_BYTES + payloadBytes
  ) {
    throw new WaveformDataError('Waveform cache payload size is inconsistent');
  }
  const storedChecksum = view.getUint32(40, true);
  if (checksumCacheEntry(bytes) !== storedChecksum) {
    throw new WaveformDataError('Waveform cache checksum does not match');
  }

  let offset = HEADER_BYTES;
  const readArray = (): Float32Array => {
    const result = new Float32Array(bucketCount);
    for (let index = 0; index < bucketCount; index += 1) {
      result[index] = view.getFloat32(offset, true);
      offset += Float32Array.BYTES_PER_ELEMENT;
    }
    return result;
  };
  const peaks: WaveformPeakData = Object.freeze({
    version: WAVEFORM_PEAKS_VERSION,
    sourceEncoding: codeToEncoding(view.getUint8(8)),
    sampleRate: view.getUint32(12, true),
    bucketCount,
    sampleCount: view.getFloat64(20, true),
    durationSeconds: view.getFloat64(28, true),
    minimum: readArray(),
    maximum: readArray(),
    rms: readArray(),
  });
  return validateWaveformPeakData(peaks);
}

function validateCacheKeyOptions(options: WaveformCacheKeyOptions): void {
  if (
    !Number.isSafeInteger(options.sourceSize) ||
    options.sourceSize < 0 ||
    !Number.isSafeInteger(options.sourceLastModified) ||
    options.sourceLastModified < 0 ||
    !Number.isSafeInteger(options.streamIndex) ||
    options.streamIndex < 0 ||
    !Number.isSafeInteger(options.sampleRate) ||
    options.sampleRate <= 0 ||
    !Number.isSafeInteger(options.bucketCount) ||
    options.bucketCount <= 0 ||
    options.bucketCount > MAX_WAVEFORM_BUCKET_COUNT
  ) {
    throw new TypeError('Waveform cache identity contains invalid numbers');
  }
  if (options.encoding !== 's16le' && options.encoding !== 'f32le') {
    throw new TypeError('Waveform cache encoding is invalid');
  }
  if (
    options.partialHash !== undefined &&
    !/^[a-z0-9._-]{4,256}$/iu.test(options.partialHash)
  ) {
    throw new TypeError('Waveform partial hash is invalid');
  }
}

function encodingToCode(encoding: PcmEncoding): number {
  return encoding === 's16le' ? 1 : 2;
}

function codeToEncoding(code: number): PcmEncoding {
  if (code === 1) {
    return 's16le';
  }
  if (code === 2) {
    return 'f32le';
  }
  throw new WaveformDataError('Waveform cache encoding is unsupported');
}

function checksumCacheEntry(bytes: Uint8Array): number {
  let checksum = 0x811c9dc5;
  for (let index = 0; index < bytes.byteLength; index += 1) {
    if (index >= 40 && index < 44) {
      continue;
    }
    checksum ^= bytes[index] ?? 0;
    checksum = Math.imul(checksum, 0x01000193) >>> 0;
  }
  return checksum;
}

function toByteView(source: ArrayBuffer | ArrayBufferView): Uint8Array {
  return source instanceof ArrayBuffer
    ? new Uint8Array(source)
    : new Uint8Array(source.buffer, source.byteOffset, source.byteLength);
}
