/* global window, document, requestAnimationFrame */
// AgentUI serialization helpers extracted from editor.html
// Exposes window.AgentUISer.{toPipelineJSON, fromPipelineJSON}
// Depends on globals defined by editor.html: editor, NODE_IO, addNode, applyNodeDefaults, status
(function (w) {
  'use strict';

  function ensureDeps() {
    if (!w || !w.editor) throw new Error('AgentUISer: global editor is not available');
    if (!w.NODE_IO) throw new Error('AgentUISer: global NODE_IO is not available');
  }

  // Top-level pipeline meta kept in memory and included into JSON on save.
  // Allows UI to edit loop parameters without manual JSON edits.
  // DRY: единый источник дефолтов и нормализации meta
  const MetaDefaults = Object.freeze({
    id: 'pipeline_editor',
    name: 'Edited Pipeline',
    parallel_limit: 8,
    loop_mode: 'dag',
    loop_max_iters: 1000,
    loop_time_budget_ms: 10000,
    clear_var_store: true,
    http_timeout_sec: 60,
    text_extract_strategy: 'auto',
    text_extract_json_path: '',
    text_join_sep: '\n',
    // v2: коллекция пресетов извлечения текста, управляется в "Запуск"
    // [{ id, name, strategy, json_path, join_sep }]
    text_extract_presets: [],
  });

  let _pipelineMeta = { ...MetaDefaults };

  // Нормализатор meta: приводит типы, поддерживает синонимы ключей, заполняет дефолты
  function ensureMeta(p) {
    const src = (p && typeof p === 'object') ? p : {};
    const out = { ...MetaDefaults };

    // helpers
    const toInt = (v, def) => {
      try {
        const n = parseInt(v, 10);
        return Number.isFinite(n) && n > 0 ? n : def;
      } catch { return def; }
    };
    const toNum = (v, def) => {
      try {
        const n = parseFloat(v);
        return !Number.isNaN(n) && n > 0 ? n : def;
      } catch { return def; }
    };

    // базовые поля
    try { out.id = String((src.id ?? out.id) || out.id); } catch {}
    try { out.name = String((src.name ?? out.name) || out.name); } catch {}

    out.parallel_limit       = toInt(src.parallel_limit, out.parallel_limit);
    out.loop_mode            = String((src.loop_mode ?? out.loop_mode) || out.loop_mode);
    out.loop_max_iters       = toInt(src.loop_max_iters, out.loop_max_iters);
    out.loop_time_budget_ms  = toInt(src.loop_time_budget_ms, out.loop_time_budget_ms);
    out.clear_var_store      = (typeof src.clear_var_store === 'boolean') ? !!src.clear_var_store : out.clear_var_store;
    out.http_timeout_sec     = toNum(src.http_timeout_sec, out.http_timeout_sec);
    out.text_extract_strategy    = String((src.text_extract_strategy ?? out.text_extract_strategy) || out.text_extract_strategy);
    out.text_extract_json_path   = String((src.text_extract_json_path ?? out.text_extract_json_path) || out.text_extract_json_path);

    // поддержка синонимов text_join_sep (регистр и вариации)
    let joinSep = out.text_join_sep;
    try {
      for (const k of Object.keys(src)) {
        if (String(k).toLowerCase() === 'text_join_sep') { joinSep = src[k]; break; }
      }
    } catch {}
    out.text_join_sep = String((joinSep ?? src.text_join_sep ?? out.text_join_sep) || out.text_join_sep);

    // коллекция пресетов
    try {
      const arr = Array.isArray(src.text_extract_presets) ? src.text_extract_presets : [];
      out.text_extract_presets = arr
        .filter(it => it && typeof it === 'object')
        .map((it, idx) => ({
          id: String((it.id ?? '') || ('p' + Date.now().toString(36) + Math.random().toString(36).slice(2) + idx)),
          name: String(it.name ?? (it.json_path || 'Preset')),
          strategy: String(it.strategy ?? 'auto'),
          json_path: String(it.json_path ?? ''),
          join_sep: String(it.join_sep ?? '\n'),
        }));
    } catch { out.text_extract_presets = []; }

    return out;
  }

  function getPipelineMeta() {
    return { ..._pipelineMeta };
  }

  function updatePipelineMeta(p) {
    if (!p || typeof p !== 'object') return;
    // DRY: единая точка нормализации
    _pipelineMeta = ensureMeta({ ..._pipelineMeta, ...p });
  }

  // Drawflow -> pipeline JSON
  function toPipelineJSON() {
    ensureDeps();
    const editor = w.editor;
    const NODE_IO = w.NODE_IO;

    const data = editor.export();
    const nodes = [];
    const idMap = {}; // drawflow id -> generated id like n1, n2

    const dfNodes = (data && data.drawflow && data.drawflow.Home && data.drawflow.Home.data) ? data.drawflow.Home.data : {};

    // 1) Собираем ноды с устойчивыми id на основе _origId (если валиден/уникален)
    const usedIds = new Set();
    const wantIds = {}; // drawflow id -> желаемый/финальный nX
    const isValidNid = (s) => typeof s === 'string' && /^n\d+$/i.test(s.trim());
  
    // Helper: вернуть исключительно «живые» данные ноды из редактора (если доступны).
    // Это исключает расхождения между DOM.__data и editor.getNodeFromId(..).data.
    function mergedNodeData(df, el, dfid) {
      try {
        const nid = parseInt(dfid, 10);
        const n = (w.editor && typeof w.editor.getNodeFromId === 'function') ? w.editor.getNodeFromId(nid) : null;
        if (n && n.data) return n.data;
      } catch (_) {}
      if (df && df.data) return df.data;
      // как последний fallback — DOM.__data (почти не используется после этого изменения)
      return (el && el.__data) ? el.__data : {};
    }

    // Первый проход: резервируем существующие валидные _origId
    for (const id in dfNodes) {
      const df = dfNodes[id];
      const el = document.querySelector(`#node-${id}`);
      const datacopySrc = mergedNodeData(df, el, id);
      const tmp = typeof w.applyNodeDefaults === 'function'
        ? w.applyNodeDefaults(df.name, JSON.parse(JSON.stringify(datacopySrc)))
        : (JSON.parse(JSON.stringify(datacopySrc)));
      let desired = (tmp && typeof tmp._origId === 'string') ? String(tmp._origId).trim() : '';
      if (isValidNid(desired) && !usedIds.has(desired)) {
        wantIds[id] = desired;
        usedIds.add(desired);
      } else {
        wantIds[id] = null; // назначим позже
      }
    }
    // Поиск ближайшего свободного nX
    function nextFreeId() {
      let x = 1;
      while (usedIds.has('n' + x)) x += 1;
      return 'n' + x;
    }
    // Второй проход: назначаем конфликты/пустые
    for (const id in dfNodes) {
      if (!wantIds[id]) {
        const nid = nextFreeId();
        wantIds[id] = nid;
        usedIds.add(nid);
      }
      idMap[id] = wantIds[id];
    }
    // Финальный проход: формируем массив нод, синхронизируя _origId
    for (const id in dfNodes) {
      const df = dfNodes[id];
      const el = document.querySelector(`#node-${id}`);
      const datacopySrc = mergedNodeData(df, el, id);
      const datacopy = typeof w.applyNodeDefaults === 'function'
        ? w.applyNodeDefaults(df.name, JSON.parse(JSON.stringify(datacopySrc)))
        : (JSON.parse(JSON.stringify(datacopySrc)));
      try { datacopy._origId = idMap[id]; } catch (e) {}

      // Спец-обработка SetVars: гарантированно берём свежие variables из живых данных редактора
      try {
        if (String(df.name) === 'SetVars') {
          const nid = parseInt(id, 10);
          const nLive = (w.editor && typeof w.editor.getNodeFromId === 'function') ? w.editor.getNodeFromId(nid) : null;
          const v = nLive && nLive.data && Array.isArray(nLive.data.variables) ? nLive.data.variables : (Array.isArray(datacopy.variables) ? datacopy.variables : []);
          datacopy.variables = v.map(x => ({ ...(x || {}) })); // глубокая копия
        }
      } catch (_) {}

      nodes.push({
        id: idMap[id],
        type: df.name,
        pos_x: df.pos_x,
        pos_y: df.pos_y,
        config: datacopy,
        in: {}
      });
    }
    try { console.debug('[AgentUISer.toPipelineJSON] idMap drawflowId->nX', idMap); } catch (e) {}

    // 2) Восстанавливаем связи по входам (inputs)
    // В Drawflow v0.0.55 inputs/outputs — это объекты вида input_1/output_1
    for (const id in dfNodes) {
      const df = dfNodes[id];
      const targetNode = nodes.find(n => n.id === idMap[id]);
      if (!targetNode) continue;
      const io = NODE_IO[targetNode.type] || { inputs: [], outputs: [] };

      for (let i = 0; i < io.inputs.length; i++) {
        const inputKey = `input_${i + 1}`;
        const input = df.inputs && df.inputs[inputKey];
        if (!input || !Array.isArray(input.connections) || input.connections.length === 0) continue;

        // Собираем все связи этого входа и сохраняем строку либо массив строк (для depends поддерживаем мульти-коннекты)
        const refs = [];
        for (const conn of (input.connections || [])) {
          if (!conn) continue;
          const sourceDfId = String(conn.node);
          const outKey = String(conn.output ?? '');

          // 1) Попробуем определить индекс выхода из conn.output
          let sourceOutIdx = -1;
          let m = outKey.match(/output_(\d+)/);
          if (m) {
            sourceOutIdx = parseInt(m[1], 10) - 1;
          } else if (/^\d+$/.test(outKey)) {
            sourceOutIdx = parseInt(outKey, 10) - 1;
          } else if (typeof conn.output === 'number') {
            sourceOutIdx = conn.output - 1;
          }

          const sourceNode = nodes.find(n => n.id === idMap[sourceDfId]);
          if (!sourceNode) continue;
          const sourceIo = NODE_IO[sourceNode.type] || { outputs: [] };

          // 2) Fallback: если индекс не распознан или вне диапазона — проверим dfNodes[source].outputs
          if (!(sourceOutIdx >= 0) || !(Array.isArray(sourceIo.outputs) && sourceIo.outputs[sourceOutIdx] != null)) {
            try {
              const srcDf = dfNodes[sourceDfId];
              const outsObj = (srcDf && srcDf.outputs) ? srcDf.outputs : {};
              let found = -1;
              // Текущая целевая drawflow-нода — это id (внешняя переменная цикла по dfNodes)
              const tgtDfId = id;
              for (const k of Object.keys(outsObj || {})) {
                const conns = (outsObj[k] && Array.isArray(outsObj[k].connections)) ? outsObj[k].connections : [];
                if (conns.some(c => String(c && c.node) === String(tgtDfId))) {
                  const m2 = String(k).match(/output_(\d+)/);
                  if (m2) { found = parseInt(m2[1], 10) - 1; break; }
                }
              }
              if (found >= 0) sourceOutIdx = found;
            } catch (e) {
              // ignore
            }
          }

          // 3) Ещё один safety: если до сих пор индекс невалидный — зажмём в границы
          if (!(sourceOutIdx >= 0)) sourceOutIdx = 0;
          if (Array.isArray(sourceIo.outputs) && sourceIo.outputs.length > 0) {
            if (sourceOutIdx >= sourceIo.outputs.length) sourceOutIdx = sourceIo.outputs.length - 1;
          }

          // 4) Вычислим каноническое имя выхода по NODE_IO
          let sourceOutName;
          if (Array.isArray(sourceIo.outputs) && sourceIo.outputs[sourceOutIdx] != null) {
            sourceOutName = sourceIo.outputs[sourceOutIdx];
          } else {
            // Fallback на технические имена (совместимость со старыми out0/out1)
            sourceOutName = `out${sourceOutIdx}`;
          }

          refs.push(`${sourceNode.id}.${sourceOutName}`);
        }

        // Каноничное имя входа: по NODE_IO, иначе in{0-based}
        const targetInName = (io.inputs && io.inputs[i] != null)
          ? io.inputs[i]
          : `in${i}`;

        if (!targetNode.in) targetNode.in = {};
        targetNode.in[targetInName] = (refs.length <= 1 ? refs[0] : refs);
      }
    }

    // 3) Собираем итоговый pipeline JSON с метаданными (нормализованными)
    const meta = ensureMeta(getPipelineMeta());
    try { console.debug('[AgentUISer.toPipelineJSON] meta_keys', Object.keys(meta || {})); } catch (e) {}
    return { ...meta, nodes };
  }

  // pipeline JSON -> Drawflow
  async function fromPipelineJSON(p) {
    ensureDeps();
    const editor = w.editor;
    const NODE_IO = w.NODE_IO;
// Сохраняем метаданные пайплайна для UI (сквозная нормализация)
try {
  updatePipelineMeta(p || {});
  // Диагностический лог состава meta для подтверждения DRY-рефакторинга
  try {
    const metaKeys = ["id","name","parallel_limit","loop_mode","loop_max_iters","loop_time_budget_ms","clear_var_store","http_timeout_sec","text_extract_strategy","text_extract_json_path","text_join_sep","text_extract_presets"];
    const incomingKeys = metaKeys.filter(k => (p && Object.prototype.hasOwnProperty.call(p, k)));
    const currentMeta = (typeof getPipelineMeta === 'function') ? getPipelineMeta() : {};
    console.debug('[AgentUISer.fromPipelineJSON] meta_keys', {
      incomingKeys,
      resultKeys: Object.keys(currentMeta || {}),
      metaPreview: {
        id: currentMeta && currentMeta.id,
        loop_mode: currentMeta && currentMeta.loop_mode,
        http_timeout_sec: currentMeta && currentMeta.http_timeout_sec
      }
    });
  } catch (_) {}
} catch (e) {}

    editor.clear();
    let x = 100; let y = 120; // Fallback
    const idMap = {}; // pipeline id -> drawflow id
    const logs = [];
    const $ = (sel) => document.querySelector(sel);

    const resolveOutIdx = (type, outName) => {
      const outs = ((NODE_IO[type] && NODE_IO[type].outputs) || []);
      let idx = outs.indexOf(outName);
      if (idx < 0 && typeof outName === 'string') {
        // поддержка: out-1, out_1, output_1, out1, out0
        const s = String(outName);
        let m = s.match(/^out(?:put)?[_-]?(\d+)$/);
        if (m) {
          const n = parseInt(m[1], 10);
          idx = n > 0 ? n - 1 : 0;
        } else {
          m = s.match(/^out(\d+)$/); // совместимость со старым out0
          if (m) idx = parseInt(m[1], 10) | 0;
        }
      }
      return idx;
    };
    const resolveInIdx = (type, inName) => {
      const ins = ((NODE_IO[type] && NODE_IO[type].inputs) || []);
      let idx = ins.indexOf(inName);
      if (idx < 0 && typeof inName === 'string') {
        // поддержка: in-1, in_1, in1, in0
        const s = String(inName);
        let m = s.match(/^in[_-]?(\d+)$/);
        if (m) {
          const n = parseInt(m[1], 10);
          idx = n > 0 ? n - 1 : 0;
        } else {
          m = s.match(/^in(\d+)$/); // совместимость со старым in0
          if (m) idx = parseInt(m[1], 10) | 0;
        }
      }
      return idx;
    };

    // Ожидание появления порта в DOM (устранение гонки рендера)
    async function waitForPort(dfid, kind, idx, tries = 60, delay = 16) {
      // Drawflow создаёт DOM-узел с id="node-${dfid}"
      const sel = `#node-${dfid} .${kind}_${idx}`;
      for (let i = 0; i < tries; i++) {
        if ($(sel)) return true;
        await new Promise(r => setTimeout(r, delay));
      }
      logs.push(`port missing: #${dfid} ${kind}_${idx}`);
      return false;
    }
    // Повторные попытки соединить порты, пока DOM не готов
    async function connectWithRetry(srcDfId, tgtDfId, outNum, inNum, tries = 120, delay = 25) {
      const outClass = `output_${outNum}`;
      const inClass  = `input_${inNum}`;
      for (let i = 0; i < tries; i++) {
        const okOut = await waitForPort(srcDfId, 'output', outNum, 1, delay);
        const okIn  = await waitForPort(tgtDfId, 'input',  inNum, 1, delay);
        if (okOut && okIn) {
          try {
            editor.addConnection(srcDfId, tgtDfId, outClass, inClass);
            return true;
          } catch (e) {
            // retry on next loop
          }
        }
        await new Promise(r => setTimeout(r, delay));
      }
      return false;
    }

    // 1) Создаём ноды
    for (const n of p.nodes) {
      const pos = { x: n.pos_x || x, y: n.pos_y || y };
      const data = { ...(n.config || {}), _origId: n.id };
      const dfid = typeof w.addNode === 'function'
        ? w.addNode(n.type, pos, data)
        : (function () { throw new Error('AgentUISer: addNode() is not defined'); })();
      idMap[n.id] = dfid;
      if (!n.pos_x) x += 260; // раскладываем по горизонтали, если нет сохраненной позиции
    }

    // 2) Дождёмся полного рендера DOM
    await new Promise(r => setTimeout(r, 0));
    if (typeof requestAnimationFrame === 'function') {
      await new Promise(r => requestAnimationFrame(() => r()));
      await new Promise(r => requestAnimationFrame(() => r())); // двойной rAF для надежности
    } else {
      await new Promise(r => setTimeout(r, 32));
    }
    // 3) Проставляем связи из in (поддержка строк и массивов ссылок)
    for (const n of p.nodes) {
      if (!n.in) continue;
      const targetDfId = idMap[n.id];
      const targetIo = NODE_IO[n.type] || { inputs: [] };
      for (const [inName, ref] of Object.entries(n.in)) {
        const refs = Array.isArray(ref) ? ref : [ref];
        for (const oneRef of refs) {
          if (!oneRef || typeof oneRef !== 'string' || !oneRef.includes('.')) continue;
          const [srcId, outName] = oneRef.split('.');
          const sourceDfId = idMap[srcId];
          if (!sourceDfId) { logs.push(`skip: src ${srcId} not found`); continue; }
          const srcType = p.nodes.find(nn => nn.id === srcId)?.type;
  
          let outIdx = resolveOutIdx(srcType, outName);
          let inIdx  = resolveInIdx(n.type, inName);
  
          // Fallback на первый порт, если неизвестные имена, но порт существует
          if (outIdx < 0) outIdx = 0;
          if (inIdx  < 0) inIdx  = 0;
  
          const ok = await connectWithRetry(sourceDfId, targetDfId, outIdx + 1, inIdx + 1, 200, 25);
          if (ok) {
            logs.push(`connect: ${srcId}.${outName} (#${sourceDfId}.output_${outIdx + 1}) -> ${n.id}.${inName} (#${targetDfId}.input_${inIdx + 1})`);
          } else {
            logs.push(`skip connect (ports not ready after retries): ${srcId}.${outName} -> ${n.id}.${inName}`);
          }
        }
      }
    }
 
    // 4) Обновим линии и выведем лог
    try {
      Object.values(idMap).forEach((dfid) => {
        if (editor.updateConnectionNodes) {
          editor.updateConnectionNodes(`node-${dfid}`);
        }
      });
    } catch {}
    if (logs.length) {
      try { (typeof w.status === 'function') && w.status('Загружено (links):\n' + logs.join('\n')); } catch {}
      try { console.debug('[fromPipelineJSON]', logs); } catch {}
    }
  }

  w.AgentUISer = {
    toPipelineJSON,
    fromPipelineJSON,
    getPipelineMeta,
    updatePipelineMeta,
  };
})(window);