import type { ExportPreset } from './convert';
import { buildConvertPlan } from './convert';
import {
  createPlannedInput,
  diagnostic,
  normalizeSeconds,
  type SourceReference,
} from './command-utils';
import { freezeCommandPlan, type FFmpegCommandPlan } from './command-plan';

export interface LoudnessMeasurement {
  readonly inputIntegratedLufs: number;
  readonly inputTruePeakDbtp: number;
  readonly inputLoudnessRangeLu: number;
  readonly inputThresholdLufs: number;
  readonly targetOffsetLu?: number;
}

export interface LoudnessTargets {
  readonly integratedLufs: number;
  readonly truePeakDbtp: number;
  readonly loudnessRangeLu: number;
}

export interface LoudnessAnalysisOptions {
  readonly jobId: string;
  readonly source: SourceReference;
  readonly targets: LoudnessTargets;
  /** Absolute ffprobe stream index. */
  readonly audioStreamIndex: number;
  readonly expectedDurationSeconds?: number;
}

export interface LoudnessApplyOptions extends LoudnessAnalysisOptions {
  readonly measurement: LoudnessMeasurement;
  readonly preset: ExportPreset;
}

export interface PeakNormalizationOptions {
  readonly jobId: string;
  readonly source: SourceReference;
  /** Absolute ffprobe stream index. */
  readonly audioStreamIndex: number;
  readonly targetPeakDb: number;
  readonly measurement: PeakMeasurement;
  readonly preset: ExportPreset;
  readonly expectedDurationSeconds?: number;
}

export interface PeakMeasurement {
  readonly inputPeakDbfs: number;
}

export interface PeakAnalysisOptions {
  readonly jobId: string;
  readonly source: SourceReference;
  /** Absolute ffprobe stream index. */
  readonly audioStreamIndex: number;
  readonly preset: ExportPreset;
  readonly expectedDurationSeconds?: number;
}

export const LOUDNESS_PROFILES: Readonly<
  Record<'web-video' | 'podcast' | 'broadcast-style', LoudnessTargets>
> = Object.freeze({
  'web-video': Object.freeze({
    integratedLufs: -14,
    truePeakDbtp: -1,
    loudnessRangeLu: 11,
  }),
  podcast: Object.freeze({
    integratedLufs: -16,
    truePeakDbtp: -1,
    loudnessRangeLu: 11,
  }),
  'broadcast-style': Object.freeze({
    integratedLufs: -23,
    truePeakDbtp: -2,
    loudnessRangeLu: 7,
  }),
});

export function buildLoudnessAnalysisPlan(
  options: LoudnessAnalysisOptions
): FFmpegCommandPlan {
  validateLoudnessTargets(options.targets);
  const streamIndex = options.audioStreamIndex;
  if (!Number.isSafeInteger(streamIndex) || streamIndex < 0) {
    throw new RangeError('Invalid audio stream index');
  }
  const input = createPlannedInput(options.jobId, options.source);
  const filter = loudnormFilter(options.targets, undefined, true);
  return freezeCommandPlan({
    id: `${options.jobId}:loudness-analysis`,
    operation: 'loudness-analysis',
    inputs: [input],
    temporaryFiles: [],
    args: [
      '-i',
      input.path,
      '-map',
      `0:${streamIndex}`,
      '-af',
      filter,
      '-f',
      'null',
      '-',
    ],
    outputs: [],
    ...(options.expectedDurationSeconds !== undefined
      ? { expectedDurationSeconds: options.expectedDurationSeconds }
      : {}),
    requiredCapabilities: { filters: ['loudnorm'], muxers: ['null'] },
    diagnostics: [
      diagnostic(
        'loudnorm-first-pass',
        'info',
        'First pass measures loudness only and preserves its logs for diagnostics.'
      ),
    ],
  });
}

export function buildLoudnessApplyPlan(
  options: LoudnessApplyOptions
): FFmpegCommandPlan {
  validateLoudnessTargets(options.targets);
  validateLoudnessMeasurement(options.measurement);
  if (!options.preset.audio || options.preset.video) {
    throw new TypeError(
      'Measured loudness normalization requires an audio-only export preset'
    );
  }
  return buildConvertPlan({
    jobId: options.jobId,
    source: options.source,
    preset: options.preset,
    streamSelection: { audio: [options.audioStreamIndex] },
    expectedDurationSeconds: options.expectedDurationSeconds,
    operation: 'normalize',
    audioFiltergraph: loudnormFilter(
      options.targets,
      options.measurement,
      false
    ),
    additionalRequirements: { filters: ['loudnorm'] },
  });
}

