import { apiClient } from '../api/client';

/** ImageHub dataset visibility (mirrors be0 imagehub_datasets.visibility). */
export type DatasetVisibility = 'private' | 'internal' | 'public';

/** An ImageHub dataset (mirrors be0 `DatasetOut`). */
export type ImagehubDataset = {
  id: string;
  ownerUserId: string;
  ownerEmail?: string | null;
  name: string;
  slug: string;
  description: string;
  visibility: DatasetVisibility;
  modalityTags: string[];
  /** Per-dataset integer-label → name map for multi-label masks (migration 027), e.g. {"1":"kidney"}. */
  labelMap?: Record<string, string>;
  defaultBranch: string;
  /** The research project ("workspace") this dataset belongs to, if any (migration 024). */
  researchProjectId?: string | null;
  fileCount: number;
  versionCount: number;
  createdAt?: string | null;
  updatedAt?: string | null;
};

/** A current working file in a dataset (mirrors be0 `FileOut`). */
export type ImagehubFile = {
  id: string;
  logicalPath: string;
  /** Relative directory inside the dataset ('' = root); preserves an uploaded folder tree. */
  folderPath: string;
  sha256: string;
  size: number;
  mediaType: string;
  imagingMeta: Record<string, unknown>;
  /** 'image' = a regular file; 'segmentation' = an organ mask linked to a parent image. */
  fileKind: 'image' | 'segmentation';
  /** For a segmentation mask: the id of the image file it segments (else null). */
  parentFileId?: string | null;
  /** For a segmentation mask: the organ it delineates (display label). */
  organLabel: string;
  uploadedAt?: string | null;
  downloadUrl?: string | null;
};

/** A frozen version snapshot (mirrors be0 `VersionOut`). */
export type ImagehubVersion = {
  id: string;
  seq: number;
  message: string;
  fileCount: number;
  manifest: Array<Record<string, unknown>>;
  createdAt?: string | null;
};

export type ImagehubAuditEntry = {
  id: number;
  occurredAt?: string | null;
  actorName: string;
  roleLabel: string;
  action: string;
  subject: string;
  detail: string;
};

export type DatasetCreateInput = {
  name: string;
  description?: string;
  visibility?: DatasetVisibility;
  modalityTags?: string[];
  /** Attach the new dataset to a research project ("workspace"). */
  researchProjectId?: string | null;
};

export type DatasetUpdateInput = Partial<DatasetCreateInput> & {
  /** Replace the per-dataset value→name label map (migration 027). */
  labelMap?: Record<string, string>;
};

export type UploadedFileResult = {
  path: string;
  sha256: string;
  size: number;
  deduped: boolean;
  imagingMeta: Record<string, unknown>;
};

export type UploadResult = { ok: boolean; files: UploadedFileResult[] };

const BASE = '/api/v1/datasets';

// --- datasets ---
export function listDatasets(
  opts: { scope?: 'mine' | 'all'; projectId?: string } = {},
): Promise<ImagehubDataset[]> {
  const params: Record<string, string> = {};
  if (opts.scope) params.scope = opts.scope;
  if (opts.projectId) params.projectId = opts.projectId;
  return apiClient.get<ImagehubDataset[]>(BASE, {
    params: Object.keys(params).length ? params : undefined,
  });
}

export function getDataset(id: string): Promise<ImagehubDataset> {
  return apiClient.get<ImagehubDataset>(`${BASE}/${encodeURIComponent(id)}`);
}

export function createDataset(input: DatasetCreateInput): Promise<ImagehubDataset> {
  return apiClient.post<ImagehubDataset>(BASE, input);
}

export function updateDataset(id: string, input: DatasetUpdateInput): Promise<ImagehubDataset> {
  return apiClient.put<ImagehubDataset>(`${BASE}/${encodeURIComponent(id)}`, input);
}

export function deleteDataset(id: string): Promise<{ ok: boolean }> {
  return apiClient.delete(`${BASE}/${encodeURIComponent(id)}`);
}

