import { safeOutputFileName } from '../media/safe-file-name';
import {
  migrateProjectDocument,
  type ProjectMigrationWarning,
} from './migrations';
import type { AvProjectV1 } from './project.types';
import {
  ProjectValidationError,
  assertValidProject,
} from './project.validation';

export interface SerializeProjectOptions {
  pretty?: boolean;
  includeUiState?: boolean;
}

export interface ImportProjectOptions {
  markSourcesMissing?: boolean;
  maximumBytes?: number;
}

export interface ProjectImportWarning {
  code: 'source-reattachment-required' | ProjectMigrationWarning['code'];
  path: string;
  message: string;
}

export interface ProjectImportResult {
  project: AvProjectV1;
  fromVersion: 0 | 1;
  warnings: ProjectImportWarning[];
}

export class ProjectImportError extends Error {
  readonly causeValue?: unknown;

  constructor(message: string, causeValue?: unknown) {
    super(message);
    this.name = 'ProjectImportError';
    this.causeValue = causeValue;
  }
}

export function serializeProject(
  project: AvProjectV1,
  options: SerializeProjectOptions = {}
): string {
  const validProject = assertValidProject(project);
  const document =
    options.includeUiState === false ? omitUi(validProject) : validProject;
  const indentation = options.pretty === false ? undefined : 2;
  return `${JSON.stringify(document, null, indentation)}\n`;
}

export function importProjectDocument(
  input: string | unknown,
  options: ImportProjectOptions = {}
): ProjectImportResult {
  const maximumBytes = options.maximumBytes ?? 10 * 1024 * 1024;
  if (!Number.isSafeInteger(maximumBytes) || maximumBytes <= 0) {
    throw new RangeError('Maximum project size must be a positive integer.');
  }

  let parsed: unknown = input;
  if (typeof input === 'string') {
    if (new TextEncoder().encode(input).byteLength > maximumBytes) {
      throw new ProjectImportError(
        `The project document exceeds the ${maximumBytes}-byte import limit.`
      );
    }
    try {
      parsed = JSON.parse(input) as unknown;
    } catch (error) {
      throw new ProjectImportError(
        'The project document is not valid JSON.',
        error
      );
    }
  }

  const migration = migrateProjectDocument(parsed);
  let project: AvProjectV1;
  try {
    project = assertValidProject(migration.project);
  } catch (error) {
    if (error instanceof ProjectValidationError) {
      throw error;
    }
    throw new ProjectImportError(
      'The project document could not be validated.',
      error
    );
  }

  const warnings: ProjectImportWarning[] = [...migration.warnings];
  if (options.markSourcesMissing !== false) {
    project = {
      ...project,
      assets: project.assets.map((asset) => ({
        ...asset,
        sourceStatus: 'missing',
      })),
      subtitles: project.subtitles.map((track) =>
        track.sourceStatus === undefined
          ? track
          : { ...track, sourceStatus: 'missing' }
      ),
    };
    if (
      project.assets.length +
        project.subtitles.filter((track) => track.sourceStatus !== undefined)
          .length >
      0
    ) {
      warnings.push({
        code: 'source-reattachment-required',
        path: 'assets',
        message:
          'Browser File objects are not stored in a project document. Reattach source files before export.',
      });
    }
  }

  return {
    project,
    fromVersion: migration.fromVersion,
    warnings,
  };
}

export function deserializeProject(
  json: string,
  options?: ImportProjectOptions
): AvProjectV1 {
  return importProjectDocument(json, options).project;
}

export function projectDocumentFileName(project: AvProjectV1): string {
  return safeOutputFileName(project.name, 'avproject.json', 'av-tools-project');
}

function omitUi(project: AvProjectV1): AvProjectV1 {
  const document = { ...project };
  delete document.ui;
  return document;
}