export function buildPeakAnalysisPlan(
  options: PeakAnalysisOptions
): FFmpegCommandPlan {
  if (
    !Number.isSafeInteger(options.audioStreamIndex) ||
    options.audioStreamIndex < 0
  ) {
    throw new RangeError('Invalid audio stream index');
  }
  validateAudioOnlyPreset(options.preset, 'Peak analysis');
  const input = createPlannedInput(options.jobId, options.source);
  const formatFilters = audioPresetFormatFilters(options.preset);
  return freezeCommandPlan({
    id: `${options.jobId}:peak-analysis`,
    operation: 'peak-analysis',
    inputs: [input],
    temporaryFiles: [],
    args: [
      '-i',
      input.path,
      '-map',
      `0:${options.audioStreamIndex}`,
      '-af',
      [...formatFilters, 'volumedetect'].join(','),
      '-f',
      'null',
      '-',
    ],
    outputs: [],
    ...(options.expectedDurationSeconds !== undefined
      ? { expectedDurationSeconds: options.expectedDurationSeconds }
      : {}),
    requiredCapabilities: {
      filters: [
        ...formatFilters.map((filter) => filter.split('=')[0] as string),
        'volumedetect',
      ],
      muxers: ['null'],
    },
    diagnostics: [
      diagnostic(
        'peak-first-pass',
        'info',
        'A complete local first pass measures the selected stream peak before a constant gain is calculated.'
      ),
    ],
  });
}

export function parsePeakMeasurement(logText: string): PeakMeasurement {
  const matches = [
    ...logText.matchAll(
      /max_volume:\s*(-?(?:\d+(?:\.\d+)?|\.\d+|inf))\s*dB/giu
    ),
  ];
  const raw = matches.at(-1)?.[1];
  if (!raw || raw.toLowerCase().includes('inf')) {
    throw new TypeError(
      'No finite max_volume measurement was found; silent audio cannot be peak-normalized'
    );
  }
  const measurement = Object.freeze({ inputPeakDbfs: Number(raw) });
  validatePeakMeasurement(measurement);
  return measurement;
}

export function validatePeakMeasurement(measurement: PeakMeasurement): void {
  if (
    !Number.isFinite(measurement.inputPeakDbfs) ||
    measurement.inputPeakDbfs < -200 ||
    measurement.inputPeakDbfs > 100
  ) {
    throw new TypeError(
      'Peak measurement is incomplete or outside safe bounds'
    );
  }
}

export function buildPeakNormalizationFilter(
  targetPeakDb: number,
  measurement: PeakMeasurement
): string {
  validatePeakMeasurement(measurement);
  const inputPeakDbfs = measurement.inputPeakDbfs;
  if (
    !Number.isFinite(targetPeakDb) ||
    targetPeakDb > 0 ||
    targetPeakDb < -20
  ) {
    throw new RangeError('Peak target must be between -20 and 0 dB');
  }
  if (!Number.isFinite(inputPeakDbfs)) {
    throw new TypeError('Measured input peak must be finite');
  }
  const gainDb = targetPeakDb - inputPeakDbfs;
  if (Math.abs(gainDb) > 60) {
    throw new RangeError(
      'Peak normalization would require more than 60 dB of gain change'
    );
  }
  const linear = 10 ** (gainDb / 20);
  return `volume=${linear.toFixed(8).replace(/0+$/u, '').replace(/\.$/u, '')}`;
}

export function buildPeakNormalizationPlan(
  options: PeakNormalizationOptions
): FFmpegCommandPlan {
  validateAudioOnlyPreset(options.preset, 'Peak normalization');
  if (
    !Number.isSafeInteger(options.audioStreamIndex) ||
    options.audioStreamIndex < 0
  ) {
    throw new RangeError('Invalid audio stream index');
  }
  validatePeakMeasurement(options.measurement);
  const filter = buildPeakNormalizationFilter(
    options.targetPeakDb,
    options.measurement
  );
  const base = buildConvertPlan({
    jobId: options.jobId,
    source: options.source,
    preset: options.preset,
    streamSelection: { audio: [options.audioStreamIndex] },
    operation: 'peak-normalize',
    audioFiltergraph: [
      ...audioPresetFormatFilters(options.preset),
      filter,
    ].join(','),
    additionalRequirements: {
      filters: [
        ...audioPresetFormatFilters(options.preset).map(
          (entry) => entry.split('=')[0] as string
        ),
        'volume',
      ],
    },
    ...(options.expectedDurationSeconds !== undefined
      ? { expectedDurationSeconds: options.expectedDurationSeconds }
      : {}),
  });
  return freezeCommandPlan({
    ...base,
    diagnostics: [
      diagnostic(
        'measured-peak-gain',
        'info',
        `Measured ${normalizeSigned(options.measurement.inputPeakDbfs)} dBFS; applying ${normalizeSigned(options.targetPeakDb - options.measurement.inputPeakDbfs)} dB constant gain to target ${normalizeSigned(options.targetPeakDb)} dBFS.`
      ),
      ...base.diagnostics,
    ],
  });
}

function validateAudioOnlyPreset(
  preset: ExportPreset,
  operation: string
): void {
  if (!preset.audio || preset.video) {
    throw new TypeError(`${operation} requires an audio-only export preset`);
  }
}

