import type {
  CommandDiagnostic,
  FFmpegCommandPlan,
} from '../commands/command-plan';
import { JSON_MIME_TYPE } from './mime';
import { safeExportFileName } from './output-name';
import type { ExportResult } from './result-collection';

const REPORT_SCHEMA_VERSION = 1;
const MAX_DIAGNOSTICS = 100;
const MAX_TEXT_LENGTH = 2_000;

export type ExportReportStatus =
  'completed' | 'failed' | 'cancelled' | 'partial';

export interface ExportExecutionSummary {
  readonly status?: ExportReportStatus;
  readonly engineMode?: 'single-thread' | 'multi-thread';
  readonly ffmpegVersion?: string;
  readonly coreVersion?: string;
  readonly elapsedSeconds?: number;
  readonly conversionMilliseconds?: number;
  readonly outputReadMilliseconds?: number;
  readonly cleanupMilliseconds?: number;
  readonly exitCode?: number;
}

export interface ExportReportInput {
  readonly plan: Pick<FFmpegCommandPlan, 'id' | 'operation' | 'diagnostics'>;
  readonly results: readonly Pick<
    ExportResult,
    'id' | 'outputId' | 'fileName' | 'mimeType' | 'role' | 'size'
  >[];
  readonly execution?: ExportExecutionSummary;
  readonly diagnostics?: readonly CommandDiagnostic[];
  readonly generatedAt?: string;
}

export interface ExportReport {
  readonly schemaVersion: 1;
  readonly generatedAt: string;
  readonly plan: {
    readonly id: string;
    readonly operation: string;
  };
  readonly execution?: ExportExecutionSummary;
  readonly outputs: readonly {
    readonly id: string;
    readonly outputId: string;
    readonly fileName: string;
    readonly mimeType: string;
    readonly role: ExportResult['role'];
    readonly sizeBytes: number;
  }[];
  readonly diagnostics: readonly CommandDiagnostic[];
  readonly privacy: {
    readonly includesSourceMedia: false;
    readonly includesCommandArguments: false;
    readonly privatePathsRedacted: true;
  };
}

/**
 * Builds an intentionally narrow report. Source records, command arguments,
 * Blobs, object URLs and browser storage paths are not copied into the output.
 */
export function createExportReport(input: ExportReportInput): ExportReport {
  const generatedAt = normalizeTimestamp(
    input.generatedAt ?? new Date().toISOString()
  );
  const diagnostics = [...input.plan.diagnostics, ...(input.diagnostics ?? [])]
    .slice(0, MAX_DIAGNOSTICS)
    .map(sanitizeDiagnostic);
  const outputs = [...input.results]
    .map((result) =>
      Object.freeze({
        id: sanitizeText(result.id, 256),
        outputId: sanitizeText(result.outputId, 256),
        fileName: safeExportFileName(result.fileName, {
          fallback: 'output',
        }),
        mimeType: sanitizeText(result.mimeType, 128),
        role: result.role,
        sizeBytes: safeByteSize(result.size),
      })
    )
    .sort((left, right) => compareText(left.id, right.id));
  const execution = sanitizeExecution(input.execution);

  return Object.freeze({
    schemaVersion: REPORT_SCHEMA_VERSION,
    generatedAt,
    plan: Object.freeze({
      id: sanitizeText(input.plan.id, 256),
      operation: sanitizeText(input.plan.operation, 128),
    }),
    ...(execution ? { execution } : {}),
    outputs: Object.freeze(outputs),
    diagnostics: Object.freeze(diagnostics),
    privacy: Object.freeze({
      includesSourceMedia: false,
      includesCommandArguments: false,
      privatePathsRedacted: true,
    }),
  });
}

export function serializeExportReport(report: ExportReport): string {
  return `${JSON.stringify(report, undefined, 2)}\n`;
}

export function exportReportBlob(report: ExportReport): Blob {
  return new Blob([serializeExportReport(report)], {
    type: JSON_MIME_TYPE,
  });
}

export function exportReportFileName(operation: string): string {
  return safeExportFileName(`${operation}-export-report`, {
    extension: 'json',
    fallback: 'export-report',
  });
}

