/**
 * Dataset Workspace — domain adapter (pure, no React / no API).
 *
 * Bridges REAL ImageHub files (`listDatasetFiles`) into the nnU-Net case model so the cockpit
 * workspace can show a dataset case-by-case with an nnU-Net readiness check.
 *
 * ImageHub-native mapping (more correct than the mockup's filename-only guess):
 *   - `fileKind: 'image'`        -> an input CHANNEL. Channel index from a trailing `_XXXX`
 *                                   (nnU-Net multi-modal), else channel 0 (single-modality).
 *   - `fileKind: 'segmentation'` -> the LABEL of its parent image's case (the ground truth).
 *
 * The nnU-Net DESCRIPTOR (declared channel names, label class-map, numTraining) is NOT yet
 * persisted server-side — that is nnU-Net Phase 2. So the channels/labels here are INFERRED and
 * `inferred: true` flags it for the UI. We therefore keep only the per-case / per-file readiness
 * issues from `validateDataset` and drop the descriptor-level noise (dataset id/name, label class
 * map), which would be meaningless against an inferred meta.
 *
 * Reuses features/data-import/domain/nnunet/core.ts — single source of truth for nnU-Net rules.
 */
import type { ImagehubFile } from '@ump/shared';

import {
  FILE_ENDINGS,
  groupFilesIntoCases,
  parseFilename,
  stripEnding,
  validateDataset,
  type Channel,
  type DatasetCase,
  type DatasetMeta,
  type FileEnding,
  type Label,
  type SourceFile,
  type ValidationReport,
} from '../../data-import/domain/nnunet/core';

export type CaseStatus = 'valid' | 'warn' | 'error';

export interface WorkspaceModel {
  meta: DatasetMeta;
  ending: FileEnding;
  cases: DatasetCase[];
  unassigned: SourceFile[];
  /** nnU-Net readiness issues (per-case / per-file only — descriptor noise filtered out). */
  report: ValidationReport;
  /** Has at least one nnU-Net-style image file (so the case view is meaningful). */
  organized: boolean;
  /** Always true today: the descriptor was inferred from files, not loaded from a saved descriptor. */
  inferred: boolean;
}

/** Last path segment (logical_path is basename-flattened already, but be defensive). */
export function basename(path: string): string {
  const parts = path.split('/');
  return parts[parts.length - 1] || path;
}

/** Longest-match a known nnU-Net file ending (".nii.gz" wins over ".gz"). */
export function inferEnding(filename: string): FileEnding | null {
  const lower = filename.toLowerCase();
  const hit = [...FILE_ENDINGS]
    .sort((a, b) => b.length - a.length)
    .find((e) => lower.endsWith(e));
  return hit ?? null;
}

/** The dataset's dominant file ending; falls back to ".nii.gz" when nothing matches. */
export function dominantEnding(files: ImagehubFile[]): FileEnding {
  const counts = new Map<FileEnding, number>();
  for (const f of files) {
    const e = inferEnding(basename(f.logicalPath));
    if (e) counts.set(e, (counts.get(e) ?? 0) + 1);
  }
  let best: FileEnding = '.nii.gz';
  let bestN = -1;
  for (const [e, n] of counts) {
    if (n > bestN) {
      best = e;
      bestN = n;
    }
  }
  return best;
}

// Issue codes that are meaningful against REAL files. Descriptor-level codes are dropped because
// the descriptor (id/name/declared channel + label class-map) is inferred, not user-declared.
const READINESS_CODES = new Set([
  'case.missingChannel',
  'case.missingLabel',
  'case.geometry',
  'case.testLabel',
  'file.lossy',
  'file.ending',
  'cases.dupId',
  'label.rgb',
]);

function filterReadiness(report: ValidationReport): ValidationReport {
  const issues = report.issues.filter((i) => READINESS_CODES.has(i.code));
  const errorCount = issues.filter((i) => i.level === 'error').length;
  const warningCount = issues.filter((i) => i.level === 'warning').length;
  return { issues, errorCount, warningCount, ok: errorCount === 0 };
}

/**
 * A file is the case LABEL when it is EITHER a linked segmentation (fileKind) OR it sits in an
 * nnU-Net label folder (labelsTr / labelsTs). The "Tải nhãn" button uploads masks as plain files
 * into labelsTr/, so the folder convention — not fileKind — is what identifies them as labels.
 */
export function isLabelFile(f: Pick<ImagehubFile, 'fileKind' | 'folderPath'>): boolean {
  if (f.fileKind === 'segmentation') return true;
  const seg = (f.folderPath || '').split('/').filter(Boolean).pop();
  return seg === 'labelsTr' || seg === 'labelsTs';
}

/** Map an ImageHub file to a typed nnU-Net source file (image channel or label). */
function fileToSource(
  f: ImagehubFile,
  ending: FileEnding,
  caseIdByFileId: Map<string, string>,
): SourceFile {
  const name = basename(f.logicalPath);
  const base = {
    id: f.id,
    filename: name,
    sizeBytes: f.size,
    split: 'train' as const, // no split is persisted yet; everything is treated as training data
  };

  if (isLabelFile(f)) {
    // A mask is the label of its parent image's case.
    const parentCase = f.parentFileId ? caseIdByFileId.get(f.parentFileId) : undefined;
    const caseId = parentCase ?? stripEnding(name, ending) ?? name;
    return { ...base, role: 'label', caseId };
  }

  // An image file. A trailing `_XXXX` selects the channel; otherwise it is a single channel-0 image.
  const parsed = parseFilename(name, ending);
  if (parsed?.role === 'image' && parsed.channelIndex != null) {
    return { ...base, role: 'image', caseId: parsed.caseId, channelIndex: parsed.channelIndex };
  }
  const caseId = stripEnding(name, ending) ?? name;
  return { ...base, role: 'image', caseId, channelIndex: 0 };
}

