/**
 * Общие info_*-хелперы для ФО / инфокартины и связки с ЭП.
 */
(function (global) {
  'use strict';

  /**
   * Сформировать и запустить автоматизм с периодом ожидания ответа по кадру ЭП; цикл отметить expired и снять с главного.
   * Единая точка для ур.1 (после выбора автоматизма), ур.2 (выживание), ур.3 (правило из ЭП).
   *
   * Ответ оператора закрывает кадр в ЭП; пайплайн ур.2 по этому циклу вызывается из episodic_memory.closeLastFrame
   * (иначе при expired и не-главном цикле runOneStep до него не доходит).
   *
   * @param {Object} cycle — цикл осмысления
   * @param {number} branchId — узел дерева восприятия
   * @param {number} actionImageId — ОД
   * @param {number} automatizmId — ID автоматизма (уже созданного или штатного)
   * @param {number} goalImageId — образ цели цикла (0, если нет)
   * @param {string} logMessage — строка в лог цикла (force)
   * @returns {boolean}
   */
  function info_automatismWithEpisodeFeedbackAndExpireCycle(cycle, branchId, actionImageId, automatizmId, goalImageId, logMessage) {
    if (!cycle || !branchId || !actionImageId) return false;

    var aid = parseInt(automatizmId, 10) || 0;
    var gid = parseInt(goalImageId, 10) || 0;
    var pc = typeof global.cpuls === 'number' ? global.cpuls : 0;

    global.EpisodicMemory_pendingFeedbackAutomatizmId = 0;
    cycle.waitingFeedbackAutomatizmId = 0;

    global.EpisodicMemory_waitingAutomatismCycleId = cycle.id;
    global.EpisodicMemory_pendingFeedbackAutomatizmId = aid;
    cycle.waitingFeedbackAutomatizmId = aid;
    cycle.goalImageId = gid;

    if (aid && typeof global.InfoDet_updateActiveWaiting === 'function') {
      global.InfoDet_updateActiveWaiting(true);
    }
    if (typeof global.runActionImage === 'function') {
      global.runActionImage(actionImageId);
    }
    if (typeof global.removeDelayedReflexesForBranch === 'function') {
      global.removeDelayedReflexesForBranch(branchId);
    }
    if (!global.automatismRanForBranchIds) global.automatismRanForBranchIds = {};
    global.automatismRanForBranchIds[branchId] = true;

    cycle.awaitingAutomatismEpisodeFeedback = true;
    cycle.automatismOperatorAnswerReceived = false;
    cycle.automatismAnswerLevel2Done = false;
    cycle.lastAutomatismEpisodeEffect = null;
    cycle.operatorAnswerPipelineConsumed = false;
    cycle.status = 'running';
    cycle.step = 1;

    if (typeof global.InfoDet_updateDoubtInDefaultAutomatism === 'function') {
      global.InfoDet_updateDoubtInDefaultAutomatism(false);
    }
    if (global.unsolved_problem) {
      global.unsolved_problem = null;
      if (typeof global.Info_setProblemFromUnsolved === 'function') {
        global.Info_setProblemFromUnsolved(0);
      }
    }

    if (logMessage && typeof global.Consciousness_cycleAppendLog === 'function') {
      global.Consciousness_cycleAppendLog(cycle, logMessage, pc, { force: true });
    }

    cycle.expired = true;
    cycle.isMainCycle = false;

    if (typeof global.Info_updateView === 'function') {
      global.Info_updateView();
    }
    return true;
  }

  /**
   * Лучшее положительное правило в ЭП по ветке восприятия и ситуации; режим задаётся options.
   *
   * Порядок шагов (как в conscience_level_2 выживание / conscience_level_3):
   *  1) при situationId > 0 — кадры perceptionNodeId + situationId;
   *  2) иначе или если не найдено — perceptionNodeId + situation 0 (только ветка);
   *  3) опционально allowStimulusFallback: если stimulusImageId > 0 и кадр ещё null — getFramesByStimulus + лучший позитивный.
   *
   * Режим выборки в шагах 1–2:
   *  — filterByStimulus: true — только кадры с данным stimulusImageId (pickBestPositiveFrameForStimulus), ур.3;
   *  — filterByStimulus: false — любой стимул (pickBestPositiveFrame), ур.2 выживание.
   *
   * exactRule: true только если кадр найден на шаге 1 (ветка+ситуация).
   *
   * @param {number} perceptionNodeId
   * @param {number} situationId
   * @param {Object} [options]
   * @param {boolean} [options.filterByStimulus] — фильтр по стимулу в шагах 1–2 (по умолчанию false)
   * @param {number} [options.stimulusImageId] — при filterByStimulus обязателен; иначе используется для шага 3 при allowStimulusFallback
   * @param {boolean} [options.allowStimulusFallback] — третий шаг по стимулу (ур.2)
   * @returns {{ frame: Object|null, exactRule: boolean }|null} null только при отсутствии API ЭП или при filterByStimulus без stimulus
   */
  function info_bestEpisodicRule(perceptionNodeId, situationId, options) {
    perceptionNodeId = parseInt(perceptionNodeId, 10) || 0;
    situationId = parseInt(situationId, 10) || 0;
    options = options || {};
    var filterByStimulus = !!options.filterByStimulus;
    var stimulusImageId = parseInt(options.stimulusImageId, 10) || 0;
    var allowStimulusFallback = !!options.allowStimulusFallback;

    if (!perceptionNodeId) return null;
    if (filterByStimulus && !stimulusImageId) return null;

    var getFr = global.EpisodicMemory_getFramesByPerceptionAndSituation;
    var pickBest = global.EpisodicMemory_pickBestPositiveFrame;
    var pickForStim = global.EpisodicMemory_pickBestPositiveFrameForStimulus;
    var getByStim = global.EpisodicMemory_getFramesByStimulus;

    if (typeof getFr !== 'function') return null;

    function pickFromFrames(frames) {
      if (filterByStimulus) {
        if (typeof pickForStim !== 'function') return null;
        return pickForStim(frames, stimulusImageId);
      }
      if (typeof pickBest !== 'function') return null;
      return pickBest(frames);
    }

    var exactRule = false;
    var bestFrame = null;

    if (situationId > 0) {
      bestFrame = pickFromFrames(getFr(perceptionNodeId, situationId));
      if (bestFrame) exactRule = true;
    }
    if (!bestFrame) {
      bestFrame = pickFromFrames(getFr(perceptionNodeId, 0));
    }
    if (!bestFrame && allowStimulusFallback && stimulusImageId && typeof getByStim === 'function' && typeof pickBest === 'function') {
      bestFrame = pickBest(getByStim(stimulusImageId));
    }

    return { frame: bestFrame, exactRule: exactRule };
  }

  /**
   * Обёртка: только кадры с заданным стимулом в шагах 1–2 (ур.3).
   * @returns {Object|null}
   */
  function info_bestEpisodicRuleForStimulus(perceptionNodeId, situationId, stimulusImageId) {
    var r = info_bestEpisodicRule(perceptionNodeId, situationId, {
      filterByStimulus: true,
      stimulusImageId: stimulusImageId
    });
    return r && r.frame ? r.frame : null;
  }

  /**
   * Учительское правило в ЭП: кадр с заданным actionImageId (ОД) и ненулевым responseStimulusImageId
   * (ответный стимул оператора на это действие). Значение effect не ранжирует кадры — достаточно закрытого кадра
   * (effect — число); отбор «доверия безусловному правилу».
   *
   * Порядок: 1) последний подходящий кадр при perceptionNodeId и situationId (если situationId > 0);
   * 2) иначе — последний подходящий кадр только по perceptionNodeId (любая ситуация).
   *
   * @param {number} perceptionNodeId
   * @param {number} situationId
   * @param {number} actionImageId
   * @returns {{ frame: Object, exactRule: boolean }|null}
   */
  function info_teacherEpisodicRuleForAction(perceptionNodeId, situationId, actionImageId) {
    perceptionNodeId = parseInt(perceptionNodeId, 10) || 0;
    situationId = parseInt(situationId, 10) || 0;
    actionImageId = parseInt(actionImageId, 10) || 0;
    if (!perceptionNodeId || !actionImageId) return null;
    var ep = global.Episodes;
    if (!Array.isArray(ep) || !ep.length) return null;

    function isTeacherMatch(f) {
      if (!f) return false;
      if (typeof f.effect !== 'number' || isNaN(f.effect)) return false;
      var aid = f.actionImageId != null ? parseInt(f.actionImageId, 10) : 0;
      if (aid !== actionImageId) return false;
      var resp = f.responseStimulusImageId != null ? parseInt(f.responseStimulusImageId, 10) : 0;
      return resp > 0;
    }

    function scanLatest(situationFilter) {
      for (var i = ep.length - 1; i >= 0; i--) {
        var f = ep[i];
        if (!f || (f.perceptionNodeId || 0) !== perceptionNodeId) continue;
        if (situationFilter != null && (f.situationId || 0) !== situationFilter) continue;
        if (!isTeacherMatch(f)) continue;
        return {
          perceptionNodeId: f.perceptionNodeId || 0,
          stimulusImageId: f.stimulusImageId || 0,
          situationId: f.situationId || 0,
          actionImageId: f.actionImageId != null ? f.actionImageId : null,
          effect: f.effect != null ? f.effect : null,
          responseStimulusImageId: f.responseStimulusImageId != null ? f.responseStimulusImageId : null,
          createdAt: f.createdAt || 0
        };
      }
      return null;
    }

    var exactRule = false;
    var frame = null;
    if (situationId > 0) {
      frame = scanLatest(situationId);
      if (frame) exactRule = true;
    }
    if (!frame) {
      frame = scanLatest(null);
    }
    if (!frame) return null;
    return { frame: frame, exactRule: exactRule };
  }

  /**
   * Удержание одного базового контекста на столько же пульсов, сколько в UI (prepare.js: state_retention_period).
   * @param {number} basicContextId — id из window.BasicContexts
   * @returns {boolean}
   */
  function info_activateBasicContextForHolding(basicContextId) {
    var id = parseInt(basicContextId, 10);
    if (!id) return false;
    if (typeof global.ActivateBasicContextsForHolding !== 'function') return false;
    global.ActivateBasicContextsForHolding([id]);
    return true;
  }

  /**
   * Запуск удержания базового состояния Плохо (1), Норма (2), Хорошо (3) — тот же период, что у SetBad/SetWell/SetNorm в prepare.js
   * (аналог круглых кнопок B/W плюс явная Норма).
   *
   * @param {number} badNormWellId — 1 | 2 | 3
   * @returns {boolean}
   */
  function info_setBadNormWellForHolding(badNormWellId) {
    var id = parseInt(badNormWellId, 10);
    if (id === 1 && typeof global.SetBadForHolding === 'function') {
      global.SetBadForHolding();
      return true;
    }
    if (id === 2 && typeof global.SetNormForHolding === 'function') {
      global.SetNormForHolding();
      return true;
    }
    if (id === 3 && typeof global.SetWellForHolding === 'function') {
      global.SetWellForHolding();
      return true;
    }
    return false;
  }

  /**
   * Активировать зеркальное действие оператора: стимул по Id из window.BasicMirrorActions
   * (см. activateStimulusForBasicMirrorActionId в stimuls_ui.js).
   *
   * @param {number} basicMirrorActionId
   * @returns {boolean}
   */
  function info_activateBasicMirrorAction(basicMirrorActionId) {
    if (typeof global.activateStimulusForBasicMirrorActionId !== 'function') {
      return false;
    }
    return !!global.activateStimulusForBasicMirrorActionId(basicMirrorActionId);
  }

  /**
   * Общая реакция на инсайт (гештальт и др.): умолчательная цель «что для меня означает это?», `thinkingMode = 'target'` у цикла;
   * если передан цикл и он фоновый — перевод в главный (Consciousness_setMainCycle).
   *
   * @param {Object|null} cycle — цикл, в котором зафиксирован инсайт (null — только инфокартина)
   * @param {Object} [opts] — { skipPromote: true } — не менять главный цикл
   * @returns {boolean}
   */
  function info_insight(cycle, opts) {
    opts = opts || {};
    if (typeof global.Goals_ensureDefaultMeaningGoal === 'function') {
      global.Goals_ensureDefaultMeaningGoal();
    }
    var gid = typeof global.Goals_getDefaultMeaningGoalId === 'function'
      ? global.Goals_getDefaultMeaningGoalId() : 0;
    if (!gid) return false;

    if (typeof global.Info_initIfNeeded === 'function') {
      global.Info_initIfNeeded();
    }

    if (cycle && cycle.status === 'running' && !cycle.expired) {
      cycle.thinkingMode = 'target';
    } else if (typeof global.Consciousness_getMainCycle === 'function') {
      var mc = global.Consciousness_getMainCycle();
      if (mc) mc.thinkingMode = 'target';
    }

    if (typeof global.Goals_setCurrentGoalInInfoPicture === 'function') {
      global.Goals_setCurrentGoalInInfoPicture('meaning', gid, 0);
    }

    if (cycle && cycle.status === 'running' && !cycle.expired) {
      var wasBackground = cycle.isMainCycle === false;
      cycle.goalImageId = gid;
      if (wasBackground && !opts.skipPromote && typeof global.Consciousness_setMainCycle === 'function') {
        global.Consciousness_setMainCycle(cycle.id);
        if (typeof global.Consciousness_cycleAppendLog === 'function') {
          var pc = typeof global.cpuls === 'number' ? global.cpuls : 0;
          var label = global.GOALS_DEFAULT_MEANING_LABEL || 'значение';
          global.Consciousness_cycleAppendLog(cycle,
            'Инсайт: цикл переведён в главный, цель «' + String(label) + '».',
            pc,
            { force: true });
        }
      }
      if (global.InfoPicture) {
        if (typeof cycle.weight === 'number') {
          global.InfoPicture.thinkingImportance = cycle.weight;
        }
        global.InfoPicture.thinkingProcessId = cycle.id;
      }
    }

    if (typeof global.Info_updateView === 'function') {
      global.Info_updateView();
    }
    return true;
  }

  /**
   * Обработка накопленного сюжета пассивного фантазирования: перенос звеньев в semanticStructure клонов
   * (AbstractImages_RULE_KIND_FANTASY), без перезаписи учительских слотов. После успеха цепочки на цикле обнуляются.
   * Предназначено для вызова из «сна» и редких консолидаций; не тянет цепочки из сырого Episodes (см. task-файлы).
   *
   * @param {Object|null} cycle — цикл ФО с fantasizmArr / fantazmingPlot; можно null при plotSnapshot в opts
   * @param {Object} [opts]
   * @param {number} [opts.minChainLength] — не обрабатывать, если chain.length < (по умолчанию 0)
   * @param {boolean} [opts.clearInfoPlot=true] — для главного цикла очистить InfoPicture.fantasmPlotRules
   * @param {{ fantasizmArr?: Array }} [opts.plotSnapshot] — снимок без cycle (например отложенный сон)
   * @param {string} [opts.source] — метка источника ('sleep' и т.д.), только для лога
   * @returns {{ ok: boolean, links: number, upserts: number, skippedTeacher: number, cleared: boolean, reason?: string }}
   */
  function info_new_abstract_semantic(cycle, opts) {
    opts = opts || {};
    var pc = typeof global.cpuls === 'number' ? global.cpuls : 0;
    var upsertF = global.AbstractImages_upsertSemanticRuleFromFantasyFrame;
    var ensureP = global.AbstractImages_ensurePerceptionAbstraction;
    var ensureA = global.AbstractImages_ensureActionAbstraction;
    var out = {
      ok: false,
      links: 0,
      upserts: 0,
      skippedTeacher: 0,
      cleared: false,
      reason: ''
    };

    if (typeof upsertF !== 'function' || typeof ensureP !== 'function' || typeof ensureA !== 'function') {
      out.reason = 'abstract_images_api';
      return out;
    }

    var minChain = typeof opts.minChainLength === 'number' ? opts.minChainLength : 0;
    if (cycle && minChain > 0) {
      var ch = cycle.fantazmingPlot && cycle.fantazmingPlot.chain ? cycle.fantazmingPlot.chain.length : 0;
      if (ch < minChain) {
        out.reason = 'chain_too_short';
        return out;
      }
    }

    var arr = [];
    if (cycle && Array.isArray(cycle.fantasizmArr) && cycle.fantasizmArr.length) {
      arr = cycle.fantasizmArr.slice();
    } else if (opts.plotSnapshot && Array.isArray(opts.plotSnapshot.fantasizmArr) && opts.plotSnapshot.fantasizmArr.length) {
      arr = opts.plotSnapshot.fantasizmArr.slice();
    }

    if (!arr.length) {
      out.reason = 'empty_plot';
      return out;
    }

    out.links = arr.length;
    var srcTag = opts.source ? '[' + String(opts.source) + '] ' : '';

    for (var i = 0; i < arr.length; i++) {
      var link = arr[i];
      if (!link) continue;
      var p = parseInt(link.perceptionNodeId, 10) || 0;
      var stim = link.stimulusImageId != null ? parseInt(link.stimulusImageId, 10) || 0 : 0;
      var act = link.actionImageId != null ? parseInt(link.actionImageId, 10) || 0 : 0;
      if (!p || !stim || !act) continue;

      var epLike = {
        perceptionNodeId: p,
        situationId: link.situationId != null ? parseInt(link.situationId, 10) || 0 : 0,
        stimulusImageId: stim,
        actionImageId: act,
        effect: typeof link.effect === 'number' && !isNaN(link.effect) ? link.effect : 0,
        responseStimulusImageId: link.responseStimulusImageId != null ? link.responseStimulusImageId : null
      };

      var apObj = ensureP(p, stim);
      var aaObj = ensureA(p, act);
      if (apObj) {
        if (upsertF(apObj, epLike)) out.upserts += 1;
        else out.skippedTeacher += 1;
      }
      if (aaObj) {
        if (upsertF(aaObj, epLike)) out.upserts += 1;
        else out.skippedTeacher += 1;
      }
    }

    out.ok = arr.length > 0;

    if (arr.length && cycle) {
      cycle.fantasizmArr = [];
      cycle.fantazmingPlot = { chain: [], lastGeneratedActionId: 0 };
      cycle.currentFantazmingStimulus = 0;
      out.cleared = true;
      var clearInfo = opts.clearInfoPlot !== false;
      if (clearInfo && cycle.isMainCycle && global.InfoPicture) {
        global.InfoPicture.fantasmPlotRules = [];
      }
    }

    if (typeof global.Consciousness_cycleAppendLog === 'function' && cycle) {
      var msg = srcTag + 'info_new_abstract_semantic: звеньев=' + out.links +
        ', upsert слотов semanticStructure=' + out.upserts + ', пропуск (teacher)=' + out.skippedTeacher +
        (out.cleared ? ', сюжет цикла обнулён' : '');
      global.Consciousness_cycleAppendLog(cycle, msg, pc, { force: !cycle.isMainCycle });
    }

    if (out.upserts > 0 && typeof global.AbstractImages_flushIndexedDB === 'function') {
      global.AbstractImages_flushIndexedDB();
    }
    if (out.ok && typeof global.Info_updateView === 'function') {
      global.Info_updateView();
    }

    return out;
  }

  global.info_automatismWithEpisodeFeedbackAndExpireCycle = info_automatismWithEpisodeFeedbackAndExpireCycle;
  global.info_bestEpisodicRule = info_bestEpisodicRule;
  global.info_bestEpisodicRuleForStimulus = info_bestEpisodicRuleForStimulus;
  global.info_teacherEpisodicRuleForAction = info_teacherEpisodicRuleForAction;
  global.info_activateBasicContextForHolding = info_activateBasicContextForHolding;
  global.info_setBadNormWellForHolding = info_setBadNormWellForHolding;
  global.info_activateBasicMirrorAction = info_activateBasicMirrorAction;
  global.info_insight = info_insight;
  global.info_new_abstract_semantic = info_new_abstract_semantic;
})(window);