export function redactPrivatePaths(value: string): string {
  return value
    .replace(/\bblob:(?:https?:\/\/)?[^\s"'<>]+/giu, '[object URL]')
    .replace(/\bfile:\/\/\/?[^\s"'<>]+/giu, '[local path]')
    .replace(
      /(["'])\b[a-z]:[\\/](?:users|documents and settings|fakepath|temp)[\\/][^\r\n"'<>]*\1/giu,
      '[local path]'
    )
    .replace(
      /\b[a-z]:[\\/](?:users|documents and settings|fakepath|temp)[\\/][^\s"'<>]*/giu,
      '[local path]'
    )
    .replace(
      /(["'])\/(?:home|users|mnt|media|volumes|tmp|var\/folders|private\/var)\/[^\r\n"'<>]*\1/giu,
      '[local path]'
    )
    .replace(
      /\/(?:home|users|mnt|media|volumes|tmp|var\/folders|private\/var)\/[^\s"'<>]+/giu,
      '[local path]'
    );
}

function sanitizeDiagnostic(diagnostic: CommandDiagnostic): CommandDiagnostic {
  return Object.freeze({
    code: sanitizeText(diagnostic.code, 128),
    severity: diagnostic.severity,
    message: sanitizeText(diagnostic.message, MAX_TEXT_LENGTH),
    ...(diagnostic.field ? { field: sanitizeText(diagnostic.field, 256) } : {}),
  });
}

function sanitizeExecution(
  execution: ExportExecutionSummary | undefined
): ExportExecutionSummary | undefined {
  if (!execution) {
    return undefined;
  }
  const elapsedSeconds =
    execution.elapsedSeconds === undefined
      ? undefined
      : safeNonNegativeNumber(execution.elapsedSeconds, 'Elapsed seconds');
  const exitCode =
    execution.exitCode === undefined
      ? undefined
      : safeInteger(execution.exitCode, 'Exit code');
  const conversionMilliseconds = optionalNonNegativeNumber(
    execution.conversionMilliseconds,
    'Conversion milliseconds'
  );
  const outputReadMilliseconds = optionalNonNegativeNumber(
    execution.outputReadMilliseconds,
    'Output-read milliseconds'
  );
  const cleanupMilliseconds = optionalNonNegativeNumber(
    execution.cleanupMilliseconds,
    'Cleanup milliseconds'
  );
  return Object.freeze({
    ...(execution.status ? { status: execution.status } : {}),
    ...(execution.engineMode ? { engineMode: execution.engineMode } : {}),
    ...(execution.ffmpegVersion
      ? { ffmpegVersion: sanitizeText(execution.ffmpegVersion, 128) }
      : {}),
    ...(execution.coreVersion
      ? { coreVersion: sanitizeText(execution.coreVersion, 128) }
      : {}),
    ...(elapsedSeconds === undefined ? {} : { elapsedSeconds }),
    ...(conversionMilliseconds === undefined ? {} : { conversionMilliseconds }),
    ...(outputReadMilliseconds === undefined ? {} : { outputReadMilliseconds }),
    ...(cleanupMilliseconds === undefined ? {} : { cleanupMilliseconds }),
    ...(exitCode === undefined ? {} : { exitCode }),
  });
}

function optionalNonNegativeNumber(
  value: number | undefined,
  label: string
): number | undefined {
  return value === undefined ? undefined : safeNonNegativeNumber(value, label);
}

function sanitizeText(value: string, maximumLength: number): string {
  return removeUnsafeControlCharacters(redactPrivatePaths(value)).slice(
    0,
    maximumLength
  );
}

function removeUnsafeControlCharacters(value: string): string {
  return Array.from(value)
    .filter((character) => {
      const codePoint = character.codePointAt(0) ?? 0;
      return (
        codePoint === 9 ||
        codePoint === 10 ||
        codePoint === 13 ||
        (codePoint >= 32 && codePoint !== 127)
      );
    })
    .join('');
}

function normalizeTimestamp(value: string): string {
  const timestamp = new Date(value);
  if (!Number.isFinite(timestamp.valueOf())) {
    throw new RangeError('Report generation time must be a valid timestamp.');
  }
  return timestamp.toISOString();
}

function safeByteSize(value: number): number {
  if (!Number.isSafeInteger(value) || value < 0) {
    throw new RangeError('Output size must be a non-negative safe integer.');
  }
  return value;
}

function safeNonNegativeNumber(value: number, label: string): number {
  if (!Number.isFinite(value) || value < 0) {
    throw new RangeError(`${label} must be a non-negative finite number.`);
  }
  return value;
}

function safeInteger(value: number, label: string): number {
  if (!Number.isSafeInteger(value)) {
    throw new RangeError(`${label} must be a safe integer.`);
  }
  return value;
}

function compareText(left: string, right: string): number {
  return left < right ? -1 : left > right ? 1 : 0;
}