// --- files (content-addressed upload + browse) ---
export function uploadDatasetFiles(id: string, files: File[], folder?: string): Promise<UploadResult> {
  const form = new FormData();
  const paths: string[] = [];
  for (const f of files) {
    form.append('files', f, f.name);
    // `folder` forces a target directory (e.g. 'imagesTr' / 'labelsTr'). Otherwise a directory
    // picker's webkitRelativePath preserves the tree, and a flat picker resolves to '' (root).
    const rel = (f as File & { webkitRelativePath?: string }).webkitRelativePath || f.name;
    paths.push(folder ? `${folder}/${f.name}` : rel);
  }
  form.append('paths', JSON.stringify(paths));
  // apiClient strips Content-Type for FormData so the browser sets the multipart boundary.
  return apiClient.post<UploadResult>(`${BASE}/${encodeURIComponent(id)}/files`, form);
}

export function listDatasetFiles(id: string): Promise<ImagehubFile[]> {
  return apiClient.get<ImagehubFile[]>(`${BASE}/${encodeURIComponent(id)}/files`);
}

/** One organ mask to link to a parent image (FE input). */
export type SegmentationMaskInput = { file: File; organ: string };

export type SegmentationUploadResult = {
  ok: boolean;
  masks: Array<{ id: string; logicalPath: string; organLabel: string; parentFileId: string | null }>;
};

/**
 * Upload organ-mask files and link them to a parent image
 * (be0 `POST /datasets/{id}/files/{parentFileId}/segmentations`).
 * Appends `masks[i]` and `organs[i]` in lockstep so the server pairs them by index.
 */
export function uploadSegmentationMasks(
  id: string,
  parentFileId: string,
  items: SegmentationMaskInput[],
): Promise<SegmentationUploadResult> {
  const form = new FormData();
  for (const it of items) {
    form.append('masks', it.file, it.file.name);
    form.append('organs', it.organ);
  }
  // apiClient strips Content-Type for FormData so the browser sets the multipart boundary.
  return apiClient.post<SegmentationUploadResult>(
    `${BASE}/${encodeURIComponent(id)}/files/${encodeURIComponent(parentFileId)}/segmentations`,
    form,
  );
}

export function fileDownloadUrl(id: string, fileId: string): Promise<{ url: string; logicalPath: string }> {
  return apiClient.get(`${BASE}/${encodeURIComponent(id)}/files/${encodeURIComponent(fileId)}/download`);
}

export type NormalizeChannelsResult = {
  ok: boolean;
  renamed: number;
  skipped: number;
  files: Array<{ id: string; oldPath: string; newPath: string; folderPath: string }>;
};
/**
 * Rename files to the `{prefix}_{caseID}_0000.{ext}` convention — images get the _0000 channel
 * suffix, labels (labelsTr) stay channel-free, caseID = the file's number 5-digit zero-padded
 * (so e.g. POLYP25_00001_0000.png pairs with POLYP25_00001.png). `prefix` is the lesion+year code
 * (e.g. "POLYP25"). Idempotent; logical rename only (no blob move).
 */
export function normalizeChannels(id: string, prefix: string): Promise<NormalizeChannelsResult> {
  return apiClient.post<NormalizeChannelsResult>(
    `${BASE}/${encodeURIComponent(id)}/files/normalize-channels`,
    { prefix },
  );
}

// --- versions (the snapshot spine) + audit ---
export function createDatasetVersion(id: string, message?: string): Promise<ImagehubVersion> {
  return apiClient.post<ImagehubVersion>(`${BASE}/${encodeURIComponent(id)}/versions`, { message });
}

export function listDatasetVersions(id: string): Promise<ImagehubVersion[]> {
  return apiClient.get<ImagehubVersion[]>(`${BASE}/${encodeURIComponent(id)}/versions`);
}

export function listDatasetAudit(id: string): Promise<ImagehubAuditEntry[]> {
  return apiClient.get<ImagehubAuditEntry[]>(`${BASE}/${encodeURIComponent(id)}/audit`);
}

// --- labeling-pipeline stages (Label -> Review_1 -> Review_2 ...) ---
export type StageKind = 'label' | 'review';

/** A labeling-pipeline stage on a dataset (mirrors be0 `StageOut`). */
export type DatasetStage = {
  id: string;
  name: string;
  kind: StageKind;
  seq: number;
  /** Review threshold % (review stages only; null for label). */
  reviewPercent: number | null;
  /** "Automatic Task Assignment" — auto-assign tasks at this stage. */
  autoAssign: boolean;
};

