<?php
/**
 * ops_airos.php — Pont LECTURE Ops → signal LIVE d'un CPE sans-fil (airOS/Cambium) via status.cgi.
 * Déployé SUR facturation.targo.ca (seul hôte qui joint le réseau des CPE). Gaté X-Ops-Token (= ops_reassign.php).
 * Read-only : ne fait qu'interroger status.cgi du CPE. Port du device_ajax/airos_ac_ajax.php de F.
 *
 *   GET/POST  ?serial=<sn> | ?mac=<mac> | ?ip=<manage> | ?device_id=<id>  [&port=]
 *   header X-Ops-Token: <OPS_TOKEN>
 *   → { ok, source:"f", device_model, device_name, version, ssid, frequency, tx_power, mac_ap,
 *       signal, signal_ap, ccq, tx_rate, rx_rate, airmax_capacity_uplink, airmax_capacity_downlink,
 *       connection_time, if_speed_lan0, if_speed_lan1 }
 *
 * Anti-SSRF : on ne curl QUE l'IP `manage` d'une ligne `device` réellement provisionnée (jamais une IP brute
 * fournie par l'appelant sans correspondance en base).
 */
header('Content-Type: application/json; charset=utf-8');
require __DIR__ . '/ops_secret.php';                  // $OPS_TOKEN, $DB_USER, $DB_PASS
@include_once __DIR__ . '/facturation/lib/mcrypt.php'; // mcrypt('decrypt', ...) — déchiffre device.pass (repli défaut usine si absent)

$DB_HOST = '10.100.80.100';
$DB_NAME = 'gestionclient';
// RADIUS (WPA2) = source AUTORITAIRE de la MAC courante du CPE (auto-MàJ au remplacement, via callingstationid) +
// IP de l'AP (nasipaddress). Mêmes creds que F airos_ac.php (le hub OPS n'a PAS le GRANT → passe par ce pont).
$RADIUS_HOST = '10.5.2.25';
$RADIUS_DB = 'radiusdb';
$RADIUS_PASS = 'N0HAk4u$';

function out($a, $c = 200) { http_response_code($c); echo json_encode($a, JSON_UNESCAPED_UNICODE); exit; }

// ─── auth (mêmes secret + mécanisme que ops_reassign.php / ops_placemarks.php) ───
$tok = $_SERVER['HTTP_X_OPS_TOKEN'] ?? ($_REQUEST['token'] ?? '');
if (!is_string($tok) || !hash_equals($OPS_TOKEN, $tok)) out(['ok' => false, 'error' => 'forbidden'], 403);

// ─── airOS status fetch (copié à l'identique de F device_ajax/airos_ac_ajax.php, + timeouts) ───
function airOS_getFile_HTTPS($username, $password, $file, $address) {
  $ch = curl_init();
  curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
  curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
  curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
  curl_setopt($ch, CURLOPT_COOKIEJAR, null);
  curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 6);
  curl_setopt($ch, CURLOPT_TIMEOUT, 15);
  curl_setopt($ch, CURLOPT_HTTPHEADER, array('Expect: '));
  // airOS >= 8.5 : POST /api/auth (récupère X-CSRF-ID) ; sinon repli /login.cgi
  curl_setopt($ch, CURLOPT_URL, "https://$address/api/auth");
  curl_setopt($ch, CURLOPT_POST, 1);
  curl_setopt($ch, CURLOPT_POSTFIELDS, array('username' => $username, 'password' => $password));
  curl_setopt($ch, CURLOPT_HEADER, 1);
  $response = curl_exec($ch);
  curl_setopt($ch, CURLOPT_HEADER, 0);
  if (curl_getinfo($ch, CURLINFO_HTTP_CODE) == 200) {
    preg_match('/X-CSRF-ID: .*/', substr($response, 0, curl_getinfo($ch, CURLINFO_HEADER_SIZE)), $XCSRFID);
    curl_setopt($ch, CURLOPT_URL, "https://$address/$file");
    curl_setopt($ch, CURLOPT_POST, 0);
    $retfile = curl_exec($ch);
    curl_setopt($ch, CURLOPT_URL, "https://$address/logout.cgi");
    curl_setopt($ch, CURLOPT_HTTPHEADER, array(trim($XCSRFID[0] ?? ''), 'X-AIROS-LUA: 1'));
    curl_setopt($ch, CURLOPT_POST, 1);
    curl_setopt($ch, CURLOPT_POSTFIELDS, array());
    curl_exec($ch);
  } else {
    curl_setopt($ch, CURLOPT_URL, "https://$address/login.cgi");
    curl_setopt($ch, CURLOPT_POST, 1);
    curl_setopt($ch, CURLOPT_POSTFIELDS, array('username' => $username, 'password' => $password));
    curl_exec($ch);
    curl_setopt($ch, CURLOPT_URL, "https://$address/$file");
    curl_setopt($ch, CURLOPT_POST, 0);
    $retfile = curl_exec($ch);
  }
  curl_close($ch);
  return $retfile;
}

