'use strict';

/*
 * Which synchronisation protocol a group runs.
 *
 * ScreenTinker has its own group sync: every member derives its position from a shared clock,
 * so it needs no leader, survives a server outage, and works across Android, web, Tizen and
 * BrightSign alike. BrightSign has its own — BrightWall — which is native, frame-accurate, and
 * only exists between BrightSign players.
 *
 * The choice is therefore not "which is better" but "what is in this group":
 *
 *   screentinker  works everywhere, mixed fleets included; sync is to the second, not the frame
 *   brightsign    frame-accurate video walls; requires EVERY member to be a BrightSign
 *
 * `auto` picks the strongest protocol the group can actually run, which is what an operator
 * means when they say "just make the wall work". Explicit settings are honoured, except the one
 * that cannot physically work (native sync with a non-BrightSign member) — that downgrades and
 * says why, rather than silently doing nothing on the screens that can't participate.
 *
 * Kept pure so the decision is testable without a fleet: callers pass plain device rows.
 */

const BACKENDS = ['auto', 'screentinker', 'brightsign'];

/*
 * A device is a BrightSign if it said so: the player sends ?platform=brightsign (autorun.brs puts
 * it there), which lands in devices.platform.
 *
 * There is deliberately NO user-agent fallback. An earlier version had one, to catch panels paired
 * before this port existed — which registered as "Chrome 120" with a BrightSign user agent. It
 * could never fire: `devices` has no user_agent column, so the field is always undefined on a row
 * read from the database. It read as defensive and was dead code.
 *
 * Those pre-port panels are identified the moment they re-register on a build that carries the
 * host, which every one of them gets on its next update. Recognising them earlier would mean
 * persisting the user agent, and a column added solely to identify a population that disappears on
 * its own is not worth carrying.
 */
function isBrightSignDevice(device) {
  if (!device) return false;
  return String(device.platform || '').toLowerCase().includes('brightsign');
}

/*
 * SyncManager is MULTICAST (224.0.126.10:1539), so every member must share one L2 network. A group
 * spanning sites or VLANs cannot use it — and the failure is silent: each subnet would sync neatly
 * within itself while drifting from the others.
 *
 * The evidence available server-side is the address each device connects from. Comparing the /24
 * is a heuristic, not proof — two VLANs can share a public IP, and one subnet can span a routed
 * boundary that blocks multicast. So it is used in ONE direction only: differing networks are
 * treated as evidence against native sync, while matching ones are never treated as proof for it.
 * Unknown addresses block nothing, because "we cannot see it" must not read as "it is broken".
 */
function networkOf(device) {
  const ip = String((device && (device.ip_address || device.last_ip)) || '').trim();
  if (!ip) return null;
  if (ip.includes(':')) {                      // IPv6: compare the /64
    const parts = ip.split(':');
    return parts.slice(0, 4).join(':').toLowerCase();
  }
  const octets = ip.split('.');
  if (octets.length !== 4) return null;
  return octets.slice(0, 3).join('.');
}

function networksDiffer(members) {
  const nets = members.map(networkOf).filter(Boolean);
  if (nets.length < 2) return false;           // nothing to contradict
  return new Set(nets).size > 1;
}

/**
 * @param {string} setting  'auto' | 'screentinker' | 'brightsign' (unknown values read as auto)
 * @param {Array}  members  device rows in the group
 * @returns {{backend: 'screentinker'|'brightsign', reason: string, downgraded: boolean}}
 */
function resolveSyncBackend(setting, members) {
  const list = Array.isArray(members) ? members.filter(Boolean) : [];
  const requested = BACKENDS.includes(setting) ? setting : 'auto';

  const brightsignCount = list.filter(isBrightSignDevice).length;
  const allBrightSign = list.length > 0 && brightsignCount === list.length;
  const split = networksDiffer(list);

  if (requested === 'screentinker') {
    return { backend: 'screentinker', reason: 'explicitly selected', downgraded: false };
  }

  if (requested === 'brightsign') {
    if (allBrightSign && !split) {
      return { backend: 'brightsign', reason: 'explicitly selected', downgraded: false };
    }
    if (allBrightSign && split) {
      // Every member is a BrightSign, but they are not on one network. Native sync would appear
      // to work inside each subnet while the subnets drifted apart — worse than not using it.
      return {
        backend: 'screentinker',
        reason: 'displays are on different networks — native sync is multicast and cannot cross them',
        downgraded: true
      };
    }
    // Refusing to pretend: BrightWall cannot include a non-BrightSign screen, and a group that
    // half-syncs is worse than one that syncs to the second everywhere.
    const others = list.length - brightsignCount;
    return {
      backend: 'screentinker',
      reason: list.length === 0
        ? 'group is empty — native sync needs BrightSign members'
        : `group has ${others} non-BrightSign display${others === 1 ? '' : 's'}`,
      downgraded: true
    };
  }

  // auto
  if (allBrightSign && !split) {
    return { backend: 'brightsign', reason: 'every display is a BrightSign', downgraded: false };
  }
  if (allBrightSign && split) {
    return {
      backend: 'screentinker',
      reason: 'displays are on different networks',
      downgraded: false
    };
  }
  return {
    backend: 'screentinker',
    reason: list.length === 0 ? 'no displays in the group' : 'mixed fleet',
    downgraded: false
  };
}

module.exports = { resolveSyncBackend, isBrightSignDevice, networksDiffer, BACKENDS };
