const fs = require('fs');

const mapping = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
const rows = JSON.parse(fs.readFileSync(process.argv[3], 'utf8'));
const templateRules = JSON.parse(fs.readFileSync(process.argv[4], 'utf8'));

const isTelemetryTemplate = (rule) => {
  const filters = (rule.filters || []).map((f) => f.name);
  const fields = (rule.fields || []).map((f) => f.name);
  return (
    filters.includes('.VideoInput') &&
    !filters.includes('.Position') &&
    !filters.includes('.VideoOutput') &&
    fields.includes('VideoInput') &&
    !fields.includes('Position')
  );
};

const deriveActionName = (inputCaption) => {
  if (!inputCaption) return '';
  let name = inputCaption.trim();
  name = name.replace(/^gevi[\s_]+/i, '');
  name = name.replace(/[_\s]\d+$/i, '');
  return name.trim();
};

const normalize = (value) => (value || '').trim().toLowerCase();
const normalizeInputName = (value) => (value || '').trim().toLowerCase().replace(/\s+/g, ' ');

const findTemplate = (kind) => {
  if (kind === 'telemetry') {
    const preferred = mapping.rules.find((rule) => {
      if (!isTelemetryTemplate(rule)) return false;
      const name = String(rule.input?.name || '');
      return /^GeVi\s+(Pan|Tilt|Zoom|Focus)/i.test(name);
    });
    if (preferred) return preferred;
    return (
      mapping.rules.find((rule) => isTelemetryTemplate(rule)) ||
      (templateRules.telemetry || []).find((rule) => isTelemetryTemplate(rule)) ||
      null
    );
  }
  return (
    mapping.rules.find((rule) => {
      const filters = (rule.filters || []).map((f) => f.name);
      const fields = (rule.fields || []).map((f) => f.name);
      return (
        filters.includes('.SwitchMode') &&
        filters.includes('.VideoInput') &&
        filters.includes('.VideoOutput') &&
        fields.includes('SwitchMode') &&
        fields.includes('VideoOutput')
      );
    }) ||
    (templateRules.crossswitch || []).find((rule) => {
      const filters = (rule.filters || []).map((f) => f.name);
      const fields = (rule.fields || []).map((f) => f.name);
      return (
        filters.includes('.SwitchMode') &&
        filters.includes('.VideoInput') &&
        filters.includes('.VideoOutput') &&
        fields.includes('SwitchMode') &&
        fields.includes('VideoOutput')
      );
    }) ||
    null
  );
};

const defaultPosFallback =
  (templateRules.defaultpos || []).find((rule) => {
    const filters = (rule.filters || []).map((f) => f.name);
    const fields = (rule.fields || []).map((f) => f.name);
    return filters.includes('.Position') || fields.includes('Position');
  }) || null;

const fallbackTemplate = mapping.rules[0];
const telemetryTemplate = findTemplate('telemetry') || fallbackTemplate;
const crossSwitchTemplate = findTemplate('crossswitch') || fallbackTemplate;
const defaultPosTemplate =
  mapping.rules.find((rule) => {
    const filters = (rule.filters || []).map((f) => f.name);
    const fields = (rule.fields || []).map((f) => f.name);
    return filters.includes('.Position') || fields.includes('Position');
  }) ||
  defaultPosFallback ||
  fallbackTemplate;

const findTelemetryActionTemplate = (actionName) => {
  if (!actionName) return null;
  const needle = actionName.toLowerCase();
  return (
    mapping.rules.find((rule) => {
      const name = String(rule.input?.name || '').toLowerCase();
      if (!name.startsWith('gevi ')) return false;
      if (!name.includes(needle)) return false;
      if (name.includes('prepos')) return false;
      return isTelemetryTemplate(rule);
    }) ||
    (templateRules.telemetry || []).find((rule) => {
      const name = String(rule.input?.name || '').toLowerCase();
      if (!name.startsWith('gevi ')) return false;
      if (!name.includes(needle)) return false;
      if (name.includes('prepos')) return false;
      return isTelemetryTemplate(rule);
    }) ||
    null
  );
};

const findDefaultPosTemplate = (actionName) => {
  if (!actionName) return null;
  const needle = actionName.toLowerCase();
  return (
    mapping.rules.find((rule) => {
      const name = String(rule.input?.name || '').toLowerCase();
      const filters = (rule.filters || []).map((f) => f.name);
      const fields = (rule.fields || []).map((f) => f.name);
      if (!name.startsWith('gevi ')) return false;
      if (!name.includes(needle)) return false;
      return filters.includes('.Position') || fields.includes('Position');
    }) ||
    (templateRules.defaultpos || []).find((rule) => {
      const name = String(rule.input?.name || '').toLowerCase();
      const filters = (rule.filters || []).map((f) => f.name);
      const fields = (rule.fields || []).map((f) => f.name);
      if (!name.startsWith('gevi ')) return false;
      if (!name.includes(needle)) return false;
      return filters.includes('.Position') || fields.includes('Position');
    }) ||
    null
  );
};

