// Data-import DOMAIN — the §5 File-to-Task Mapping Engine. THE testable core: it decides how
// staged files become Tasks (one Task = one row on the Data Page). Pure, deterministic.
// Source: docs/workflows/data-import-spec.md §5 (mapping table + MAP1-MAP3) and §3 (data types).
//
// Folder-level assumptions (spec open Q9.6 — documented, easy to revisit):
//   • DICOM, no Group-by-Study: each *immediate folder* (dirOf) is one series -> one Task (MAP1).
//   • DICOM,  Group-by-Study:   each *top-level folder* (study root) -> one Task (a full study).
//   • NIfTI,  no Group-by-Study: each file -> its own Task.
//   • NIfTI,  Group-by-Study:   each *folder* -> one Task (the study).
//   • Video frames: each folder -> one Task, frames ordered by filename (MAP2).

import type { DataType } from '../dataTypes';
import type { StagedFile } from '../types';
import { basename, compareByFilename, dirOf, groupBy, stripExtension, topDirOf } from './paths';

export type TaskGrouping = 'series' | 'study' | 'frames' | 'image' | 'video' | 'volume';

/** A planned Task (intent, not yet uploaded): the files that compose it + how they were grouped. */
export interface TaskPlan {
  /** Human-facing task name, derived from the folder or file name. */
  name: string;
  dataType: DataType;
  grouping: TaskGrouping;
  /** Member files, ordered deterministically (MAP2) for series/frames. */
  files: StagedFile[];
}

/**
 * Map staged files to Task plans per the §5 rules. Deterministic: groups are emitted in
 * sorted-key order, members sorted by filename, so the output is stable for tests + previews.
 */
export function mapFilesToTasks(
  dataType: DataType,
  groupByStudy: boolean,
  files: readonly StagedFile[],
): TaskPlan[] {
  if (files.length === 0) return [];

  switch (dataType) {
    case 'image2d':
      // Each image -> one Task.
      return files.map((f) => oneFileTask(f, dataType, 'image'));

    case 'video':
      // Each video -> one Task.
      return files.map((f) => oneFileTask(f, dataType, 'video'));

    case 'videoFrames':
      // All frames in a folder -> one Task, sorted by filename (MAP2).
      return foldersToTasks(files, dataType, 'frames', dirOf, 'frames');

    case 'dicom':
      // Group-by-Study: top-level folder -> study Task. Else: immediate folder -> series Task (MAP1).
      return groupByStudy
        ? foldersToTasks(files, dataType, 'study', topDirOf, 'study')
        : foldersToTasks(files, dataType, 'series', dirOf, 'series');

    case 'nifti':
      // Group-by-Study: folder -> study Task. Else: each file -> its own volume Task.
      return groupByStudy
        ? foldersToTasks(files, dataType, 'study', dirOf, 'study')
        : files.map((f) => oneFileTask(f, dataType, 'volume'));

    default:
      return assertNever(dataType);
  }
}

/** One file becomes one Task named after the file (sans extension). */
function oneFileTask(file: StagedFile, dataType: DataType, grouping: TaskGrouping): TaskPlan {
  return { name: stripExtension(file.name), dataType, grouping, files: [file] };
}

/**
 * Group files by a folder key, emit one Task per group. Group keys are sorted for deterministic
 * output; members are sorted by filename (MAP2). The fallback name is used when files sit at the
 * drop root (empty key).
 */
function foldersToTasks(
  files: readonly StagedFile[],
  dataType: DataType,
  grouping: TaskGrouping,
  keyOf: (relativePath: string) => string,
  rootFallback: string,
): TaskPlan[] {
  const groups = groupBy(files, (f) => keyOf(f.relativePath));
  const sortedKeys = [...groups.keys()].sort(compareByFilename);
  return sortedKeys.map((key) => {
    const members = [...groups.get(key)!].sort((a, b) => compareByFilename(a.name, b.name));
    return {
      name: key === '' ? rootFallback : basename(key),
      dataType,
      grouping,
      files: members,
    };
  });
}

function assertNever(x: never): never {
  throw new Error(`Unhandled data type: ${String(x)}`);
}
