<?php
require_once __DIR__ . '/../auth.php';
if (function_exists('require_login')) require_login('login.php');
if (function_exists('require_any_perm')) require_any_perm(['can_view_dashboard_routers','can_view_dashboard_summary']);
if (session_status() === PHP_SESSION_ACTIVE) { @session_write_close(); }
require_once __DIR__ . '/../db_bootstrap.php';
if (function_exists('ensure_db')) { ensure_db(true); }
require_once __DIR__ . '/../routeros_api.php';
require_once __DIR__ . '/../mikrotik_connection_lib.php';
require_once __DIR__ . '/noc_card_cache.php';
header('Content-Type: application/json; charset=utf-8');
try {
@ini_set('display_errors','0'); while (ob_get_level()>0){@ob_end_clean();} ob_start();
$conn = $GLOBALS['conn'] ?? null; if (!($conn instanceof mysqli)) noc_json_response(['ok'=>false,'items'=>[]],500);
function table_exists_r(mysqli $c,string $t): bool { $t=$c->real_escape_string($t); $r=@$c->query("SHOW TABLES LIKE '$t'"); return $r && $r->num_rows>0; }
function col_exists_r(mysqli $c,string $t,string $col): bool { $t=$c->real_escape_string($t); $col=$c->real_escape_string($col); $r=@$c->query("SHOW COLUMNS FROM `$t` LIKE '$col'"); return $r && $r->num_rows>0; }
function get_routers_r(mysqli $c): array { $rows=[]; if (!table_exists_r($c,'routers')) return $rows; $sql='SELECT * FROM routers'; if (col_exists_r($c,'routers','is_enabled')) $sql.=' WHERE is_enabled=1'; $sql.=' ORDER BY id ASC'; $r=@$c->query($sql); if ($r) while($row=$r->fetch_assoc()) $rows[]=$row; return $rows; }
function get_mt_fallback(mysqli $c): ?array { if (!table_exists_r($c,'mikrotik_settings')) return null; $r=@$c->query('SELECT * FROM mikrotik_settings ORDER BY id ASC LIMIT 1'); $row=$r?$r->fetch_assoc():null; if(!$row) return null; return ['id'=>0,'name'=>'MikroTik','host'=>(string)($row['host']??''),'username'=>(string)($row['username']??''),'password'=>(string)($row['password']??''),'api_port'=>(int)($row['api_port']??8728),'model'=>'—','role'=>'pppoe']; }
function api_connect_r(array $router, ?string &$error=null){ $error=''; return ua_mt_connect_config($router,$error,0.8); }
function router_probe_error_code_r(string $error): string {
 $text=strtolower($error);
 if(str_contains($text,'forbidden by its access permissions')||str_contains($text,'10013')||str_contains($text,'windows')) return 'local_socket_blocked';
 if(str_contains($text,'refused')||str_contains($text,'10061')||str_contains($text,'111')) return 'api_port_refused';
 if(str_contains($text,'auth')||str_contains($text,'login')||str_contains($text,'password')||str_contains($text,'credential')) return 'api_auth_failed';
 if(str_contains($text,'timed out')||str_contains($text,'timeout')||str_contains($text,'10060')||str_contains($text,'110')) return 'network_timeout';
 if(str_contains($text,'unreachable')||str_contains($text,'no route')) return 'network_unreachable';
 return 'api_unavailable';
}
function first_re($rows){ if(!is_array($rows)) return null; foreach($rows as $r){ if(is_array($r)&&(($r['.type']??'')==='!re')) return $r; } return null; }
function count_re($rows): int { $c=0; if(!is_array($rows)) return 0; foreach($rows as $r){ if(is_array($r)&&(($r['.type']??'')==='!re')) $c++; } return $c; }
function temperature_value_r($value): ?float {
 if(is_int($value)||is_float($value)) $number=(float)$value;
 else { $text=str_replace(',','.',trim((string)$value)); if(!preg_match('/-?\d+(?:\.\d+)?/',$text,$match)) return null; $number=(float)$match[0]; }
 return ($number>=-40&&$number<=200)?$number:null;
}
function router_temperature_r($rows): array {
 $found=[];
 if(!is_array($rows)) return ['value'=>null,'sensor'=>''];
 foreach($rows as $row){
  if(!is_array($row)||(($row['.type']??'')!=='!re')) continue;
  $sensor=strtolower(trim((string)($row['name']??'')));
  if($sensor!==''&&str_contains($sensor,'temperature')&&array_key_exists('value',$row)){
   $value=temperature_value_r($row['value']); if($value!==null) $found[$sensor]=$value;
  }
  foreach($row as $key=>$raw){
   $key=strtolower(trim((string)$key));
   if(!str_contains($key,'temperature')) continue;
   $value=temperature_value_r($raw); if($value!==null) $found[$key]=$value;
  }
 }
 foreach(['temperature','board-temperature','cpu-temperature','junction-temperature'] as $preferred){
  if(array_key_exists($preferred,$found)) return ['value'=>$found[$preferred],'sensor'=>$preferred];
 }
 foreach($found as $sensor=>$value) return ['value'=>$value,'sensor'=>$sensor];
 return ['value'=>null,'sensor'=>''];
}
function router_card(array $router, ?array $fallback, array $previous=[]): array {
 $probeTs=time();
 $base=['id'=>(int)($router['id']??0),'name'=>(string)($router['name']??'Router'),'host'=>(string)($router['host']??''),'identity'=>(string)($router['name']??'Router'),'model'=>(string)(($router['model']??'')?:'—'),'version'=>'','uptime'=>'—','cpu_pct'=>0,'ram_pct'=>0,'ppp_active'=>0,'state'=>'WARN','state_note'=>'initial_probe_failed','last_probe_ts'=>$probeTs,'probe_ok'=>false,'probe_error_code'=>'','offline_streak'=>0,'offline_since'=>0];
 $base['temperature_c']=null; $base['temperature_sensor']='';
 // Never combine a managed router row with credentials from the legacy table.
 // A routers row is authoritative; the legacy row is used only when none exist.
 $cands=[$router];
 $probeError='';
 foreach($cands as $cand){ $api=api_connect_r($cand,$probeError); if(!$api) continue; $card=$base; $card['host']=(string)($cand['host']??$card['host']); $resourceOk=false; $identityOk=false; try{$row=first_re($api->command(['/system/resource/print'])); if($row){$resourceOk=true; $cpu=(int)($row['cpu-load']??0); $free=(float)($row['free-memory']??0); $total=(float)($row['total-memory']??0); $card['cpu_pct']=$cpu; $card['ram_pct']=$total>0?(int)round((1-($free/$total))*100):0; $card['uptime']=(string)($row['uptime']??'—'); if(!empty($row['board-name'])) $card['model']=(string)$row['board-name']; elseif(!empty($row['model'])) $card['model']=(string)$row['model']; $card['version']=(string)($row['version']??''); }}catch(Throwable $e){}
 try{$temperature=router_temperature_r($api->command(['/system/health/print'])); $card['temperature_c']=$temperature['value']; $card['temperature_sensor']=$temperature['sensor'];}catch(Throwable $e){}
 try{$row=first_re($api->command(['/system/identity/print'])); if($row){$identityOk=true; $card['identity']=(string)($row['name']??$card['identity']); if($card['name']==='Router'||$card['name']==='') $card['name']=$card['identity']; }}catch(Throwable $e){}
 try{$card['ppp_active']=count_re($api->command(['/ppp/active/print']));}catch(Throwable $e){}
 if(method_exists($api,'disconnect')){try{$api->disconnect();}catch(Throwable $e){}}
 $card['state']=($resourceOk&&$identityOk)?'ONLINE':'PARTIAL';
 $card['state_note']=($resourceOk&&$identityOk)?'':(!$resourceOk?'resource_unavailable':'identity_unavailable');
 $card['probe_ok']=true; $card['probe_error_code']=''; $card['offline_streak']=0; $card['offline_since']=0;
 return $card; }

 // A short API timeout is not proof that the router went offline. Preserve the
 // last measurements and require multiple distinct failed probes over time.
 $card=array_merge($base, array_intersect_key($previous,$base));
 $card['id']=$base['id']; $card['name']=$base['name']; $card['host']=$base['host'];
 $card['last_probe_ts']=$probeTs; $card['probe_ok']=false;
 $card['probe_error_code']=router_probe_error_code_r($probeError);
 // Refused/auth/Windows socket-policy errors describe the API or the local
 // monitoring path. They are not evidence that the physical router is down.
 if(in_array($card['probe_error_code'],['local_socket_blocked','api_port_refused','api_auth_failed','api_unavailable'],true)){
  $card['state']='WARN'; $card['state_note']='api_unavailable_not_router_offline';
  $card['offline_streak']=0; $card['offline_since']=0;
  return $card;
 }
 $wasConnected=in_array(strtoupper((string)($previous['state']??'')),['ONLINE','UP','PARTIAL','ONLINE_PARTIAL'],true);
 $card['offline_streak']=max(0,(int)($previous['offline_streak']??0))+1;
 $card['offline_since']=(int)($previous['offline_since']??0); if($card['offline_since']<=0) $card['offline_since']=$probeTs;
 $confirmed=$card['offline_streak']>=3 && ($probeTs-$card['offline_since'])>=40;
 if($confirmed){ $card['state']='OFFLINE'; $card['state_note']='confirmed_after_retries'; }
 elseif($wasConnected){ $card['state']=(string)$previous['state']; $card['state_note']='rechecking_after_probe_failure'; }
 else { $card['state']='WARN'; $card['state_note']='unconfirmed_probe_failure'; }
 return $card; }
$cacheKey='noc_routers_card'; $cachePath=noc_cache_file($cacheKey); $cache=noc_load_json_file($cachePath, ['ok'=>true,'items'=>[],'online'=>0,'total'=>0,'last_update'=>0]);
$now=time(); $stale=(int)($cache['last_update']??0) < ($now-20); if($stale){ $lock=noc_try_lock($cacheKey); if($lock){ $items=[]; $previousById=[]; foreach((array)($cache['items']??[]) as $old){ $oldId=(int)($old['id']??0); if($oldId>=0) $previousById[$oldId]=$old; } $routers=get_routers_r($conn); $fallback=get_mt_fallback($conn); if(!$routers && $fallback) $routers[]=$fallback; foreach($routers as $r){ $routerId=(int)($r['id']??0); $items[] = router_card($r,$fallback,$previousById[$routerId]??[]); } $online=0; foreach($items as $it){ $s=strtoupper((string)($it['state']??'')); if(in_array($s,['ONLINE','UP','PARTIAL','ONLINE_PARTIAL'],true)) $online++; } $cache=['ok'=>true,'items'=>$items,'online'=>$online,'total'=>count($items),'last_update'=>$now]; noc_save_json_file($cachePath,$cache); noc_unlock($lock);} else { clearstatcache(true,$cachePath); $cache=noc_load_json_file($cachePath,$cache); }}
noc_json_response($cache,200);
} catch (Throwable $e) {
  if (isset($cachePath)) {
    $stale = noc_load_json_file($cachePath, null);
    if (is_array($stale)) { noc_json_response($stale, 200); }
  }
  noc_json_response(['ok'=>true,'items'=>[],'summary'=>['total'=>0,'down'=>0,'total_stations'=>0],'error'=>'fallback'],200);
}