const outputKey = (inputName, o) => [normalize(inputName), normalize(o.actionField), normalize(o.action), normalize(o.server)].join('|');

const ruleIndexByName = new Map();
const ruleIndexByCamera = new Map();
const ruleIndexByAction = new Map();
const existingKeys = new Set();
const flagTemplates = new Map();
const fieldFlagCounts = new Map();

mapping.rules.forEach((rule, idx) => {
  const normalized = normalizeInputName(rule.input.name);
  ruleIndexByName.set(normalized, idx);
  const match = normalized.match(/(\d+)$/);
  if (match) {
    const cam = match[1];
    if (!ruleIndexByCamera.has(cam)) ruleIndexByCamera.set(cam, []);
    ruleIndexByCamera.get(cam).push(idx);
  }
  for (const out of rule.outputs || []) {
    existingKeys.add(outputKey(rule.input.name, out));
    const key = [normalize(out.actionField), normalize(out.action)].join('|');
    if (!flagTemplates.has(key)) {
      flagTemplates.set(key, { ...out.flags });
    }
    const fieldKey = normalize(out.actionField);
    const flagPair = `${out.flags.primary}|${out.flags.secondary}`;
    if (!fieldFlagCounts.has(fieldKey)) fieldFlagCounts.set(fieldKey, new Map());
    const counts = fieldFlagCounts.get(fieldKey);
    counts.set(flagPair, (counts.get(flagPair) || 0) + 1);
  }
});

const fallbackFlags = (actionField) => {
  const counts = fieldFlagCounts.get(normalize(actionField));
  if (!counts) return { primary: 0, secondary: 0 };
  let best = '';
  let bestCount = -1;
  counts.forEach((count, key) => {
    if (count > bestCount) {
      bestCount = count;
      best = key;
    }
  });
  const [primary, secondary] = best.split('|').map((val) => Number(val));
  return { primary: primary || 0, secondary: secondary || 0 };
};

let maxRuleId = Math.max(0, ...mapping.rules.map((r) => Number(r.id || 0)));

const isCrossSwitchAction = (actionName, category) => /crossswitch/i.test(actionName) || /crossbar/i.test(category);
const DEFAULTPOS_ACTIONS = {
  MoveToDefaultPosition: 'DefaultPosCallUp',
  ClearDefaultPosition: 'DefaultPosClear',
  SaveDefaultPosition: 'DefaultPosSave',
};
const normalizeOutputAction = (actionName) => DEFAULTPOS_ACTIONS[actionName] || actionName;
const isDefaultPosAction = (actionName) => Object.prototype.hasOwnProperty.call(DEFAULTPOS_ACTIONS, actionName);
const isFocusSpeedAction = (actionName) => /focusnear|focusfar/i.test(actionName);
const isFocusInputAction = (actionName) => /focusnear|focusfar|focusstop|focus/i.test(actionName);
const parseSpeed = (value) => {
  if (!value) return undefined;
  const parsed = Number(value);
  return Number.isFinite(parsed) ? parsed : undefined;
};
const buildAction = (actionName, cameraId, speed) => {
  if (!actionName) return '';
  const params = [];
  if (cameraId) params.push('Comment: ""');
  if (cameraId) params.push(`Camera: ${cameraId}`);
  if (speed !== undefined) params.push(`Speed: ${speed}`);
  if (params.length) return `@ ${actionName} (${params.join(', ')})`;
  return `@ ${actionName} ()`;
};

