import type { JobProgressUpdate, MediaJobStep } from './job.types';

export interface ParsedFFmpegProgress {
  readonly frame?: number;
  readonly fps?: number;
  readonly outTimeSeconds?: number;
  readonly speed?: number;
  readonly terminal: boolean;
}

export function parseFFmpegProgress(text: string): ParsedFFmpegProgress {
  const values = new Map<string, string>();
  for (const line of text.split(/\r?\n/gu)) {
    const separator = line.indexOf('=');
    if (separator > 0) {
      values.set(
        line.slice(0, separator).trim(),
        line.slice(separator + 1).trim()
      );
    }
  }
  const frame = parseOptionalFinite(values.get('frame'));
  const fps = parseOptionalFinite(values.get('fps'));
  const outTimeSeconds =
    parseMicroseconds(values.get('out_time_us')) ??
    parseProgressTime(values.get('out_time'));
  const speed = parseSpeed(values.get('speed'));
  return Object.freeze({
    ...(frame !== undefined ? { frame } : {}),
    ...(fps !== undefined ? { fps } : {}),
    ...(outTimeSeconds !== undefined ? { outTimeSeconds } : {}),
    ...(speed !== undefined ? { speed } : {}),
    terminal: values.get('progress') === 'end',
  });
}

export function progressUpdateFromFFmpeg(
  parsed: ParsedFFmpegProgress,
  expectedDurationSeconds?: number
): JobProgressUpdate {
  const progress = parsed.terminal
    ? 0.99
    : parsed.outTimeSeconds !== undefined &&
        expectedDurationSeconds !== undefined &&
        expectedDurationSeconds > 0
      ? Math.min(
          0.99,
          Math.max(0, parsed.outTimeSeconds / expectedDurationSeconds)
        )
      : undefined;
  return Object.freeze({
    ...(progress !== undefined ? { progress } : {}),
    ...(parsed.speed !== undefined ? { speed: parsed.speed } : {}),
    ...(parsed.frame !== undefined ? { frame: parsed.frame } : {}),
  });
}

export function weightedJobProgress(steps: readonly MediaJobStep[]): number {
  const totalWeight = steps.reduce((sum, step) => sum + step.weight, 0);
  if (totalWeight <= 0) {
    return 0;
  }
  const completedWeight = steps.reduce(
    (sum, step) => sum + step.weight * clampProgress(step.progress),
    0
  );
  return Math.min(0.999, completedWeight / totalWeight);
}

export function validateStepWeights(
  steps: readonly { readonly weight: number; readonly id: string }[]
): void {
  if (steps.length === 0) {
    throw new RangeError('A media job requires at least one step');
  }
  const ids = new Set<string>();
  for (const step of steps) {
    if (
      !step.id ||
      ids.has(step.id) ||
      !Number.isFinite(step.weight) ||
      step.weight <= 0
    ) {
      throw new RangeError(
        'Job step IDs must be unique and weights must be positive'
      );
    }
    ids.add(step.id);
  }
}

function parseMicroseconds(value: string | undefined): number | undefined {
  const microseconds = parseOptionalFinite(value);
  return microseconds === undefined ? undefined : microseconds / 1_000_000;
}

function parseProgressTime(value: string | undefined): number | undefined {
  if (!value) {
    return undefined;
  }
  const match = /^(\d+):([0-5]\d):([0-5]\d(?:\.\d+)?)$/u.exec(value);
  if (!match) {
    return undefined;
  }
  return Number(match[1]) * 3600 + Number(match[2]) * 60 + Number(match[3]);
}

function parseSpeed(value: string | undefined): number | undefined {
  return value?.endsWith('x')
    ? parseOptionalFinite(value.slice(0, -1))
    : undefined;
}

function parseOptionalFinite(value: string | undefined): number | undefined {
  if (value === undefined || value === 'N/A') {
    return undefined;
  }
  const result = Number(value);
  return Number.isFinite(result) ? result : undefined;
}

function clampProgress(value: number): number {
  return Math.min(1, Math.max(0, Number.isFinite(value) ? value : 0));
}