function sec2t($n) {
  if (!is_numeric($n) || $n <= 0) return '';
  $n = (int)$n;
  return sprintf('%dd %02d:%02d:%02d', intdiv($n, 86400), intdiv($n % 86400, 3600), intdiv($n % 3600, 60), $n % 60);
}

// ─── résout la ligne device (creds + IP de gestion) : par sn, puis mac, puis manage=ip, puis id ───
$serial    = trim($_REQUEST['serial'] ?? $_REQUEST['sn'] ?? '');
$mac       = trim($_REQUEST['mac'] ?? '');
$ip        = trim($_REQUEST['ip'] ?? '');
$device_id = (int)($_REQUEST['device_id'] ?? 0);

$db = @new mysqli($DB_HOST, $DB_USER, $DB_PASS, $DB_NAME);
if ($db->connect_errno) out(['ok' => false, 'error' => 'db_connect'], 500);
$db->set_charset('utf8');

// ─── helpers RADIUS ───
function norm_mac($m) { $x = strtoupper(trim(str_replace('-', ':', (string) $m))); return preg_match('/^([0-9A-F]{2}:){5}[0-9A-F]{2}$/', $x) ? $x : null; }
function radacct_latest($rdb, $username) {
  $st = $rdb->prepare("SELECT callingstationid, acctstoptime, framedipaddress, nasipaddress, acctstarttime FROM radacct WHERE username=? ORDER BY radacctid DESC LIMIT 1");
  if (!$st) return null; $st->bind_param('s', $username); $st->execute(); return $st->get_result()->fetch_assoc() ?: null;
}

// ─── MODE BULK (?bulk=1) : réconciliation MAC park-wide (job quotidien 03h). MAC RADIUS courante par appareil sans-fil. ───
if (($_REQUEST['bulk'] ?? '') === '1') {
  $rows = array(); $users = array();
  $rs = $db->query("SELECT d.id device_id, d.manage, d.mac, s.radius_user FROM device d JOIN service s ON s.device_id=d.id AND s.radius_user<>'' AND s.status=1 WHERE d.category IN ('airos_ac','airosm','cambium') AND d.manage<>''");
  if ($rs) while ($r = $rs->fetch_assoc()) { $rows[] = $r; $users[$r['radius_user']] = true; }
  $users = array_keys($users);
  $macByUser = array();
  $rdb = @new mysqli($RADIUS_HOST, $DB_USER, $RADIUS_PASS, $RADIUS_DB);
  if ($rdb && !$rdb->connect_errno) {
    for ($i = 0; $i < count($users); $i += 400) {
      $batch = array_slice($users, $i, 400); if (!$batch) break;
      $in = implode(',', array_fill(0, count($batch), '?'));
      $st = $rdb->prepare("SELECT r.username, r.callingstationid, r.acctstoptime FROM radacct r INNER JOIN (SELECT username, MAX(radacctid) mx FROM radacct WHERE username IN ($in) GROUP BY username) g ON r.radacctid = g.mx");
      if ($st) { $st->bind_param(str_repeat('s', count($batch)), ...$batch); $st->execute(); $res = $st->get_result(); while ($x = $res->fetch_assoc()) $macByUser[$x['username']] = $x; }
    }
    $rdb->close();
  }
  $devs = array();
  foreach ($rows as $r) {
    $ra = $macByUser[$r['radius_user']] ?? null; if (!$ra) continue;
    $cm = norm_mac($ra['callingstationid']); if (!$cm) continue;
    $devs[] = array('device_id' => (int) $r['device_id'], 'radius_user' => $r['radius_user'], 'manage' => $r['manage'], 'device_mac' => $r['mac'], 'radius_mac' => $cm, 'online' => ($ra['acctstoptime'] === null || $ra['acctstoptime'] === ''));
  }
  out(array('ok' => true, 'count' => count($devs), 'devices' => $devs));
}

