import { readFile } from "node:fs/promises";
import { PilotDeckToolRuntimeError } from "../../protocol/errors.js";

type NotebookCell = {
  cell_type?: string;
  source?: string[] | string;
  outputs?: Array<{
    text?: string[] | string;
    data?: Record<string, unknown>;
    ename?: string;
    evalue?: string;
    traceback?: string[];
  }>;
  execution_count?: number | null;
};

type NotebookFile = {
  cells?: NotebookCell[];
};

export type NotebookReadResult = {
  text: string;
  cellCount: number;
};

export async function readNotebook(filePath: string): Promise<NotebookReadResult> {
  const raw = await readFile(filePath, "utf8").catch((error: unknown) => {
    if (isNodeError(error) && error.code === "ENOENT") {
      throw new PilotDeckToolRuntimeError("file_not_found", `File ${filePath} does not exist.`);
    }
    throw error;
  });

  let notebook: NotebookFile;
  try {
    notebook = JSON.parse(raw) as NotebookFile;
  } catch (error) {
    throw new PilotDeckToolRuntimeError(
      "invalid_tool_input",
      `Notebook ${filePath} is not valid JSON.`,
      { cause: error instanceof Error ? error.message : String(error) },
    );
  }

  const cells = notebook.cells ?? [];
  const lines: string[] = [];
  for (let index = 0; index < cells.length; index += 1) {
    const cell = cells[index] ?? {};
    lines.push(`# Cell ${index} (${cell.cell_type ?? "unknown"})`);
    if (cell.execution_count !== undefined && cell.execution_count !== null) {
      lines.push(`execution_count: ${cell.execution_count}`);
    }
    const source = normalizeMultiline(cell.source);
    if (source) {
      lines.push(source);
    }
    const outputs = formatOutputs(cell.outputs ?? []);
    if (outputs) {
      lines.push("## Outputs");
      lines.push(outputs);
    }
    if (index < cells.length - 1) {
      lines.push("");
    }
  }

  return {
    text: lines.join("\n"),
    cellCount: cells.length,
  };
}

function formatOutputs(outputs: NotebookCell["outputs"]): string {
  const chunks: string[] = [];
  for (const output of outputs ?? []) {
    const text = normalizeMultiline(output.text);
    if (text) {
      chunks.push(text);
      continue;
    }
    if (Array.isArray(output.traceback) && output.traceback.length > 0) {
      chunks.push(output.traceback.join("\n"));
      continue;
    }
    if (output.ename || output.evalue) {
      chunks.push(`${output.ename ?? "Error"}: ${output.evalue ?? ""}`.trim());
      continue;
    }
    const plain = output.data?.["text/plain"];
    const plainText = normalizeMultiline(plain as string[] | string | undefined);
    if (plainText) {
      chunks.push(plainText);
    }
  }
  return chunks.join("\n");
}

function normalizeMultiline(value: string[] | string | undefined): string {
  if (Array.isArray(value)) {
    return value.join("");
  }
  return typeof value === "string" ? value : "";
}

function isNodeError(error: unknown): error is NodeJS.ErrnoException {
  return error instanceof Error && "code" in error;
}