// Shared model + (de)serialization for the FlowDesigner.
//
// The visual editor works on a normalized in-memory model:
//
//   {
//     name, description,
//     data:   { ... },                                 // top-level constants
//     meta: {
//       prompt:    "...",                              // AI generation prompt
//       positions: { <nodeName>: { x, y } },           // canvas layout
//     },
//     nodes: [{ name, action, description, inputs, outputs,
//                executeIf, retry, retryDelay, onError, batchOver }],
//     edges: [{ from, to }],
//   }
//
// The on-disk + on-the-wire format is **JSON**. (Earlier revisions stored
// YAML; that format is gone end-to-end.)
//
// `version` is intentionally NOT part of the authored model — it is managed
// server-side (auto-incremented per save). Legacy files that carry a
// `version` key are accepted on parse but the value is dropped on
// re-serialization, so saved JSON never contains it.

export function emptyModel(name = "new-flow") {
  return {
    name,
    description: "",
    data: {},
    meta: { prompt: "", positions: {} },
    nodes: [
      {
        name: "hello",
        action: "log",
        description: "",
        inputs: { message: "hi" },
        outputs: {},
        executeIf: "",
        retry: 0,
        retryDelay: 0,
        onError: "terminate",
        batchOver: "",
      },
    ],
    edges: [],
  };
}

/**
 * Parse a DSL string into the normalized model.
 *
 * Accepts:
 *   - JSON text (preferred)
 *   - an empty string  → empty model
 *
 * Throws on malformed JSON.
 */
export function parseDslToModel(text) {
  if (text == null || text === "") return normalize({});
  let parsed;
  try {
    parsed = JSON.parse(text);
  } catch (e) {
    throw new Error(`Invalid JSON: ${e.message}`);
  }
  return normalize(parsed || {});
}

// Backwards-compatible alias for the old callsites that still import the
// YAML-named function. New code should call parseDslToModel().
export const parseYamlToModel = parseDslToModel;

/** Take any (parsed or raw) shape and produce the normalized in-memory model. */
export function normalize(parsed) {
  const meta = { prompt: "", positions: {}, ...(parsed.meta || {}) };
  return {
    name:        parsed.name || "untitled",
    description: parsed.description || "",
    data:        parsed.data || {},
    meta,
    nodes:       (parsed.nodes || []).map(normalizeNode),
    edges:       (parsed.edges || []).map(e => ({ from: e.from, to: e.to })),
  };
}

function normalizeNode(n) {
  return {
    name:        n.name || "",
    action:      n.action || "",
    description: n.description || "",
    inputs:      kvFromAny(n.inputs),
    outputs:     kvFromAny(n.outputs),
    executeIf:   n.executeIf || "",
    retry:       n.retry || 0,
    retryDelay:  n.retryDelay || 0,
    onError:     n.onError || "terminate",
    batchOver:   n.batchOver || "",
    outputVar:   n.outputVar || "",
  };
}

/** Accept either object form { k: v } or array form [{ k: v }] and return a flat object. */
function kvFromAny(value) {
  if (!value) return {};
  if (Array.isArray(value)) {
    const out = {};
    for (const item of value) {
      if (item && typeof item === "object") Object.assign(out, item);
    }
    return out;
  }
  return { ...value };
}

/**
 * Serialize the normalized model to JSON.
 *
 * Empty / default fields are pruned so the output stays compact and human-
 * readable. The `inputs` / `outputs` maps are emitted as plain objects
 * (the engine accepts both forms but objects are the natural JSON shape).
 */
export function serializeModelToDsl(model) {
  const out = { name: model.name };
  if (model.description) out.description = model.description;

  const meta = {};
  if (model.meta?.prompt) meta.prompt = model.meta.prompt;
  if (model.meta?.positions && Object.keys(model.meta.positions).length) {
    meta.positions = model.meta.positions;
  }
  if (Object.keys(meta).length) out.meta = meta;

  if (model.data && Object.keys(model.data).length) out.data = model.data;

  out.nodes = (model.nodes || []).map(serializeNode);
  if (model.edges?.length) out.edges = model.edges.map(e => ({ from: e.from, to: e.to }));

  return JSON.stringify(out, null, 2);
}

// Backwards-compatible alias for the old callsites.
export const serializeModelToYaml = serializeModelToDsl;

function serializeNode(n) {
  const out = { name: n.name, action: n.action };
  if (n.description)     out.description = n.description;
  if (n.inputs  && Object.keys(n.inputs).length)  out.inputs  = { ...n.inputs };
  if (n.outputs && Object.keys(n.outputs).length) out.outputs = { ...n.outputs };
  if (n.executeIf)              out.executeIf  = n.executeIf;
  if (n.retry)                  out.retry      = n.retry;
  if (n.retryDelay)             out.retryDelay = n.retryDelay;
  if (n.onError && n.onError !== "terminate") out.onError = n.onError;
  if (n.batchOver)              out.batchOver  = n.batchOver;
  if (n.outputVar)              out.outputVar  = n.outputVar;
  return out;
}

/** Generate a unique node name based on a prefix (action shortname). */
export function uniqueNodeName(model, prefix) {
  const taken = new Set((model.nodes || []).map(n => n.name));
  const base = (prefix || "node").split(".").pop().replace(/[^A-Za-z0-9_-]/g, "_") || "node";
  if (!taken.has(base)) return base;
  for (let i = 2; i < 1000; i++) {
    const candidate = `${base}-${i}`;
    if (!taken.has(candidate)) return candidate;
  }
  return base + "-" + Date.now();
}

/** Trigger a browser download of `text` as `filename`. */
export function downloadText(filename, text, mime = "application/json") {
  const blob = new Blob([text], { type: mime });
  const url = URL.createObjectURL(blob);
  const a = document.createElement("a");
  a.href = url;
  a.download = filename;
  document.body.appendChild(a);
  a.click();
  document.body.removeChild(a);
  URL.revokeObjectURL(url);
}

/** Read a single user-selected file as text. Returns a Promise<string>. */
export function pickFileAsText(accept = ".json,.txt") {
  return new Promise((resolve, reject) => {
    const input = document.createElement("input");
    input.type = "file";
    input.accept = accept;
    input.onchange = () => {
      const file = input.files?.[0];
      if (!file) return resolve(null);
      const reader = new FileReader();
      reader.onload = () => resolve(String(reader.result || ""));
      reader.onerror = () => reject(reader.error);
      reader.readAsText(file);
    };
    input.click();
  });
}