$row = null;
$fetch1 = function ($sql, $type, $val) use ($db) {
  $st = $db->prepare($sql); if (!$st) return null;
  $st->bind_param($type, $val); $st->execute();
  return $st->get_result()->fetch_assoc() ?: null;
};
$cols = 'id, category, manage, port, user, pass, sn, mac, model';
if ($device_id)          $row = $fetch1("SELECT $cols FROM device WHERE id=? LIMIT 1", 'i', $device_id);
if (!$row && $serial !== '') $row = $fetch1("SELECT $cols FROM device WHERE sn=? AND manage<>'' LIMIT 1", 's', $serial);
if (!$row && $mac !== '')    $row = $fetch1("SELECT $cols FROM device WHERE mac=? AND manage<>'' LIMIT 1", 's', $mac);
if (!$row && $ip !== '')     $row = $fetch1("SELECT $cols FROM device WHERE manage=? LIMIT 1", 's', $ip);
if (!$row) out(['ok' => false, 'error' => 'device introuvable (serial/mac/ip/device_id)'], 404);

if (stripos((string)$row['category'], 'cambium') !== false)
  out(['ok' => false, 'error' => 'Cambium non supporté par ce pont (API différente)'], 200);

$mip = trim((string)$row['manage']);
if ($mip === '') out(['ok' => false, 'error' => 'aucune IP de gestion pour cet appareil'], 200);
$port = (int)($_REQUEST['port'] ?? 0); if ($port <= 0) $port = (int)$row['port']; if ($port <= 0) $port = 2196;
$addr = $mip . ':' . $port;

$user = trim((string)$row['user']);
$pwd  = '';
if (trim((string)$row['pass']) !== '' && function_exists('mcrypt')) { $pwd = @mcrypt('decrypt', $row['pass']); }
if ($user === '' || $pwd === '' || $pwd === false) { $user = 'admin'; $pwd = 'N0HAk4u$'; } // repli = défaut usine F

// RADIUS (WPA2) : radius_user (service) → dernière session radacct → MAC courante + online + IPs (nasip = AP fiable).
$radius_user = null; $radius_mac = null; $ap_ip = null; $r_online = null; $framed_ip = null; $r_since = null;
$stR = $db->prepare("SELECT radius_user FROM service WHERE device_id=? AND radius_user<>'' ORDER BY (status=1) DESC, id DESC LIMIT 1");
if ($stR) { $stR->bind_param('i', $row['id']); $stR->execute(); $rr = $stR->get_result()->fetch_assoc(); if ($rr) $radius_user = $rr['radius_user']; }
if ($radius_user) {
  $rdb = @new mysqli($RADIUS_HOST, $DB_USER, $RADIUS_PASS, $RADIUS_DB);
  if ($rdb && !$rdb->connect_errno) {
    $ra = radacct_latest($rdb, $radius_user);
    if ($ra) { $radius_mac = norm_mac($ra['callingstationid']); $nip = trim((string) $ra['nasipaddress']); if ($nip !== '' && $nip !== '0.0.0.0') $ap_ip = $nip; $r_online = ($ra['acctstoptime'] === null || $ra['acctstoptime'] === ''); $framed_ip = trim((string) $ra['framedipaddress']) ?: null; $r_since = $ra['acctstarttime'] ?: null; }
    $rdb->close();
  }
}

// MODE STATUS (?mode=status) : état RAPIDE via RADIUS seul (aucun status.cgi) — pour l'affichage au chargement de la page.
if (($_REQUEST['mode'] ?? '') === 'status') {
  out(array('ok' => true, 'source' => 'radius', 'kind' => 'sans-fil', 'model' => ($row['model'] ?: null),
    'online' => $r_online, 'since' => $r_since, 'radius_user' => $radius_user, 'radius_mac' => $radius_mac, 'framed_ip' => $framed_ip, 'ap_ip' => $ap_ip));
}