export type StageCreateInput = {
  name: string;
  kind: StageKind;
  reviewPercent?: number | null;
  autoAssign?: boolean;
};

export type StageUpdateInput = Partial<{
  name: string;
  reviewPercent: number | null;
  autoAssign: boolean;
  seq: number;
}>;

export function listDatasetStages(id: string): Promise<DatasetStage[]> {
  return apiClient.get<DatasetStage[]>(`${BASE}/${encodeURIComponent(id)}/stages`);
}

export function addDatasetStage(id: string, input: StageCreateInput): Promise<DatasetStage> {
  return apiClient.post<DatasetStage>(`${BASE}/${encodeURIComponent(id)}/stages`, input);
}

export function updateDatasetStage(
  id: string,
  stageId: string,
  input: StageUpdateInput,
): Promise<DatasetStage> {
  return apiClient.patch<DatasetStage>(
    `${BASE}/${encodeURIComponent(id)}/stages/${encodeURIComponent(stageId)}`,
    input,
  );
}

export function deleteDatasetStage(id: string, stageId: string): Promise<{ ok: boolean }> {
  return apiClient.delete(`${BASE}/${encodeURIComponent(id)}/stages/${encodeURIComponent(stageId)}`);
}

// --- task pipeline (a file flows Label -> Review_n -> Ground Truth) ---
export type TaskPipelineState = 'inLabel' | 'inReview' | 'groundTruth' | 'issue';
export type TaskQueueStatus = 'assigned' | 'saved' | 'pendingFinalization' | 'skipped';
export type TaskAssignmentMode = 'auto' | 'manual';
export type ReviewDecision = 'accept' | 'acceptWithCorrections' | 'reject';

/** A unit of labeling work — one image file moving through the pipeline (mirrors be0 `TaskOut`). */
export type DatasetTask = {
  id: string;
  name: string;
  fileId: string;
  fileLogicalPath: string;
  currentStageId: string | null;
  currentStageName: string | null;
  pipelineState: TaskPipelineState;
  queueStatus: TaskQueueStatus;
  assigneeUserId: string | null;
  assigneeName: string | null;
  assignmentMode: TaskAssignmentMode;
  priority: number;
  isReferenceStandard: boolean;
  createdAt?: string | null;
  updatedAt?: string | null;
};

export type GenerateTasksResult = { created: number; total: number };

export type TaskListFilter = {
  stage?: string;
  status?: TaskQueueStatus;
  state?: TaskPipelineState;
  reference?: boolean;
  /** Only tasks assigned to the caller (the labeler's own queue). */
  mine?: boolean;
};

/** Create one task per image file that doesn't have one (requires >=1 stage; idempotent). */
export function generateTasks(id: string): Promise<GenerateTasksResult> {
  return apiClient.post<GenerateTasksResult>(`${BASE}/${encodeURIComponent(id)}/tasks/generate`);
}

export function listTasks(id: string, filter: TaskListFilter = {}): Promise<DatasetTask[]> {
  const params: Record<string, string> = {};
  if (filter.stage) params.stage = filter.stage;
  if (filter.status) params.status = filter.status;
  if (filter.state) params.state = filter.state;
  if (filter.reference != null) params.reference = String(filter.reference);
  if (filter.mine) params.mine = 'true';
  return apiClient.get<DatasetTask[]>(`${BASE}/${encodeURIComponent(id)}/tasks`, {
    params: Object.keys(params).length ? params : undefined,
  });
}

function taskAction(id: string, taskId: string, action: string, body?: unknown): Promise<DatasetTask> {
  return apiClient.post<DatasetTask>(
    `${BASE}/${encodeURIComponent(id)}/tasks/${encodeURIComponent(taskId)}/${action}`,
    body,
  );
}

/** Assign a task. Omit userId to claim it yourself; pass a member's userId (owner/admin) to assign them. */
export const assignTask = (id: string, taskId: string, userId?: string): Promise<DatasetTask> =>
  taskAction(id, taskId, 'assign', userId ? { userId } : undefined);