function audioPresetFormatFilters(preset: ExportPreset): readonly string[] {
  if (!preset.audio) {
    return [];
  }
  const filters: string[] = [];
  if (preset.audio.sampleRate !== undefined) {
    filters.push(`aresample=${preset.audio.sampleRate}`);
  }
  if (preset.audio.channels !== undefined) {
    const layout =
      preset.audio.channels === 1
        ? 'mono'
        : preset.audio.channels === 2
          ? 'stereo'
          : '5.1';
    filters.push(`aformat=channel_layouts=${layout}`);
  }
  return filters;
}

export function parseLoudnessMeasurement(logText: string): LoudnessMeasurement {
  const blocks = logText.match(/\{[\s\S]*?\}/gu) ?? [];
  for (const block of blocks.reverse()) {
    let candidate: unknown;
    try {
      candidate = JSON.parse(block) as unknown;
    } catch {
      continue;
    }
    if (!isRecord(candidate) || !('input_i' in candidate)) {
      continue;
    }
    const measurement: LoudnessMeasurement = {
      inputIntegratedLufs: parseMeasurement(candidate.input_i, 'input_i'),
      inputTruePeakDbtp: parseMeasurement(candidate.input_tp, 'input_tp'),
      inputLoudnessRangeLu: parseMeasurement(candidate.input_lra, 'input_lra'),
      inputThresholdLufs: parseMeasurement(
        candidate.input_thresh,
        'input_thresh'
      ),
      ...('target_offset' in candidate
        ? {
            targetOffsetLu: parseMeasurement(
              candidate.target_offset,
              'target_offset'
            ),
          }
        : {}),
    };
    validateLoudnessMeasurement(measurement);
    return Object.freeze(measurement);
  }
  throw new TypeError('No complete loudnorm measurement JSON was found');
}

export function validateLoudnessMeasurement(
  measurement: LoudnessMeasurement
): void {
  const values = [
    measurement.inputIntegratedLufs,
    measurement.inputTruePeakDbtp,
    measurement.inputLoudnessRangeLu,
    measurement.inputThresholdLufs,
    ...(measurement.targetOffsetLu === undefined
      ? []
      : [measurement.targetOffsetLu]),
  ];
  if (values.some((value) => !Number.isFinite(value))) {
    throw new TypeError('Loudness measurement is incomplete or non-finite');
  }
}

function loudnormFilter(
  targets: LoudnessTargets,
  measurement?: LoudnessMeasurement,
  printJson = false
): string {
  const values = [
    `I=${normalizeSigned(targets.integratedLufs)}`,
    `TP=${normalizeSigned(targets.truePeakDbtp)}`,
    `LRA=${normalizeSeconds(targets.loudnessRangeLu)}`,
  ];
  if (measurement) {
    values.push(
      `measured_I=${normalizeSigned(measurement.inputIntegratedLufs)}`,
      `measured_TP=${normalizeSigned(measurement.inputTruePeakDbtp)}`,
      `measured_LRA=${normalizeSeconds(measurement.inputLoudnessRangeLu)}`,
      `measured_thresh=${normalizeSigned(measurement.inputThresholdLufs)}`,
      `offset=${normalizeSigned(measurement.targetOffsetLu ?? 0)}`,
      'linear=true'
    );
  }
  if (printJson) {
    values.push('print_format=json');
  }
  return `loudnorm=${values.join(':')}`;
}

function validateLoudnessTargets(targets: LoudnessTargets): void {
  if (
    !Number.isFinite(targets.integratedLufs) ||
    targets.integratedLufs < -70 ||
    targets.integratedLufs > -5
  ) {
    throw new RangeError(
      'Integrated loudness target must be between -70 and -5 LUFS'
    );
  }
  if (
    !Number.isFinite(targets.truePeakDbtp) ||
    targets.truePeakDbtp < -9 ||
    targets.truePeakDbtp > 0
  ) {
    throw new RangeError('True peak target must be between -9 and 0 dBTP');
  }
  if (
    !Number.isFinite(targets.loudnessRangeLu) ||
    targets.loudnessRangeLu < 1 ||
    targets.loudnessRangeLu > 50
  ) {
    throw new RangeError('Loudness range target must be between 1 and 50 LU');
  }
}

function normalizeSigned(value: number): string {
  if (!Number.isFinite(value)) {
    throw new RangeError('Value must be finite');
  }
  return value.toFixed(6).replace(/0+$/u, '').replace(/\.$/u, '');
}

function parseMeasurement(value: unknown, label: string): number {
  if (typeof value !== 'string' && typeof value !== 'number') {
    throw new TypeError(`${label} is missing`);
  }
  const parsed = typeof value === 'number' ? value : Number(value);
  if (!Number.isFinite(parsed)) {
    throw new TypeError(`${label} is not finite`);
  }
  return parsed;
}

function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === 'object' && value !== null && !Array.isArray(value);
}