$j = airOS_getFile_HTTPS($user, $pwd, 'status.cgi', $addr);
$s = $j ? json_decode($j, true) : null;
if (!is_array($s) || !isset($s['host'])) out(['ok' => false, 'error' => 'CPE injoignable ou réponse invalide', 'addr' => $addr], 200);

$idx  = array('1x', '2x', '2x', '4x', '4x', '6x', '6x', '8x', '8x');
$w    = $s['wireless'] ?? array();
$sta  = $w['sta'][0] ?? array();
$host = $s['host'] ?? array();
$eth0 = null; $eth1 = null;
foreach (($s['interfaces'] ?? array()) as $if) {
  if (($if['ifname'] ?? '') === 'eth0') $eth0 = $if['status'] ?? array();
  if (($if['ifname'] ?? '') === 'eth1') $eth1 = $if['status'] ?? array();
}
$lan = function ($st) { return ($st && !empty($st['plugged'])) ? (($st['speed'] ?? '?') . ' Mbps' . (!empty($st['duplex']) ? ' [Full]' : ' [Half]')) : 'unplugged'; };
// signal : v8 = wireless.sta[0].signal ; v6 (NanoStation) = wireless.signal (top-niveau).
$signal = $sta['signal'] ?? ($w['signal'] ?? null);
// CCQ (Transmit) = 0-100 % ; certains firmwares exposent un champ hors échelle → on masque l'aberrant.
$ccq = $sta['ccq'] ?? ($w['ccq'] ?? null);
if ($ccq !== null && (!is_numeric($ccq) || $ccq < 0 || $ccq > 100)) $ccq = null;

// Repli AP IP si RADIUS muet : MAC radio de l'AP → ligne device (best-effort).
if ($ap_ip === null) { $apmac = $w['apmac'] ?? ''; if ($apmac !== '') { $st = $db->prepare("SELECT manage FROM device WHERE (mac=? OR mac=?) AND manage<>'' LIMIT 1"); if ($st) { $macNC = str_replace(array(':', '-'), '', $apmac); $st->bind_param('ss', $apmac, $macNC); $st->execute(); $r2 = $st->get_result()->fetch_assoc(); if ($r2) $ap_ip = $r2['manage']; } } }
$eth0b = $eth0 ?: array();

out(array(
  'ok' => true, 'source' => 'f',
  'device_model' => $host['devmodel'] ?? null,
  'device_name'  => $host['hostname'] ?? null,
  'version'      => $host['fwversion'] ?? null,
  'ssid'         => $w['essid'] ?? null,
  'frequency'    => $w['frequency'] ?? null,
  'tx_power'     => $w['txpower'] ?? null,
  'mac_ap'       => $w['apmac'] ?? null,
  'ap_ip'        => $ap_ip,
  'radius_user'  => $radius_user,
  'radius_mac'   => $radius_mac,
  'signal'       => $signal,
  'signal_ap'    => $sta['remote']['signal'] ?? null,
  'ccq'          => $ccq,
  'tx_rate'      => $idx[$w['tx_idx'] ?? -1] ?? null,
  'rx_rate'      => $idx[$w['rx_idx'] ?? -1] ?? null,
  'airmax_capacity_uplink'   => isset($sta['airmax']['uplink_capacity'])   ? round($sta['airmax']['uplink_capacity'] / 1024, 2)   : null,
  'airmax_capacity_downlink' => isset($sta['airmax']['downlink_capacity']) ? round($sta['airmax']['downlink_capacity'] / 1024, 2) : null,
  'connection_time' => sec2t($host['uptime'] ?? 0),
  'if_speed_lan0'   => $lan($eth0),
  'if_speed_lan1'   => $lan($eth1),
  // Compteurs d'octets eth0 (LAN client) → débit LIVE calculé côté OPS (Δoctets/Δs × 8). eth0 rx = upload, tx = download.
  'eth_rx_bytes'    => isset($eth0b['rx_bytes']) ? (float) $eth0b['rx_bytes'] : null,
  'eth_tx_bytes'    => isset($eth0b['tx_bytes']) ? (float) $eth0b['tx_bytes'] : null,
  'ts'              => round(microtime(true) * 1000),
));