/** Clear a task's assignee (back to the auto pool). */
export const unassignTask = (id: string, taskId: string): Promise<DatasetTask> =>
  taskAction(id, taskId, 'unassign');
/** TP1: finalize a Label task — advance to the next stage (or Ground Truth). */
export const finalizeTask = (id: string, taskId: string): Promise<DatasetTask> =>
  taskAction(id, taskId, 'finalize');
/** TP2/TP3: accept (optionally with corrections) advances; reject returns to the first stage. */
export const reviewTask = (
  id: string,
  taskId: string,
  decision: ReviewDecision,
  note?: string,
): Promise<DatasetTask> => taskAction(id, taskId, 'review', { decision, note });
/** Q2: send a task to the end of the queue (still owned). */
export const skipTask = (id: string, taskId: string): Promise<DatasetTask> =>
  taskAction(id, taskId, 'skip');
/** PR4: set a task's priority (float 0..1; higher floats to the top of queues). */
export const setTaskPriority = (id: string, taskId: string, priority: number): Promise<DatasetTask> =>
  taskAction(id, taskId, 'priority', { priority });
/** RS1/RS2: set or unset a Ground Truth task as a project reference standard. */
export const setTaskReference = (
  id: string,
  taskId: string,
  isReferenceStandard: boolean,
): Promise<DatasetTask> => taskAction(id, taskId, 'reference', { isReferenceStandard });

/** Accept / acceptWithCorrections / reject tallies for the productivity panel (be0 ReviewStatsOut). */
export type ReviewStats = { accepted: number; acceptWithCorrections: number; rejected: number };

/** Per-reviewer review-decision counts over the last `days` (omit userId for dataset-wide). */
export function getReviewStats(
  id: string,
  opts: { userId?: string; days?: number } = {},
): Promise<ReviewStats> {
  const params: Record<string, string> = {};
  if (opts.userId) params.userId = opts.userId;
  if (opts.days != null) params.days = String(opts.days);
  return apiClient.get<ReviewStats>(`${BASE}/${encodeURIComponent(id)}/review-stats`, {
    params: Object.keys(params).length ? params : undefined,
  });
}

/** A single task plus its saved annotations (mirrors be0 `TaskDetailOut`). */
export type DatasetTaskDetail = DatasetTask & { annotations: unknown[] };

/** Fetch one task with its saved annotations (for the AnnotationTool). */
export function getTask(id: string, taskId: string): Promise<DatasetTaskDetail> {
  return apiClient.get<DatasetTaskDetail>(
    `${BASE}/${encodeURIComponent(id)}/tasks/${encodeURIComponent(taskId)}`,
  );
}

/** Persist a task's annotations. submit=true marks the draft pendingFinalization (Q1); else saved (Q3). */
export function saveTask(
  id: string,
  taskId: string,
  annotations: unknown[],
  submit = false,
): Promise<DatasetTaskDetail> {
  return apiClient.post<DatasetTaskDetail>(
    `${BASE}/${encodeURIComponent(id)}/tasks/${encodeURIComponent(taskId)}/save`,
    { annotations, submit },
  );
}

// --- dataset members (multi-labeler) ---
/** A dataset member (mirrors be0 `MemberOut`). */
export type DatasetMember = {
  userId: string;
  email: string;
  fullName: string;
  role: string;
  createdAt?: string | null;
};

export function listMembers(id: string): Promise<DatasetMember[]> {
  return apiClient.get<DatasetMember[]>(`${BASE}/${encodeURIComponent(id)}/members`);
}

/** Add a member by email (owner/admin/project-admin). role 'project_admin' is owner/admin-only. */
export function addMember(
  id: string,
  email: string,
  role: 'member' | 'project_admin' = 'member',
): Promise<DatasetMember> {
  return apiClient.post<DatasetMember>(`${BASE}/${encodeURIComponent(id)}/members`, { email, role });
}

export function removeMember(id: string, userId: string): Promise<{ ok: boolean }> {
  return apiClient.delete(`${BASE}/${encodeURIComponent(id)}/members/${encodeURIComponent(userId)}`);
}

// --- display helpers ---
export const VISIBILITY_LABEL: Record<DatasetVisibility, string> = {
  private: 'Riêng tư',
  internal: 'Nội bộ',
  public: 'Công khai',
};

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