for (const row of rows) {
  const output1 = row.outputs.find((o) => o.kind === 'output1');
  const inputName = row.caption || `Camera ${row.cameraId}`;
  const inputCategoryRaw = String(output1?.category ?? '');
  const inputActionRaw = String(output1?.action ?? '').trim();
  const inputActionName = inputActionRaw || deriveActionName(inputName);
  const actionName = inputActionName;
  const inputCategory = inputCategoryRaw.toLowerCase();
  const cameraId = String(row.cameraId ?? '').trim();
  const outputs = [];
  const seenOutputs = new Set();
  const serverType = String(row.serverType ?? '').toLowerCase();
  const isGscTarget = serverType.includes('geviscope') || serverType.includes('gsc');
  const isGcoreTarget = serverType.includes('g-core') || serverType.includes('gcore');
  const prefer = isGscTarget ? 'gsc' : isGcoreTarget ? 'gcore' : 'both';

  const gscOut = prefer !== 'gcore' ? row.outputs.find((o) => o.kind === 'gsc') : null;
  if (gscOut) {
    const gscActionRaw = String(gscOut.action ?? actionName).trim();
    const gscActionName = normalizeOutputAction(gscActionRaw);
    const gscCamera = String(gscOut.ptzHead ?? '').trim();
    const gscSpeed = parseSpeed(gscOut.speed);
    const gscSpeedValue = isFocusSpeedAction(gscActionName) ? gscSpeed : undefined;
    const gscCategory = String(gscOut.category ?? '').toLowerCase();
    const crossSwitch = isCrossSwitchAction(gscActionName, inputCategory) || /viewer/.test(gscCategory);
    const gscCaption = gscOut.caption || `GSC ${gscActionName}${cameraId ? `_${cameraId}` : ''}`;
    const gscServer = gscOut.server || (String(row.serverType ?? '').toLowerCase().includes('geviscope') ? row.server : '');
    if (actionName || gscOut.caption || gscOut.category || gscOut.server || String(row.serverType ?? '').toLowerCase().includes('geviscope')) {
      const gscAction = crossSwitch ? '@ ViewerConnectLive ()' : buildAction(gscActionName, gscCamera, gscSpeedValue);
      const gscFlags = flagTemplates.get([normalize('GscAction'), normalize(gscAction)].join('|')) || fallbackFlags('GscAction');
      const out = {
        id: '1',
        name: gscCaption,
        flags: gscFlags,
        actionField: 'GscAction',
        serverField: 'GscServer',
        action: gscAction,
        server: gscServer || '',
      };
      const key = outputKey(inputName, out);
      if (!seenOutputs.has(key) && !existingKeys.has(key)) {
        outputs.push(out);
        seenOutputs.add(key);
        existingKeys.add(key);
      }
    }
  }

  const gcoreOut = prefer !== 'gsc' ? row.outputs.find((o) => o.kind === 'gcore') : null;
  if (gcoreOut) {
    const gcoreActionRaw = String(gcoreOut.action ?? actionName).trim();
    const gcoreActionName = normalizeOutputAction(gcoreActionRaw);
    const gcoreCamera = String(gcoreOut.ptzHead ?? '').trim();
    const gcoreSpeed = parseSpeed(gcoreOut.speed);
    const gcoreSpeedValue = isFocusSpeedAction(gcoreActionName) ? gcoreSpeed : undefined;
    const gcoreCategory = String(gcoreOut.category ?? '').toLowerCase();
    const crossSwitch = isCrossSwitchAction(gcoreActionName, inputCategory) || /viewer/.test(gcoreCategory);
    const gcoreCaption = gcoreOut.caption || `GNG ${gcoreActionName}${cameraId ? `_${cameraId}` : ''}`;
    const gcoreServer = gcoreOut.server || (String(row.serverType ?? '').toLowerCase().includes('g-core') ? row.server : '');
    if (actionName || gcoreOut.caption || gcoreOut.category || gcoreOut.server || String(row.serverType ?? '').toLowerCase().includes('g-core')) {
      const gcoreAction = crossSwitch ? '@ ViewerConnectLive (Comment: "")' : buildAction(gcoreActionName, gcoreCamera, gcoreSpeedValue);
      const gcoreFlags = flagTemplates.get([normalize('GCoreAction'), normalize(gcoreAction)].join('|')) || fallbackFlags('GCoreAction');
      const out = {
        id: '1',
        name: gcoreCaption,
        flags: gcoreFlags,
        actionField: 'GCoreAction',
        serverField: 'GCoreServer',
        action: gcoreAction,
        server: gcoreServer || '',
      };
      const key = outputKey(inputName, out);
      if (!seenOutputs.has(key) && !existingKeys.has(key)) {
        outputs.push(out);
        seenOutputs.add(key);
        existingKeys.add(key);
      }
    }
  }

  const kind = isCrossSwitchAction(actionName, inputCategory)
    ? 'crossswitch'
    : isDefaultPosAction(actionName)
    ? 'defaultpos'
    : 'telemetry';

  let chosenTemplate = null;
  if (kind === 'crossswitch') {
    chosenTemplate = crossSwitchTemplate;
  } else if (kind === 'defaultpos') {
    chosenTemplate = findDefaultPosTemplate(normalizeOutputAction(actionName) || actionName) || defaultPosTemplate;
  } else {
    chosenTemplate = findTelemetryActionTemplate(actionName) || telemetryTemplate;
  }

  const nextFilters = chosenTemplate?.filters
    ? chosenTemplate.filters.map((f) => ({ ...f }))
    : [];
  const baseFilters = nextFilters;
  const isTelemetry = kind === 'telemetry';
  const nextFilterSet = isTelemetry
    ? baseFilters.map((f) => (f.name === '.VideoInput' ? { ...f, value: true } : f))
    : baseFilters;

  const baseFields = chosenTemplate?.fields ? [...chosenTemplate.fields] : [];
  const nextFields = isTelemetry
    ? [...baseFields.filter((f) => f.name !== 'Position' && f.name !== 'VideoOutput'), ...(baseFields.some((f) => f.name === 'VideoInput') ? [] : [{ name: 'VideoInput', type: 'int32', value: 0 }])]
    : baseFields;

  const cleanedNextFields = isFocusInputAction(actionName) ? nextFields.filter((f) => f.name !== 'Temp') : nextFields;

  const actionKey = actionName && cameraId ? `action:${normalize(actionName)}|cam:${cameraId}` : null;
  let existingIdx = (actionKey ? ruleIndexByAction.get(actionKey) : undefined) ?? ruleIndexByName.get(normalizeInputName(inputName));

  if (existingIdx === undefined && actionName && cameraId) {
    const candidates = ruleIndexByCamera.get(cameraId) || [];
    const actionNeedle = normalize(actionName);
    for (const idx of candidates) {
      const ruleName = normalizeInputName(mapping.rules[idx].input.name);
      if (ruleName.includes(actionNeedle) && ruleName.endsWith(cameraId)) {
        existingIdx = idx;
        break;
      }
    }
  }

  if (existingIdx !== undefined) {
    const existingRule = mapping.rules[existingIdx];
    const existingOutputs = existingRule.outputs || [];
    let nextId = Math.max(0, ...existingOutputs.map((o) => Number(o.id || 0))) + 1;
    const mergedOutputs = [...existingOutputs];
    for (const out of outputs) {
      const key = outputKey(existingRule.input.name, out);
      if (!existingKeys.has(key)) {
        mergedOutputs.push({ ...out, id: String(nextId) });
        nextId += 1;
        existingKeys.add(key);
      }
    }
    const existingFilters = isTelemetry
      ? (existingRule.filters || []).filter((f) => f.name !== '.Position' && f.name !== '.VideoOutput').map((f) => (f.name === '.VideoInput' ? { ...f, value: true } : f))
      : (existingRule.filters || []).map((f) => (f.name === '.VideoInput' ? { ...f, value: true } : f));
    if (isTelemetry && !existingFilters.some((f) => f.name === '.VideoInput')) {
      existingFilters.push({ name: '.VideoInput', type: 'bool', value: true });
    }
    const existingFields = isTelemetry
      ? (existingRule.fields || []).filter((f) => f.name !== 'Position' && f.name !== 'VideoOutput')
      : (existingRule.fields || []);
    if (isFocusInputAction(actionName)) {
      for (let i = existingFields.length - 1; i >= 0; i -= 1) {
        if (existingFields[i]?.name === 'Temp') existingFields.splice(i, 1);
      }
    }
    if (isTelemetry && !existingFields.some((f) => f.name === 'VideoInput')) {
      existingFields.push({ name: 'VideoInput', type: 'int32', value: 0 });
    }
    mapping.rules[existingIdx] = {
      ...existingRule,
      input: {
        ...existingRule.input,
        videoInputId: existingRule.input.videoInputId ?? (cameraId ? Number(cameraId) : undefined),
        ptz: typeof row.ptz === 'string' ? row.ptz.toUpperCase() === 'O' : existingRule.input.ptz,
        temp: isFocusInputAction(actionName) ? undefined : existingRule.input.temp,
      },
      outputs: mergedOutputs,
      filters: existingFilters,
      fields: existingFields,
    };
  } else {
    maxRuleId += 1;
    const ruleId = String(maxRuleId);
    mapping.rules.push({
      id: ruleId,
      input: {
        name: inputName,
        flags: chosenTemplate?.input?.flags || { primary: 0, secondary: 0 },
        videoInputId: cameraId ? Number(cameraId) : undefined,
        ptz: typeof row.ptz === 'string' ? row.ptz.toUpperCase() === 'O' : undefined,
        temp: isFocusInputAction(actionName) ? undefined : chosenTemplate?.input?.temp,
      },
      outputs,
      _order: chosenTemplate?._order,
      filters: nextFilterSet,
      fields: cleanedNextFields,
    });
    ruleIndexByName.set(normalizeInputName(inputName), mapping.rules.length - 1);
    if (actionKey) ruleIndexByAction.set(actionKey, mapping.rules.length - 1);
  }
}

fs.writeFileSync(process.argv[5], JSON.stringify(mapping));