/** Best-effort nnU-Net descriptor inferred from the source files. */
function inferMeta(datasetName: string, ending: FileEnding, sources: SourceFile[]): DatasetMeta {
  const idxs = new Set<number>();
  let anyLabel = false;
  for (const s of sources) {
    if (s.role === 'label') anyLabel = true;
    else if (s.role === 'image' && s.channelIndex != null) idxs.add(s.channelIndex);
  }
  const channels: Channel[] = (idxs.size ? [...idxs] : [0])
    .sort((a, b) => a - b)
    .map((index) => ({ index, name: `Kênh ${index}`, isCT: false }));

  const labels: Label[] = anyLabel
    ? [
        { name: 'background', value: 0 },
        { name: 'nhãn', value: 1 },
      ]
    : [{ name: 'background', value: 0 }];

  const dim: '2d' | '3d' = ['.png', '.bmp', '.tif', '.tiff'].includes(ending) ? '2d' : '3d';
  return {
    id: 1, // placeholder — descriptor-level checks are filtered out, so the value is unused
    name: datasetName || 'Dataset',
    fileEnding: ending,
    dim,
    rgb: false,
    channels,
    labels,
  };
}

/** Build the full workspace model from a dataset's real files. */
export function buildWorkspaceModel(datasetName: string, files: ImagehubFile[]): WorkspaceModel {
  const ending = dominantEnding(files);

  // Resolve each image file's case id first, so a segmentation mask can join its parent's case.
  const caseIdByFileId = new Map<string, string>();
  for (const f of files) {
    if (isLabelFile(f)) continue;
    const name = basename(f.logicalPath);
    const parsed = parseFilename(name, ending);
    caseIdByFileId.set(f.id, parsed?.caseId ?? stripEnding(name, ending) ?? name);
  }

  const sources = files.map((f) => fileToSource(f, ending, caseIdByFileId));
  const meta = inferMeta(datasetName, ending, sources);
  const { cases, unassigned } = groupFilesIntoCases(sources, meta);
  const report = filterReadiness(validateDataset(meta, cases));
  const organized = sources.some((s) => s.role === 'image');

  // Stable order for the table: case id ascending.
  cases.sort((a, b) => a.caseId.localeCompare(b.caseId));

  return { meta, ending, cases, unassigned, report, organized, inferred: true };
}

/** All files attached to a case (channels + label), in channel order. */
export function caseFiles(c: DatasetCase, meta: DatasetMeta): SourceFile[] {
  const out: SourceFile[] = [];
  for (const ch of meta.channels) {
    const f = c.channelFiles[ch.index];
    if (f) out.push(f);
  }
  if (c.labelFile) out.push(c.labelFile);
  return out;
}

export function caseHasLabel(c: DatasetCase): boolean {
  return !!c.labelFile;
}

/**
 * Per-case status for the table: channel completeness + file-ending only. Missing label is a
 * (training-readiness) WARNING, not an error, because most datasets are images-pending-annotation.
 */
export function caseStatus(c: DatasetCase, meta: DatasetMeta): CaseStatus {
  const missingChannel = meta.channels.some((ch) => !c.channelFiles[ch.index]);
  if (missingChannel) return 'error';
  const badEnding = caseFiles(c, meta).some((f) => !f.filename.endsWith(meta.fileEnding));
  if (badEnding) return 'error';
  if (!c.labelFile) return 'warn';
  return 'valid';
}

/** Channel presence map for a case (drives the channel chips). */
export function channelPresence(c: DatasetCase, meta: DatasetMeta): { channel: Channel; present: boolean }[] {
  return meta.channels.map((channel) => ({ channel, present: !!c.channelFiles[channel.index] }));
}

/** Human-readable byte size. */
export function formatBytes(n: number): string {
  if (!Number.isFinite(n) || n < 0) return '—';
  if (n < 1024) return `${n} B`;
  const units = ['KB', 'MB', 'GB', 'TB'];
  let v = n;
  let i = -1;
  do {
    v /= 1024;
    i++;
  } while (v >= 1024 && i < units.length - 1);
  return `${v.toFixed(v < 10 ? 1 : 0)} ${units[i]}`;
}

/** The generated dataset.json preview (inferred descriptor). */
export function datasetJsonPreview(model: WorkspaceModel): Record<string, unknown> {
  const channel_names: Record<string, string> = {};
  [...model.meta.channels]
    .sort((a, b) => a.index - b.index)
    .forEach((c) => (channel_names[String(c.index)] = c.isCT ? 'CT' : c.name));
  const labels: Record<string, number> = {};
  [...model.meta.labels]
    .sort((a, b) => a.value - b.value)
    .forEach((l) => (labels[l.name || `label_${l.value}`] = l.value));
  return {
    channel_names,
    labels,
    numTraining: model.cases.filter((c) => c.split === 'train').length,
    file_ending: model.ending,
  };
}
