// api.ts から分割（挙動不変）: タスクワークスペースのファイル（一覧・内容・Office プレビュー・アップロード/削除/zip）。
import { BASE, triggerBlobDownload } from './client';

// Mirrors the API's safe projection (local-files-api.ts): task IDs + source
// kind + piece/movement + timestamps only. Job UUIDs, checksum, and the
// free-text note are intentionally NOT sent to the client (adversarial-review D4).
export interface FileProvenance {
  relPath: string;
  sourceKind: string;
  createdByTaskId: number | null;
  createdByPiece: string | null;
  createdByMovement: string | null;
  firstSeenAt: string | null;
  lastModifiedByTaskId: number | null;
  lastModifiedAt: string | null;
}

/** Fetch the provenance record for one workspace file (null when untracked). */
export async function fetchFileProvenance(
  taskId: number,
  section: string,
  path: string,
): Promise<FileProvenance | null> {
  const qs = new URLSearchParams({ section, path });
  const res = await fetch(`${BASE}/local/tasks/${taskId}/files/provenance?${qs.toString()}`);
  if (!res.ok) return null;
  const data = await res.json();
  return (data.provenance ?? null) as FileProvenance | null;
}

export interface LocalFileEntry {
  name: string;
  path: string;
  kind: 'directory' | 'file';
  size: number;
  modifiedAt: string;
}

export async function fetchLocalFiles(taskId: number, section: 'workspace' | 'input' | 'output' | 'logs', path: string = ''): Promise<{ basePath: string; path: string; entries: LocalFileEntry[] }> {
  const params = new URLSearchParams({ section });
  if (path) params.set('path', path);
  const res = await fetch(`${BASE}/local/tasks/${taskId}/files?${params.toString()}`);
  const data = await res.json();
  if (!res.ok) throw new Error(data?.error ?? 'Failed to list files');
  return data;
}

export async function fetchLocalFileContent(taskId: number, section: 'workspace' | 'input' | 'output' | 'logs', path: string): Promise<string> {
  const params = new URLSearchParams({ section, path });
  const res = await fetch(`${BASE}/local/tasks/${taskId}/files/content?${params.toString()}`);
  if (!res.ok) {
    const data = await res.json().catch(() => ({}));
    throw new Error(data?.error ?? 'Failed to read file');
  }
  return await res.text();
}

export function getLocalFileRawUrl(taskId: number, section: 'workspace' | 'input' | 'output' | 'logs', path: string): string {
  const params = new URLSearchParams({ section, path });
  return `${BASE}/local/tasks/${taskId}/files/raw?${params.toString()}`;
}

export function getTrustedLocalHtmlUrl(taskId: number, section: 'workspace' | 'input' | 'output' | 'logs', path: string): string {
  const params = new URLSearchParams({ section, path, trusted: '1' });
  return `${BASE}/local/tasks/${taskId}/files/raw?${params.toString()}`;
}

// ── Office プレビュー (Excel / PowerPoint) ───────────────────────────────
// サーバが Excel→シートのセル配列、PPTX→スライド画像・DOCX→ページ画像(PNG data URL)に変換して返す。

export interface OfficeSpreadsheetSheet {
  name: string;
  rows: string[][];
  rowCount: number;
  colCount: number;
  truncated: boolean;
}
export interface OfficeSpreadsheetPreview {
  kind: 'spreadsheet';
  sheets: OfficeSpreadsheetSheet[];
  truncated: boolean;
}
export interface OfficePresentationPreview {
  kind: 'presentation';
  slides: { index: number; dataUrl: string }[];
  slideCount: number;
  truncated: boolean;
}
export interface OfficeDocumentPreview {
  kind: 'document';
  pages: { index: number; dataUrl: string }[];
  pageCount: number;
  truncated: boolean;
}
export type OfficePreview = OfficeSpreadsheetPreview | OfficePresentationPreview | OfficeDocumentPreview;

/** office-preview エンドポイントの失敗を、変換エンジン未導入(503)とそれ以外で区別できる型。 */
export class OfficePreviewError extends Error {
  /** サーバが返した error コード ('converter_unavailable' 等)。 */
  code?: string;
  constructor(message: string, code?: string) {
    super(message);
    this.name = 'OfficePreviewError';
    this.code = code;
  }
}

export async function fetchOfficePreview(url: string): Promise<OfficePreview> {
  const res = await fetch(url);
  if (!res.ok) {
    const data = await res.json().catch(() => ({} as { error?: string; message?: string }));
    throw new OfficePreviewError(data?.message ?? data?.error ?? 'Failed to load preview', data?.error);
  }
  return (await res.json()) as OfficePreview;
}

export function getLocalFileOfficePreviewUrl(taskId: number, section: 'workspace' | 'input' | 'output' | 'logs', path: string): string {
  const params = new URLSearchParams({ section, path });
  return `${BASE}/local/tasks/${taskId}/files/office-preview?${params.toString()}`;
}

// アップロード・削除が許される区分。サーバ側 WRITABLE_SECTIONS と一致させること。
export type WritableTaskSection = 'input' | 'output';

// タスクワークスペースの input/output へファイルをアップロード（複数可）。サーバが
// O_EXCL で衝突回避リネーム + ensurePathWithin で section root に封じ込め。実行中は 409。
export async function uploadLocalFiles(
  taskId: number,
  section: WritableTaskSection,
  path: string,
  files: { name: string; contentBase64: string }[],
): Promise<{ uploaded: { name: string; path: string }[] }> {
  const res = await fetch(`${BASE}/local/tasks/${taskId}/files/upload`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ section, path, files }),
  });
  const data = await res.json();
  if (!res.ok) throw new Error(data?.error ?? 'Failed to upload files');
  return data as { uploaded: { name: string; path: string }[] };
}

// タスクワークスペースの input/output ファイルを削除（複数選択は paths 配列）。サーバ側で
// section root に封じ込め、ファイルのみ対象、存在しないものは冪等スキップ。
export async function deleteLocalFiles(
  taskId: number,
  section: WritableTaskSection,
  paths: string[],
): Promise<{ deleted: string[]; skipped: string[] }> {
  const res = await fetch(`${BASE}/local/tasks/${taskId}/files/delete`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ section, paths }),
  });
  const data = await res.json();
  if (!res.ok) throw new Error(data?.error ?? 'Failed to delete files');
  return data as { deleted: string[]; skipped: string[] };
}


// タスクワークスペースの複数ファイルを zip でまとめてダウンロード（全 section 可、read）。
// サーバが paths を section root に封じ込め、ファイルのみ zip 化。1 件でも zip にする。
export async function downloadLocalFilesZip(
  taskId: number,
  section: 'workspace' | 'input' | 'output' | 'logs',
  paths: string[],
  filename = 'files.zip',
): Promise<void> {
  const res = await fetch(`${BASE}/local/tasks/${taskId}/files/download-zip`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ section, paths }),
  });
  if (!res.ok) {
    const data = await res.json().catch(() => ({}));
    throw new Error(data?.error ?? 'Failed to download files');
  }
  triggerBlobDownload(await res.blob(), filename);
}

export async function updateLocalFileContent(taskId: number, section: string, path: string, content: string): Promise<void> {
  const res = await fetch(`${BASE}/local/tasks/${taskId}/files/content`, {
    method: 'PUT',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ section, path, content }),
  });
  if (!res.ok) {
    const err = await res.json().catch(() => ({ error: res.statusText }));
    throw new Error(err.error || res.statusText);
  }
}
