<?php
$summary = $summary ?? [];
$boxes = $boxes ?? [];
$zones = $zones ?? [];
$zonePreviewStats = $zonePreviewStats ?? [];
$filters = $filters ?? ['q' => '', 'zones' => [], 'per_page' => 50];
$pagination = $pagination ?? ['current_page' => 1, 'per_page' => 50, 'per_page_options' => [50], 'total_items' => count($boxes), 'total_pages' => 1, 'offset' => 0];
$stageBox = $stageBox ?? null;
$otbImportError = trim((string)($_SESSION['otb_import_error'] ?? ''));
if ($otbImportError !== '') {
  unset($_SESSION['otb_import_error']);
}

$boxPreviewList = [];
$zoneLookup = [];

foreach ($zones as $zone) {
  $zoneKey = trim((string)($zone['zone_key'] ?? ''));
  if ($zoneKey !== '') {
    $zoneLookup[$zoneKey] = $zone;
  }
}

$normalizeOtbBox = static function (array $box, array $zoneLookup, bool $includePorts = false): array {
  $capacity = max(0, (int)($box['capacity_ports'] ?? 0));
  $occupied = min($capacity, max(0, (int)($box['occupied_ports'] ?? 0)));
  $reserved = min(max(0, $capacity - $occupied), max(0, (int)($box['reserved_ports'] ?? 0)));
  $available = max(0, (int)($box['available_ports'] ?? ($capacity - $occupied - $reserved)));
  $zoneKey = trim((string)($box['zone_key'] ?? ''));
  $zoneLabel = trim((string)($box['zone_label'] ?? ''));
  $zoneData = $zoneLookup[$zoneKey] ?? null;
  $normalized = [
    'id' => (int)($box['id'] ?? 0),
    'box_code' => (string)($box['box_code'] ?? ''),
    'city_name' => (string)($box['city_name'] ?? ''),
    'olt_name' => (string)($box['olt_name'] ?? ''),
    'slot_port' => (string)($box['slot_port'] ?? ''),
    'sector_code' => (string)($box['sector_code'] ?? ''),
    'plaque' => (string)($box['plaque'] ?? ''),
    'hub' => (string)($box['hub'] ?? ''),
    'zone_key' => $zoneKey,
    'zone_label' => $zoneLabel,
    'location_details' => (string)($box['location_details'] ?? ''),
    'latitude' => isset($box['latitude']) ? (float)$box['latitude'] : null,
    'longitude' => isset($box['longitude']) ? (float)$box['longitude'] : null,
    'capacity_ports' => $capacity,
    'occupied_ports' => $occupied,
    'reserved_ports' => $reserved,
    'available_ports' => $available,
    'occupancy_rate' => $capacity > 0 ? round((($occupied + $reserved) / $capacity) * 100, 1) : 0.0,
    'deployment_date' => (string)($box['deployment_date'] ?? ''),
    'saturation_label' => (string)($box['saturation_label'] ?? ''),
    'service_provider' => (string)($box['service_provider'] ?? ''),
    'source_file_name' => (string)($box['source_file_name'] ?? ''),
    'source_imported_at' => (string)($box['source_imported_at'] ?? ''),
    'status' => (string)($box['status'] ?? 'active'),
    'notes' => (string)($box['notes'] ?? ''),
    'radius_meters' => (int)($zoneData['default_radius_meters'] ?? 0),
    'alert_threshold_pct' => (int)($box['alert_threshold_pct'] ?? \App\Core\RaccordementOtbService::defaultAlertThresholdPct()),
    'is_alert' => !empty($box['is_alert']),
  ];

  if (!$includePorts) {
    return $normalized;
  }

  $ports = array_values(array_map(static function (array $port): array {
    $status = (string)($port['status'] ?? 'active');
    if ($status === 'active') {
      $status = 'available';
    }
    return [
      'number' => (int)($port['number'] ?? 0),
      'status' => $status,
      'client_name' => trim((string)($port['client_name'] ?? '')),
      'company_name' => trim((string)($port['company_name'] ?? '')),
      'ref_code' => trim((string)($port['ref_code'] ?? '')),
      'ticket_id' => (int)($port['ticket_id'] ?? 0),
      'report_id' => (int)($port['report_id'] ?? 0),
    ];
  }, is_array($box['ports'] ?? null) ? $box['ports'] : []));

  if ($ports === []) {
    for ($portNumber = 1; $portNumber <= $capacity; $portNumber++) {
      $ports[] = [
        'number' => $portNumber,
        'status' => $portNumber <= $occupied ? 'occupied' : ($portNumber <= ($occupied + $reserved) ? 'reserved' : 'available'),
      ];
    }
  }

  $normalized['ports'] = $ports;

  return $normalized;
};

foreach ($boxes as $box) {
  $boxPreviewList[] = $normalizeOtbBox($box, $zoneLookup, false);
}

$initialPreviewBox = $stageBox ? $normalizeOtbBox($stageBox, $zoneLookup, true) : ($boxPreviewList[0] ?? null);
$buildOtbPageUrl = static function (array $overrides = []) use ($filters, $pagination): string {
  $params = [
    'q' => (string)($filters['q'] ?? ''),
    'zone' => array_values(array_filter((array)($filters['zones'] ?? []), static fn($value): bool => trim((string)$value) !== '')),
    'per_page' => (int)($filters['per_page'] ?? 50),
    'page' => (int)($pagination['current_page'] ?? 1),
  ];

  foreach ($overrides as $key => $value) {
    $params[$key] = $value;
  }

  if (($params['q'] ?? '') === '') {
    unset($params['q']);
  }
  if (empty($params['zone'])) {
    unset($params['zone']);
  }
  if ((int)($params['page'] ?? 1) <= 1) {
    unset($params['page']);
  }

  $query = http_build_query($params);
  return route_url('/otb-management') . ($query !== '' ? '?' . $query : '');
};
?>

<style>
.otb-shell{display:grid;gap:1.25rem}
.otb-hero{border-radius:1.5rem;padding:1.5rem;background:linear-gradient(135deg,#082f49 0%,#0f766e 56%,#34d399 100%);color:#fff;box-shadow:0 1.2rem 2.8rem rgba(8,47,73,.18)}
.otb-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:1rem}
.otb-stat{padding:1rem;border-radius:1rem;background:rgba(255,255,255,.12);border:1px solid rgba(255,255,255,.16)}
.otb-stat strong{display:block;font-size:1.4rem;line-height:1;margin-bottom:.35rem}
.otb-toolbar{display:flex;align-items:flex-start;justify-content:space-between;gap:1rem;flex-wrap:wrap}
.otb-toolbar-copy{max-width:56rem}
.otb-toolbar-copy p{margin:0;color:#5c7281}
.otb-action-bar{display:flex;align-items:center;gap:.75rem;flex-wrap:wrap}
.otb-switch{display:inline-flex;align-items:center;gap:.65rem;padding:.55rem .8rem;border-radius:999px;background:rgba(255,255,255,.72);border:1px solid #dbe7ef;color:#0f172a;font-size:.8rem;font-weight:700}
.otb-switch input{margin:0}
.otb-card{border:1px solid #dbe7ef;border-radius:1.25rem;background:#fff;box-shadow:0 .8rem 1.8rem rgba(15,23,42,.05)}
.otb-card .card-body{padding:1.1rem 1.15rem}
.otb-zone-strip{display:flex;flex-wrap:wrap;gap:.5rem}
.otb-zone-chip{display:inline-flex;align-items:center;gap:.42rem;padding:.42rem .72rem;border-radius:999px;border:1px solid #d8e6ee;background:#fff;color:#34515f;font-size:.77rem;font-weight:700}
.otb-filter-stack{display:grid;gap:.55rem;min-width:0;flex:1 1 280px}
.otb-filter-details{position:relative}
.otb-filter-details summary{list-style:none;display:flex;align-items:center;justify-content:space-between;gap:.75rem;min-height:44px;padding:.68rem .9rem;border-radius:.9rem;border:1px solid #dbe7ef;background:#fff;color:#0f172a;font-weight:700;cursor:pointer;user-select:none}
.otb-filter-details summary::-webkit-details-marker{display:none}
.otb-filter-details[open] summary{border-color:#bfd7e6;box-shadow:0 .4rem 1rem rgba(15,23,42,.06)}
.otb-filter-panel{position:absolute;left:0;top:calc(100% + .4rem);z-index:20;width:min(380px,100%);padding:.85rem;border-radius:1rem;border:1px solid #dbe7ef;background:#fff;box-shadow:0 1rem 2rem rgba(15,23,42,.12)}
.otb-filter-actions{display:flex;gap:.5rem;flex-wrap:wrap;margin-bottom:.7rem}
.otb-filter-actions button{border:1px solid #dbe7ef;background:#f8fbfd;color:#334155;border-radius:999px;padding:.32rem .65rem;font-size:.74rem;font-weight:700}
.otb-filter-options{display:grid;gap:.45rem;max-height:260px;overflow:auto;padding-right:.1rem}
.otb-filter-option{display:flex;align-items:flex-start;gap:.6rem;padding:.55rem .65rem;border:1px solid #e5eef5;border-radius:.85rem;background:#fbfdfe}
.otb-filter-option input{margin-top:.15rem}
.otb-filter-option strong{display:block;font-size:.84rem;color:#0f172a}
.otb-filter-option span{display:block;font-size:.74rem;color:#64748b}
.otb-filter-summary{font-size:.78rem;color:#64748b}
.otb-server-filter{display:grid;gap:1rem;padding:1rem 1.1rem;border:1px solid #dbe7ef;border-radius:1.15rem;background:linear-gradient(180deg,#ffffff 0%,#f8fbfd 100%)}
.otb-server-filter-grid{display:grid;grid-template-columns:minmax(220px,1.4fr) minmax(240px,1fr) 130px auto;gap:.85rem;align-items:end}
.otb-server-filter-actions{display:flex;gap:.65rem;flex-wrap:wrap;align-items:center}
.otb-server-filter .form-control,.otb-server-filter .form-select{min-height:44px;border-radius:.9rem}
.otb-results-meta{display:flex;align-items:center;justify-content:space-between;gap:1rem;flex-wrap:wrap;color:#64748b;font-size:.84rem}
.otb-pagination{display:flex;align-items:center;justify-content:space-between;gap:1rem;flex-wrap:wrap;margin-top:1rem}
.otb-pagination-links{display:flex;gap:.5rem;flex-wrap:wrap}
.otb-pagination-links a,.otb-pagination-links span{display:inline-flex;align-items:center;justify-content:center;min-width:40px;min-height:40px;padding:.45rem .75rem;border-radius:.85rem;border:1px solid #dbe7ef;background:#fff;color:#0f172a;text-decoration:none;font-weight:700}
.otb-pagination-links span.is-active{background:#0f766e;color:#fff;border-color:#0f766e}
.otb-pagination-links span.is-disabled{color:#94a3b8;background:#f8fafc}
.otb-explorer{border:1px solid #dbe7ef;border-radius:1.35rem;background:linear-gradient(180deg,#ffffff 0%,#f7fbfd 100%);box-shadow:0 1rem 2.3rem rgba(15,23,42,.06);overflow:hidden}
.otb-explorer-head{display:flex;align-items:flex-start;justify-content:space-between;gap:1rem;flex-wrap:wrap;padding:1.2rem 1.25rem;border-bottom:1px solid #e5eef5;background:linear-gradient(135deg,rgba(8,47,73,.04) 0%,rgba(52,211,153,.08) 100%)}
.otb-explorer-grid{display:grid;grid-template-columns:minmax(280px,340px) minmax(0,1fr);align-items:start}
.otb-explorer-grid>*{min-width:0}
.otb-browser{padding:1rem;border-right:1px solid #e5eef5;background:#fbfdfe}
.otb-browser-toolbar{display:flex;align-items:center;gap:.75rem;flex-wrap:wrap;margin-bottom:.85rem}
.otb-browser-toolbar .form-select{max-width:260px;border-radius:.9rem;min-height:44px}
.otb-browser-list{display:grid;gap:.8rem;max-height:620px;overflow:auto;padding-right:.15rem}
.otb-browser-card{width:100%;padding:.95rem 1rem;border:1px solid #dbe7ef;border-radius:1rem;background:#fff;text-align:left;box-shadow:0 .35rem .95rem rgba(15,23,42,.04);transition:.18s ease;display:grid;gap:.45rem}
.otb-browser-card:hover{transform:translateY(-1px);box-shadow:0 .7rem 1.3rem rgba(15,23,42,.08)}
.otb-browser-card.is-active{border-color:rgba(13,110,253,.28);background:rgba(13,110,253,.05)}
.otb-browser-card-top,.otb-browser-card-bottom{display:flex;align-items:center;justify-content:space-between;gap:.75rem;flex-wrap:wrap}
.otb-browser-title strong{display:block;color:#0f172a;font-size:.92rem}
.otb-browser-title span{display:block;color:#64748b;font-size:.78rem}
.otb-mini-stack{display:flex;flex-wrap:wrap;gap:.4rem}
.otb-mini-pill{display:inline-flex;align-items:center;gap:.35rem;padding:.28rem .55rem;border-radius:999px;background:#ecfeff;border:1px solid #d6fbff;color:#0f766e;font-size:.72rem;font-weight:700}
.otb-mini-pill.is-warning{background:#fff7ed;border-color:#ffedd5;color:#c2410c}
.otb-mini-pill.is-neutral{background:#f1f5f9;border-color:#e2e8f0;color:#334155}
.otb-browser-empty{padding:1rem;border:1px dashed #cbd5e1;border-radius:1rem;background:#fff;color:#64748b;text-align:center}
.otb-browser-empty strong{display:block;color:#0f172a;margin-bottom:.35rem}
.otb-browser-empty p{margin:0 0 .8rem;color:#64748b}
.otb-stage{padding:1.1rem 1.15rem;display:grid;gap:1rem;min-width:0}
.otb-stage-top{display:flex;align-items:flex-start;justify-content:space-between;gap:1rem;flex-wrap:wrap}
.otb-stage-top>*{min-width:0}
.otb-stage-title{display:grid;gap:.3rem}
.otb-stage-title h2{margin:0;font-size:1.15rem;color:#0f172a}
.otb-stage-title p{margin:0;color:#64748b;max-width:42rem}
.otb-status-chip{display:inline-flex;align-items:center;gap:.45rem;padding:.46rem .75rem;border-radius:999px;font-size:.78rem;font-weight:800;letter-spacing:.03em;text-transform:uppercase;border:1px solid #dbe7ef;background:#f8fafc;color:#334155;max-width:100%}
.otb-status-chip.is-active{background:#ecfdf5;border-color:#bbf7d0;color:#166534}
.otb-status-chip.is-maintenance{background:#fff7ed;border-color:#fed7aa;color:#c2410c}
.otb-status-chip.is-inactive{background:#f8fafc;border-color:#e2e8f0;color:#475569}
.otb-stage-grid{display:grid;grid-template-columns:minmax(300px,1fr) minmax(260px,320px);gap:1rem;align-items:start}
.otb-stage-grid>*{min-width:0}
.otb-visual-card,.otb-meta-card{padding:1rem;border:1px solid #dbe7ef;border-radius:1.15rem;background:#fff;box-shadow:0 .45rem 1rem rgba(15,23,42,.04);min-width:0;align-self:start}
.otb-visual-card{padding:1.15rem;background:linear-gradient(180deg,#fdfefe 0%,#eef6fb 100%)}
.otb-visual-scene{position:relative;min-height:var(--otb-scene-height,360px);border-radius:1.1rem;overflow:hidden;background:radial-gradient(circle at 50% 8%,rgba(255,255,255,.95) 0%,rgba(190,232,227,.72) 22%,rgba(24,79,96,.55) 46%,rgba(15,23,42,.98) 100%);perspective:900px;box-shadow:inset 0 1px 0 rgba(255,255,255,.45);transition:min-height .18s ease}
.otb-visual-scene::before{content:'';position:absolute;inset:0;background:linear-gradient(180deg,rgba(255,255,255,.12) 0%,rgba(255,255,255,0) 28%,rgba(15,23,42,0) 100%);pointer-events:none}
.otb-visual-floor{position:absolute;left:12%;right:12%;bottom:22px;height:28px;border-radius:50%;background:radial-gradient(circle,rgba(148,230,211,.38) 0%,rgba(15,23,42,0) 74%);filter:blur(10px)}
.otb-box-3d{position:absolute;left:50%;top:46%;width:250px;height:var(--otb-box-height,160px);transform-style:preserve-3d;transform:translate(-50%,-50%) scale(1.01) rotateX(-4deg) rotateY(-6deg);animation:otbBoxFloat 8s ease-in-out infinite;transition:height .18s ease}
.otb-box-face{position:absolute;inset:0;border-radius:18px;border:1px solid rgba(255,255,255,.18);backdrop-filter:blur(4px);box-shadow:inset 0 0 0 1px rgba(255,255,255,.05),0 1rem 2rem rgba(15,23,42,.38)}
.otb-box-face.front{background:linear-gradient(180deg,#154b5c 0%,#0f2f3b 100%);transform:translateZ(26px);padding:1rem;display:grid;grid-template-rows:auto auto 1fr auto;gap:.65rem}
.otb-box-face.back{background:linear-gradient(180deg,#10242c 0%,#0a1820 100%);transform:translateZ(-26px)}
.otb-box-face.left,.otb-box-face.right{width:52px;left:99px;background:linear-gradient(180deg,#133846 0%,#0c2430 100%)}
.otb-box-face.left{transform:rotateY(90deg) translateZ(-99px)}
.otb-box-face.right{transform:rotateY(90deg) translateZ(151px)}
.otb-box-face.top,.otb-box-face.bottom{height:52px;top:54px;background:linear-gradient(180deg,#1a5a6f 0%,#103845 100%)}
.otb-box-face.top{transform:rotateX(90deg) translateZ(81px)}
.otb-box-face.bottom{transform:rotateX(90deg) translateZ(-79px)}
.otb-box-kicker{display:flex;justify-content:space-between;align-items:center;gap:.5rem;color:#d3fdf4;font-size:.72rem;font-weight:800;letter-spacing:.08em;text-transform:uppercase}
.otb-box-name{font-size:1.05rem;font-weight:800;color:#fff;letter-spacing:.02em}
.otb-box-subtitle{color:rgba(255,255,255,.7);font-size:.8rem}
.otb-port-grid-3d{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:.48rem;align-content:start}
.otb-port-node{position:relative;display:grid;align-content:start;gap:.18rem;min-height:var(--otb-port-node-height,54px);padding:.42rem .4rem;border-radius:12px;background:rgba(255,255,255,.08);border:1px solid rgba(255,255,255,.08);color:#d7e6ee;font-size:.78rem;font-weight:800;transform:translateZ(0);animation:otbPortPulse 3.6s ease-in-out infinite;overflow:hidden}
.otb-port-node{cursor:pointer;transition:transform .16s ease, box-shadow .16s ease,border-color .16s ease}
.otb-port-node:hover,.otb-port-node:focus-visible{transform:translateY(-1px) scale(1.02);box-shadow:0 .55rem 1rem rgba(15,23,42,.24);border-color:rgba(255,255,255,.24);outline:none}
.otb-port-node::after{content:'';position:absolute;inset:auto 12px -8px 12px;height:10px;border-radius:999px;background:rgba(52,211,153,.18);filter:blur(6px);opacity:.7}
.otb-port-node-number{position:relative;z-index:1;display:block;font-size:.74rem;letter-spacing:.03em;text-transform:uppercase}
.otb-port-node-client{position:relative;z-index:1;display:-webkit-box;overflow:hidden;-webkit-box-orient:vertical;-webkit-line-clamp:2;font-size:.56rem;line-height:1.16;font-weight:700;color:rgba(255,255,255,.96);text-shadow:0 1px 2px rgba(15,23,42,.45);word-break:break-word}
.otb-port-node-client.is-muted{color:rgba(255,255,255,.68);font-weight:600}
.otb-port-node.is-occupied{background:linear-gradient(180deg,rgba(248,113,113,.26) 0%,rgba(127,29,29,.5) 100%);border-color:rgba(248,113,113,.35);color:#ffe4e6}
.otb-port-node.is-occupied::after{background:rgba(248,113,113,.28)}
.otb-port-node.is-reserved{background:linear-gradient(180deg,rgba(192,132,252,.24) 0%,rgba(88,28,135,.5) 100%);border-color:rgba(192,132,252,.38);color:#f3e8ff}
.otb-port-node.is-reserved::after{background:rgba(192,132,252,.3)}
.otb-port-node.is-available{background:linear-gradient(180deg,rgba(52,211,153,.2) 0%,rgba(6,95,70,.42) 100%);border-color:rgba(52,211,153,.32);color:#d1fae5}
.otb-stage-legend{display:flex;flex-wrap:wrap;gap:.45rem}
.otb-stage-legend span{display:inline-flex;align-items:center;gap:.4rem;padding:.28rem .5rem;border-radius:999px;background:#0f172a;color:#e2e8f0;font-size:.68rem}
.otb-stage-legend i{width:10px;height:10px;border-radius:50%;display:inline-block}
.otb-stage-legend .legend-available i{background:#34d399}
.otb-stage-legend .legend-reserved i{background:#c084fc}
.otb-stage-legend .legend-occupied i{background:#f87171}
.otb-stage-legend .legend-shell i{background:#38bdf8}
.otb-meta-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:.8rem;min-width:0}
.otb-meta-item{padding:.85rem .9rem;border-radius:1rem;background:#f8fbfd;border:1px solid #e5eef5}
.otb-meta-item span{display:block;font-size:.74rem;font-weight:800;letter-spacing:.04em;text-transform:uppercase;color:#64748b;margin-bottom:.2rem}
.otb-meta-item strong{display:block;color:#0f172a;font-size:.94rem;overflow-wrap:anywhere}
.otb-notes-panel{padding:.95rem 1rem;border-radius:1rem;background:linear-gradient(180deg,#ffffff 0%,#f8fbfd 100%);border:1px solid #e5eef5;color:#475569;line-height:1.6;white-space:pre-wrap;min-height:110px}
.otb-proximity-card{padding:1rem;border:1px solid #dbe7ef;border-radius:1.15rem;background:linear-gradient(180deg,#ffffff 0%,#f7fbfd 100%);box-shadow:0 .45rem 1rem rgba(15,23,42,.04)}
.otb-proximity-head{display:flex;align-items:flex-start;justify-content:space-between;gap:1rem;flex-wrap:wrap;margin-bottom:.9rem}
.otb-proximity-copy{display:grid;gap:.25rem}
.otb-proximity-copy p{margin:0;color:#64748b}
.otb-radar-board{position:relative;min-height:300px;border-radius:1.1rem;overflow:hidden;border:1px solid #dbe7ef;background:radial-gradient(circle at center,rgba(17,94,89,.18) 0%,rgba(17,94,89,.12) 24%,rgba(255,255,255,.96) 25%,rgba(255,255,255,.98) 100%)}
.otb-radar-board::before{content:'';position:absolute;inset:0;background:radial-gradient(circle at center,rgba(255,255,255,0) 0%,rgba(255,255,255,0) 48%,rgba(15,23,42,.03) 100%)}
.otb-radar-ring{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);border-radius:50%;border:1px dashed rgba(15,118,110,.24)}
.otb-radar-ring.is-outer{width:84%;height:84%}
.otb-radar-ring.is-middle{width:56%;height:56%}
.otb-radar-ring.is-inner{width:28%;height:28%}
.otb-radar-crosshair{position:absolute;left:50%;top:50%;width:1px;height:84%;background:linear-gradient(180deg,rgba(15,118,110,0) 0%,rgba(15,118,110,.22) 18%,rgba(15,118,110,.22) 82%,rgba(15,118,110,0) 100%);transform:translate(-50%,-50%)}
.otb-radar-crosshair.is-horizontal{width:84%;height:1px}
.otb-radar-sweep{position:absolute;left:50%;top:50%;width:84%;height:84%;transform:translate(-50%,-50%);border-radius:50%;background:conic-gradient(from 0deg,rgba(52,211,153,.28),rgba(52,211,153,0) 22%,rgba(52,211,153,0) 100%);filter:blur(1px);animation:otbRadarSweep 4.2s linear infinite;opacity:.95}
.otb-radar-origin{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);display:grid;place-items:center;width:72px;height:72px;border-radius:50%;background:linear-gradient(135deg,#0f766e 0%,#16425b 100%);color:#fff;font-size:.72rem;font-weight:800;letter-spacing:.04em;text-transform:uppercase;box-shadow:0 .7rem 1.4rem rgba(15,23,42,.18);z-index:2}
.otb-radar-point{position:absolute;display:grid;gap:.22rem;min-width:68px;transform:translate(-50%,-50%);text-align:center;z-index:2}
.otb-radar-dot{width:16px;height:16px;border-radius:50%;margin:0 auto;background:#34d399;border:2px solid #fff;box-shadow:0 .25rem .55rem rgba(18,52,77,.12)}
.otb-radar-point.is-far .otb-radar-dot{background:#38bdf8}
.otb-radar-point.is-busy .otb-radar-dot{background:#f59e0b}
.otb-radar-point span{display:block;font-size:.68rem;font-weight:800;color:#34515f;text-shadow:0 1px 0 rgba(255,255,255,.78)}
.otb-radar-point small{display:block;font-size:.62rem;color:#64748b}
.otb-radar-empty{position:absolute;inset:0;display:grid;place-items:center;padding:1.5rem;text-align:center;color:#607583;font-size:.85rem;line-height:1.55}
.otb-radar-meta{display:flex;flex-wrap:wrap;gap:.55rem;margin-top:.9rem}
.otb-radar-pill{display:inline-flex;align-items:center;gap:.38rem;padding:.35rem .62rem;border-radius:999px;background:#f8fafc;border:1px solid #dbe7ef;color:#334155;font-size:.74rem;font-weight:700}
.otb-section-grid{display:grid;gap:1rem}
.otb-three-d-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:1rem}
.otb-three-d-card{position:relative;border:1px solid #dbe7ef;border-radius:1.15rem;background:linear-gradient(180deg,#ffffff 0%,#f7fbfd 100%);padding:1rem;overflow:hidden;box-shadow:0 .55rem 1.1rem rgba(15,23,42,.05)}
.otb-three-d-card::before{content:'';position:absolute;inset:-20% 10% auto 10%;height:120px;background:radial-gradient(circle,rgba(52,211,153,.18) 0%,rgba(255,255,255,0) 70%)}
.otb-three-d-header{position:relative;display:flex;align-items:flex-start;justify-content:space-between;gap:.75rem;flex-wrap:wrap;margin-bottom:.8rem}
.otb-three-d-mini-scene{position:relative;height:180px;border-radius:1rem;background:radial-gradient(circle at top,rgba(52,211,153,.16) 0%,rgba(8,47,73,.08) 30%,rgba(15,23,42,.92) 100%);overflow:hidden}
.otb-three-d-mini-box{position:absolute;left:50%;top:55%;width:136px;height:92px;border-radius:18px;background:linear-gradient(180deg,#184c5d 0%,#102b37 100%);transform:translate(-50%,-50%) rotate(-8deg);box-shadow:0 1rem 1.8rem rgba(15,23,42,.35);animation:otbMiniFloat 7s ease-in-out infinite}
.otb-three-d-mini-box::before,.otb-three-d-mini-box::after{content:'';position:absolute;border-radius:14px}
.otb-three-d-mini-box::before{inset:10px 14px auto 14px;height:12px;background:rgba(255,255,255,.12)}
.otb-three-d-mini-box::after{inset:auto 14px 10px 14px;height:34px;background:linear-gradient(180deg,rgba(255,255,255,.04) 0%,rgba(255,255,255,.12) 100%)}
.otb-three-d-mini-ports{position:absolute;left:50%;bottom:24px;display:grid;grid-template-columns:repeat(4,12px);gap:8px;transform:translateX(-50%)}
.otb-three-d-mini-port{width:12px;height:12px;border-radius:50%;background:#34d399;box-shadow:0 0 12px rgba(52,211,153,.5);animation:otbPortBlink 2.8s ease-in-out infinite}
.otb-three-d-mini-port.is-reserved{background:#c084fc;box-shadow:0 0 12px rgba(192,132,252,.45)}
.otb-three-d-mini-port.is-occupied{background:#f87171;box-shadow:0 0 12px rgba(248,113,113,.45)}
.otb-list-rows{display:grid;gap:.75rem}
.otb-list-row{display:grid;grid-template-columns:minmax(220px,1.05fr) minmax(120px,.55fr) minmax(120px,.55fr) minmax(160px,.8fr) auto;gap:1rem;align-items:center;padding:.95rem 1rem;border:1px solid #dbe7ef;border-radius:1rem;background:#fff}
.otb-list-row strong{display:block;color:#0f172a}
.otb-list-row span{display:block;color:#64748b;font-size:.8rem}
.otb-table-wrap{overflow:auto}
.otb-table{width:100%;min-width:1080px;border-collapse:collapse}
.otb-table th,.otb-table td{padding:.85rem;border-bottom:1px solid #e5eef5;vertical-align:top}
.otb-table th{font-size:.76rem;text-transform:uppercase;letter-spacing:.04em;color:#5d7281;background:#f8fbfd}
.otb-chip{display:inline-flex;align-items:center;gap:.35rem;padding:.38rem .7rem;border-radius:999px;background:#ecfeff;color:#0f766e;font-size:.76rem;font-weight:700}
.otb-modal-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:.9rem}
.otb-modal-grid .form-control,.otb-modal-grid .form-select{min-height:46px;border-radius:.9rem}
.otb-modal-grid .full{grid-column:1/-1}
.otb-port-detail-list{display:grid;gap:.7rem}
.otb-port-detail-item{display:flex;align-items:flex-start;justify-content:space-between;gap:.9rem;padding:.8rem .9rem;border:1px solid #e5eef5;border-radius:1rem;background:#f8fbfd}
.otb-port-detail-copy{display:grid;gap:.18rem;min-width:0}
.otb-port-detail-copy strong{color:#0f172a}
.otb-port-detail-copy span{color:#64748b;font-size:.8rem;overflow-wrap:anywhere}
.otb-port-status-badge{display:inline-flex;align-items:center;gap:.35rem;padding:.28rem .55rem;border-radius:999px;font-size:.72rem;font-weight:800;text-transform:uppercase;letter-spacing:.03em;white-space:nowrap}
.otb-port-status-badge.is-available{background:#ecfdf5;color:#166534}
.otb-port-status-badge.is-reserved{background:#f3e8ff;color:#7c3aed}
.otb-port-status-badge.is-occupied{background:#fee2e2;color:#b91c1c}
.otb-port-modal-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:.9rem}
.otb-port-modal-grid .otb-meta-item{min-height:84px}
.otb-port-modal-grid .full{grid-column:1/-1}
.otb-modal-scroll{margin:1rem auto;max-height:calc(100vh - 2rem)}
.otb-modal-scroll .modal-content{display:flex;flex-direction:column;max-height:calc(100vh - 2rem);overflow:hidden}
.otb-modal-scroll .modal-content > form{display:flex;flex-direction:column;min-height:0;flex:1 1 auto}
.otb-modal-scroll .modal-header,.otb-modal-scroll .modal-footer{flex:0 0 auto}
.otb-modal-scroll .modal-body{flex:1 1 auto;min-height:0;overflow-y:auto;overscroll-behavior:contain}
.otb-muted-note{color:#64748b;font-size:.82rem}
.otb-gps-inline{display:flex;align-items:center;gap:.65rem;flex-wrap:wrap;margin-top:.65rem}
.otb-gps-status{font-size:.8rem;color:#64748b}
.otb-gps-status.is-success{color:#198754}
.otb-gps-status.is-warning{color:#c2410c}
.otb-empty-state{padding:1.2rem;border:1px dashed #cbd5e1;border-radius:1rem;background:#fff;color:#64748b;text-align:center}
@keyframes otbBoxFloat{0%,100%{transform:translate(-50%,-50%) scale(1.01) rotateX(-4deg) rotateY(-6deg)}50%{transform:translate(-50%,-51%) scale(1.03) rotateX(-3deg) rotateY(-4deg)}}
@keyframes otbPortPulse{0%,100%{transform:translateZ(0) scale(1);filter:brightness(1)}50%{transform:translateZ(8px) scale(1.03);filter:brightness(1.12)}}
@keyframes otbMiniFloat{0%,100%{transform:translate(-50%,-50%) rotate(-8deg)}50%{transform:translate(-50%,-54%) rotate(-4deg)}}
@keyframes otbPortBlink{0%,100%{transform:scale(1);opacity:.88}50%{transform:scale(1.14);opacity:1}}
@keyframes otbRadarSweep{from{transform:translate(-50%,-50%) rotate(0deg)}to{transform:translate(-50%,-50%) rotate(360deg)}}
@media (max-width: 1399.98px){
  .otb-stage-grid{grid-template-columns:1fr}
  .otb-meta-grid{grid-template-columns:repeat(2,minmax(0,1fr))}
}
@media (max-width: 1279.98px){
  .otb-explorer-grid,.otb-list-row,.otb-modal-grid,.otb-port-modal-grid{grid-template-columns:1fr}
  .otb-browser{border-right:0;border-bottom:1px solid #e5eef5}
  .otb-browser-list{max-height:none;padding-right:0}
}
@media (max-width: 1199.98px){
  .otb-stage-grid{grid-template-columns:1fr}
  .otb-meta-grid{grid-template-columns:repeat(2,minmax(0,1fr))}
}
@media (max-width: 991.98px){
  .otb-server-filter-grid{grid-template-columns:1fr}
  .otb-meta-grid{grid-template-columns:1fr}
}
@media (max-width: 767.98px){
  .otb-meta-grid{grid-template-columns:1fr}
  .otb-visual-scene{min-height:max(300px,var(--otb-scene-height,300px))}
  .otb-radar-board{min-height:260px}
  .otb-radar-origin{width:60px;height:60px;font-size:.66rem}
  .otb-box-3d{width:220px;height:var(--otb-box-height,146px)}
  .otb-box-3d{transform:translate(-50%,-50%) scale(.94) rotateX(-3deg) rotateY(-5deg)}
  .otb-box-face.left,.otb-box-face.right{left:84px}
  .otb-box-face.left{transform:rotateY(90deg) translateZ(-84px)}
  .otb-box-face.right{transform:rotateY(90deg) translateZ(136px)}
  .otb-modal-scroll{margin:.5rem auto;max-height:calc(100vh - 1rem)}
  .otb-modal-scroll .modal-content{max-height:calc(100vh - 1rem)}
}
</style>

<section class="content">
  <div class="container-fluid">
    <div class="otb-shell">
      <section class="otb-hero">
        <div class="otb-toolbar">
          <div class="otb-toolbar-copy">
            <div class="text-uppercase" style="font-size:.75rem;font-weight:700;letter-spacing:.06em;opacity:.85">Gestion des PCO OTB</div>
            <h1 class="h3 mt-3 mb-2">Référentiel PCO OTB avec pagination et vue 3D ciblée.</h1>
            <p class="mb-0" style="max-width:60rem;opacity:.88">La page charge maintenant uniquement la tranche demandée du référentiel. La vue 3D se concentre sur le PCO sélectionné, avec édition depuis le modal sans surcharger l’écran.</p>
          </div>
          <div class="otb-action-bar">
            <button type="button" class="btn btn-outline-light" data-bs-toggle="modal" data-bs-target="#otbImportModal"><i class="fas fa-file-excel me-1"></i>Importer Excel</button>
            <button type="button" class="btn btn-light" id="openCreateOtbModalBtn"><i class="fas fa-plus me-1"></i>Nouveau PCO</button>
          </div>
        </div>
        <div class="otb-grid mt-4">
          <div class="otb-stat"><strong><?= (int)($summary['total_boxes'] ?? 0) ?></strong><span>PCO enregistrés</span></div>
          <div class="otb-stat"><strong><?= (int)($summary['active_boxes'] ?? 0) ?></strong><span>PCO actifs</span></div>
          <div class="otb-stat"><strong><?= (int)($summary['total_capacity'] ?? 0) ?></strong><span>ports totaux</span></div>
          <div class="otb-stat"><strong><?= (float)($summary['occupancy_rate'] ?? 0) ?>%</strong><span>occupation moyenne</span></div>
          <div class="otb-stat"><strong><?= (int)($summary['alert_boxes'] ?? 0) ?></strong><span>PCO en alerte</span></div>
          <div class="otb-stat"><strong><?= (int)($summary['saturated_boxes'] ?? 0) ?></strong><span>PCO saturés</span></div>
        </div>
      </section>

      <?php if ((int)($summary['alert_boxes'] ?? 0) > 0): ?>
      <div class="alert alert-warning d-flex flex-wrap align-items-center justify-content-between gap-2 mb-0">
        <div>
          <strong><i class="fas fa-triangle-exclamation me-1"></i>Alerte capacité OTB</strong>
          <?= (int)($summary['alert_boxes'] ?? 0) ?> PCO ont atteint leur seuil d'alerte. Les superviseurs Études & Raccordement ont été notifiés lors du franchissement du seuil.
        </div>
        <span class="badge rounded-pill text-bg-light border px-3 py-2">Seuil moyen <?= htmlspecialchars(number_format((float)($summary['avg_alert_threshold_pct'] ?? \App\Core\RaccordementOtbService::defaultAlertThresholdPct()), 1, ',', '')) ?>%</span>
      </div>
      <?php endif; ?>

      <?php if (isset($_GET['saved_box'])): ?>
      <div class="alert alert-success">Le PCO a été enregistré.</div>
      <?php endif; ?>
      <?php if (isset($_GET['imported'])): ?>
      <div class="alert alert-success">Import Excel terminé. <?= (int)($_GET['created'] ?? 0) ?> création(s), <?= (int)($_GET['updated'] ?? 0) ?> mise(s) à jour, <?= (int)($_GET['skipped'] ?? 0) ?> ligne(s) ignorée(s).</div>
      <?php endif; ?>
      <?php if (isset($_GET['deleted_box'])): ?>
      <div class="alert alert-warning">Le PCO a été supprimé.</div>
      <?php endif; ?>
      <?php if (isset($_GET['saved_zone'])): ?>
      <div class="alert alert-info">Les paramètres de zone restent disponibles en base et continuent d’alimenter le moteur de recommandation.</div>
      <?php endif; ?>
      <?php if (($_GET['err'] ?? '') === 'import_missing_file'): ?>
      <div class="alert alert-danger">Aucun fichier Excel n’a été transmis.</div>
      <?php endif; ?>
      <?php if (($_GET['err'] ?? '') === 'import_invalid_format'): ?>
      <div class="alert alert-danger">Le format attendu est un fichier .xlsx aligné sur BASE PCO EN SERVICE.xlsx.</div>
      <?php endif; ?>
      <?php if (($_GET['err'] ?? '') === 'import_parse'): ?>
      <div class="alert alert-danger">Le fichier Excel n’a pas pu être lu.</div>
      <?php endif; ?>
      <?php if (($_GET['err'] ?? '') === 'import_empty'): ?>
      <div class="alert alert-warning">Le fichier Excel ne contient aucune ligne exploitable.</div>
      <?php endif; ?>
      <?php if (($_GET['err'] ?? '') === 'import_failed'): ?>
      <div class="alert alert-danger">
        L’import Excel a échoué pendant l’enregistrement en base.
        <?php if ($otbImportError !== ''): ?>
        <div class="small mt-2"><strong>Détail :</strong> <?= htmlspecialchars($otbImportError) ?></div>
        <?php endif; ?>
      </div>
      <?php endif; ?>

      <form method="get" action="<?= htmlspecialchars(route_url('/otb-management')) ?>" class="otb-server-filter">
        <div class="otb-toolbar">
          <div class="otb-toolbar-copy">
            <h2 class="h5 mb-1">Filtrer le référentiel PCO</h2>
            <p>La page charge uniquement la tranche demandée. Utilisez la recherche, les zones et la pagination pour parcourir plus de 11000 PCO sans alourdir l’affichage.</p>
          </div>
        </div>
        <div class="otb-server-filter-grid">
          <div>
            <label class="form-label">Recherche PCO / OLT / plaque / hub</label>
            <input type="search" class="form-control" name="q" value="<?= htmlspecialchars((string)($filters['q'] ?? '')) ?>" placeholder="Ex: PCO001, 7TR01, OLT_DON_MELO">
          </div>
          <div class="otb-filter-stack">
            <label class="form-label mb-0">Zones</label>
            <details class="otb-filter-details">
              <summary>
                <span><i class="fas fa-filter me-2"></i>Choix multiple</span>
                <span class="small text-muted" id="otbZoneFilterCount"><?= !empty($filters['zones']) ? count((array)$filters['zones']) . ' zone(s)' : 'Toutes' ?></span>
              </summary>
              <div class="otb-filter-panel">
                <div class="otb-filter-actions">
                  <button type="button" id="otbZoneSelectAll">Tout sélectionner</button>
                  <button type="button" id="otbZoneClearAll">Tout effacer</button>
                </div>
                <div class="otb-filter-options">
                  <?php foreach ($zonePreviewStats as $zoneKey => $zoneInfo): ?>
                  <label class="otb-filter-option">
                    <input type="checkbox" class="form-check-input otb-zone-filter-checkbox" name="zone[]" value="<?= htmlspecialchars((string)$zoneKey) ?>" data-label="<?= htmlspecialchars((string)$zoneInfo['label']) ?>"<?= in_array((string)$zoneKey, (array)($filters['zones'] ?? []), true) ? ' checked' : '' ?>>
                    <span>
                      <strong><?= htmlspecialchars((string)$zoneInfo['label']) ?></strong>
                      <span><?= (int)($zoneInfo['count'] ?? 0) ?> PCO<?php if ((int)($zoneInfo['radius'] ?? 0) > 0): ?> · rayon <?= (int)$zoneInfo['radius'] ?> m<?php endif; ?></span>
                    </span>
                  </label>
                  <?php endforeach; ?>
                  <?php if (empty($zonePreviewStats)): ?>
                  <div class="otb-filter-summary">Aucune zone exploitable disponible.</div>
                  <?php endif; ?>
                </div>
              </div>
            </details>
            <div class="otb-filter-summary" id="otbZoneFilterSummary">Toutes les zones sont affichées.</div>
          </div>
          <div>
            <label class="form-label">Par page</label>
            <select class="form-select" name="per_page">
              <?php foreach ((array)($pagination['per_page_options'] ?? []) as $perPageOption): ?>
              <option value="<?= (int)$perPageOption ?>"<?= (int)$perPageOption === (int)($filters['per_page'] ?? 50) ? ' selected' : '' ?>><?= (int)$perPageOption ?></option>
              <?php endforeach; ?>
            </select>
          </div>
          <div class="otb-server-filter-actions">
            <button type="submit" class="btn btn-primary"><i class="fas fa-sliders me-1"></i>Appliquer</button>
            <a href="<?= htmlspecialchars(route_url('/otb-management')) ?>" class="btn btn-outline-secondary">Réinitialiser</a>
          </div>
        </div>
        <div class="otb-results-meta">
          <span>
            <?php if ((int)($pagination['total_items'] ?? 0) > 0): ?>
            Affichage de <?= (int)($pagination['offset'] ?? 0) + 1 ?> à <?= min((int)($pagination['offset'] ?? 0) + count($boxPreviewList), (int)($pagination['total_items'] ?? 0)) ?> sur <?= (int)($pagination['total_items'] ?? 0) ?> PCO.
            <?php else: ?>
            Aucun PCO ne correspond aux filtres courants.
            <?php endif; ?>
          </span>
          <span>Page <?= (int)($pagination['current_page'] ?? 1) ?> / <?= max(1, (int)($pagination['total_pages'] ?? 1)) ?></span>
        </div>
      </form>

      <section class="otb-explorer">
        <div class="otb-explorer-head">
          <div>
            <div class="text-uppercase text-muted" style="font-size:.74rem;font-weight:800;letter-spacing:.08em">PCO sélectionné</div>
            <h2 class="h4 mt-2 mb-2">Présentation 3D détaillée</h2>
            <p>Le panneau principal reste dédié à l’inspection rapide d’un PCO: ports libres ou occupés, zone, coordonnées et capacité exploitable.</p>
          </div>
          <label class="otb-switch">
            <input type="checkbox" class="form-check-input" id="otbProximityToggle">
            <span>Proximité activée</span>
          </label>
        </div>
        <div class="otb-explorer-grid">
          <aside class="otb-browser">
            <div class="otb-browser-toolbar">
              <div class="otb-muted-note">Page courante du référentiel. Sélectionnez un PCO pour mettre à jour la scène 3D et les détails techniques.</div>
            </div>
            <div class="otb-browser-list" id="otbBrowserList">
              <?php foreach ($boxPreviewList as $box): ?>
              <button type="button" class="otb-browser-card<?= (int)($box['id'] ?? 0) === (int)($initialPreviewBox['id'] ?? 0) ? ' is-active' : '' ?>" data-otb-browser-card="<?= (int)$box['id'] ?>" data-zone-key="<?= htmlspecialchars((string)($box['zone_key'] !== '' ? $box['zone_key'] : ($box['zone_label'] !== '' ? $box['zone_label'] : 'hors_zone'))) ?>">
                <div class="otb-browser-card-top">
                  <div class="otb-browser-title">
                    <strong><?= htmlspecialchars((string)$box['box_code']) ?></strong>
                    <span><?= htmlspecialchars((string)($box['zone_label'] ?: 'Zone libre')) ?></span>
                  </div>
                  <div class="otb-mini-stack">
                    <span class="otb-mini-pill<?= ($box['status'] ?? 'active') === 'maintenance' ? ' is-warning' : ' is-neutral' ?>"><i class="fas fa-signal"></i><?= htmlspecialchars((string)$box['status']) ?></span>
                  </div>
                </div>
                <div class="otb-browser-card-bottom">
                  <div class="otb-mini-stack">
                    <span class="otb-mini-pill"><i class="fas fa-ethernet"></i><?= (int)$box['available_ports'] ?> libre(s)</span>
                    <span class="otb-mini-pill is-neutral"><i class="fas fa-layer-group"></i><?= (int)$box['occupied_ports'] ?>/<?= (int)$box['capacity_ports'] ?></span>
                  </div>
                  <span class="small text-muted"><?= htmlspecialchars((string)($box['plaque'] ?: ($box['hub'] ?: 'Sans plaque'))) ?></span>
                </div>
              </button>
              <?php endforeach; ?>
              <?php if (empty($boxPreviewList)): ?>
              <div class="otb-browser-empty">
                <strong>Aucun PCO disponible</strong>
                <p>Le référentiel OTB est vide pour le moment. Créez un premier PCO pour alimenter la vue 3D, la liste et le tableau.</p>
                <button type="button" class="btn btn-primary btn-sm js-open-create-otb"><i class="fas fa-plus me-1"></i>Créer le premier PCO</button>
              </div>
              <?php endif; ?>
            </div>
          </aside>

          <div class="otb-stage">
            <div class="otb-stage-top">
              <div class="otb-stage-title">
                <h2 id="otbStageCode"><?= htmlspecialchars((string)($initialPreviewBox['box_code'] ?? 'Aucun PCO sélectionné')) ?></h2>
                <p id="otbStageSummary"><?= $initialPreviewBox ? htmlspecialchars((string)(($initialPreviewBox['zone_label'] ?: 'Zone libre') . ' · ' . ($initialPreviewBox['plaque'] ?: ($initialPreviewBox['hub'] ?: 'Référentiel OTB')))) : 'Ajoutez un PCO pour activer la scène 3D.' ?></p>
              </div>
              <span class="otb-status-chip<?= $initialPreviewBox ? ' is-' . htmlspecialchars((string)($initialPreviewBox['status'] ?? 'active')) : '' ?>" id="otbStageStatus"><?= htmlspecialchars((string)($initialPreviewBox['status'] ?? 'inactive')) ?></span>
            </div>
            <div class="otb-stage-grid">
              <div class="otb-visual-card">
                <div class="otb-visual-scene">
                  <div class="otb-visual-floor"></div>
                  <div class="otb-box-3d">
                    <div class="otb-box-face front">
                      <div class="otb-box-kicker">
                        <span>OTB Fiber Node</span>
                        <span id="otbStagePortsLabel"><?= $initialPreviewBox ? (int)$initialPreviewBox['capacity_ports'] . ' ports' : '0 port' ?></span>
                      </div>
                      <div>
                        <div class="otb-box-name" id="otbStageFrontCode"><?= htmlspecialchars((string)($initialPreviewBox['box_code'] ?? 'OTB')) ?></div>
                        <div class="otb-box-subtitle" id="otbStageFrontZone"><?= htmlspecialchars((string)($initialPreviewBox['zone_label'] ?? 'Zone libre')) ?></div>
                      </div>
                      <div class="otb-port-grid-3d" id="otbPortGrid3d"></div>
                      <div class="otb-stage-legend">
                        <span class="legend-available"><i></i>Port libre</span>
                        <span class="legend-reserved"><i></i>Port réservé</span>
                        <span class="legend-occupied"><i></i>Port occupé</span>
                        <span class="legend-shell"><i></i>PCO animé</span>
                      </div>
                    </div>
                    <div class="otb-box-face back"></div>
                    <div class="otb-box-face left"></div>
                    <div class="otb-box-face right"></div>
                    <div class="otb-box-face top"></div>
                    <div class="otb-box-face bottom"></div>
                  </div>
                </div>
              </div>
              <div class="otb-meta-card">
                <div class="otb-meta-grid">
                  <div class="otb-meta-item"><span>Zone</span><strong id="otbMetaZone"><?= htmlspecialchars((string)($initialPreviewBox['zone_label'] ?? 'Non défini')) ?></strong></div>
                  <div class="otb-meta-item"><span>Plaque / Hub</span><strong id="otbMetaPlaque"><?= htmlspecialchars((string)(($initialPreviewBox['plaque'] ?? '') ?: (($initialPreviewBox['hub'] ?? '') ?: 'Non défini'))) ?></strong></div>
                  <div class="otb-meta-item"><span>Coordonnées</span><strong id="otbMetaGps"><?= $initialPreviewBox ? htmlspecialchars(number_format((float)$initialPreviewBox['latitude'], 6, '.', '') . ', ' . number_format((float)$initialPreviewBox['longitude'], 6, '.', '')) : 'Non défini' ?></strong></div>
                  <div class="otb-meta-item"><span>Occupation</span><strong id="otbMetaOccupancy"><?= $initialPreviewBox ? htmlspecialchars((string)(($initialPreviewBox['occupied_ports'] ?? 0) + ($initialPreviewBox['reserved_ports'] ?? 0))) . '/' . htmlspecialchars((string)$initialPreviewBox['capacity_ports']) . ' · ' . htmlspecialchars(number_format((float)$initialPreviewBox['occupancy_rate'], 1, ',', '')) . '%' : '0/0 · 0%' ?></strong></div>
                  <div class="otb-meta-item"><span>Ports réservés</span><strong id="otbMetaReserved"><?= htmlspecialchars((string)($initialPreviewBox['reserved_ports'] ?? 0)) ?></strong></div>
                  <div class="otb-meta-item"><span>Ports libres</span><strong id="otbMetaAvailable"><?= htmlspecialchars((string)($initialPreviewBox['available_ports'] ?? 0)) ?></strong></div>
                  <div class="otb-meta-item"><span>Capacité</span><strong id="otbMetaCapacity"><?= htmlspecialchars((string)($initialPreviewBox['capacity_ports'] ?? 0)) ?> ports</strong></div>
                  <div class="otb-meta-item"><span>Seuil d'alerte</span><strong id="otbMetaThreshold"><?= htmlspecialchars((string)($initialPreviewBox['alert_threshold_pct'] ?? \App\Core\RaccordementOtbService::defaultAlertThresholdPct())) ?>%</strong></div>
                  <div class="otb-meta-item"><span>État alerte</span><strong id="otbMetaAlertState"><?= !empty($initialPreviewBox['is_alert']) ? 'Seuil atteint' : 'Normal' ?></strong></div>
                  <div class="otb-meta-item"><span>Ville / Secteur</span><strong id="otbMetaCitySector"><?= htmlspecialchars((string)(($initialPreviewBox['city_name'] ?? '') !== '' ? ($initialPreviewBox['city_name'] . ' / ' . (($initialPreviewBox['sector_code'] ?? '') ?: 'Secteur non défini')) : 'Non défini')) ?></strong></div>
                  <div class="otb-meta-item"><span>OLT / SLOT</span><strong id="otbMetaNetwork"><?= htmlspecialchars((string)(($initialPreviewBox['olt_name'] ?? '') !== '' ? (($initialPreviewBox['olt_name'] ?? '') . ' / ' . (($initialPreviewBox['slot_port'] ?? '') ?: 'Port non défini')) : 'Non défini')) ?></strong></div>
                  <div class="otb-meta-item"><span>Prestataire</span><strong id="otbMetaProvider"><?= htmlspecialchars((string)(($initialPreviewBox['service_provider'] ?? '') ?: 'Non défini')) ?></strong></div>
                  <div class="otb-meta-item"><span>Déploiement</span><strong id="otbMetaDeployment"><?= htmlspecialchars((string)(($initialPreviewBox['deployment_date'] ?? '') ?: 'Non défini')) ?></strong></div>
                  <div class="otb-meta-item"><span>Saturation</span><strong id="otbMetaSaturation"><?= htmlspecialchars((string)(($initialPreviewBox['saturation_label'] ?? '') ?: 'Non renseignée')) ?></strong></div>
                </div>
                <div class="mt-3">
                  <div class="small text-uppercase text-muted fw-bold mb-2">Localisation terrain</div>
                  <div class="otb-notes-panel" id="otbMetaLocation"><?= htmlspecialchars((string)(($initialPreviewBox['location_details'] ?? '') !== '' ? $initialPreviewBox['location_details'] : 'Aucune localisation terrain importée.')) ?></div>
                </div>
                <div class="mt-3">
                  <div class="small text-uppercase text-muted fw-bold mb-2">Notes et repères terrain</div>
                  <div class="otb-notes-panel" id="otbMetaNotes"><?= htmlspecialchars((string)($initialPreviewBox['notes'] ?? 'Aucune note renseignée pour ce PCO.')) ?></div>
                </div>
              </div>
            </div>
            <div class="otb-proximity-card" id="otbProximityCard" hidden>
              <div class="otb-proximity-head">
                <div class="otb-proximity-copy">
                  <strong>Radar de proximité</strong>
                  <p>Le PCO sélectionné est au centre. Le radar compare les PCO actifs disponibles dans la page filtrée en cours.</p>
                </div>
                <div class="otb-mini-stack">
                  <span class="otb-radar-pill" id="otbRadarCountPill"><i class="fas fa-satellite-dish"></i>0 PCO</span>
                  <span class="otb-radar-pill" id="otbRadarRadiusPill"><i class="fas fa-circle-notch"></i>Rayon 0 m</span>
                </div>
              </div>
              <div class="otb-radar-board" id="otbRadarBoard"></div>
              <div class="otb-radar-meta">
                <span class="otb-radar-pill"><i class="fas fa-bullseye"></i>Centre: PCO sélectionné</span>
                <span class="otb-radar-pill"><i class="fas fa-circle" style="color:#34d399"></i>Disponible proche</span>
                <span class="otb-radar-pill"><i class="fas fa-circle" style="color:#38bdf8"></i>Disponible éloigné</span>
                <span class="otb-radar-pill"><i class="fas fa-circle" style="color:#f59e0b"></i>Ports limités</span>
              </div>
            </div>
          </div>
        </div>
      </section>

      <section class="otb-card">
        <div class="card-body otb-section-grid">
          <div class="otb-toolbar">
            <div class="otb-toolbar-copy">
              <h2 class="h5 mb-1">Tableau paginé des PCO</h2>
              <p>Vue principale du référentiel. Chaque page charge uniquement les PCO affichés et chaque ligne peut alimenter la vue 3D sans recharger tout le catalogue.</p>
            </div>
          </div>
          <div class="otb-table-wrap">
            <table class="otb-table" id="otbDataTable">
              <thead>
                <tr>
                  <th>PCO</th>
                  <th>Réseau</th>
                  <th>Zone</th>
                  <th>GPS</th>
                  <th>Ports</th>
                  <th>Statut</th>
                  <th>Notes</th>
                  <th>Actions</th>
                </tr>
              </thead>
              <tbody>
                <?php foreach ($boxPreviewList as $box): ?>
                <tr data-otb-table-row="<?= (int)$box['id'] ?>" data-zone-key="<?= htmlspecialchars((string)($box['zone_key'] !== '' ? $box['zone_key'] : ($box['zone_label'] !== '' ? $box['zone_label'] : 'hors_zone'))) ?>">
                  <td>
                    <strong><?= htmlspecialchars((string)$box['box_code']) ?></strong><br>
                    <span class="text-muted small"><?= htmlspecialchars((string)($box['plaque'] ?: 'Plaque non renseignée')) ?></span>
                  </td>
                  <td>
                    <strong><?= htmlspecialchars((string)(($box['olt_name'] ?? '') ?: 'OLT non renseignée')) ?></strong><br>
                    <span class="text-muted small"><?= htmlspecialchars((string)(($box['slot_port'] ?? '') ?: (($box['hub'] ?? '') ?: 'Port / hub non renseigné'))) ?></span>
                  </td>
                  <td>
                    <span class="otb-chip"><?= htmlspecialchars((string)($box['zone_label'] ?: 'Zone libre')) ?></span><br>
                    <span class="text-muted small"><?= htmlspecialchars((string)((($box['city_name'] ?? '') ?: 'Ville non renseignée') . ((($box['sector_code'] ?? '') !== '') ? ' · ' . $box['sector_code'] : ''))) ?></span>
                  </td>
                  <td>
                    <?= $box['latitude'] !== null ? htmlspecialchars(number_format((float)$box['latitude'], 6, '.', '')) : '—' ?><br>
                    <?= $box['longitude'] !== null ? htmlspecialchars(number_format((float)$box['longitude'], 6, '.', '')) : '—' ?>
                  </td>
                  <td>
                    <strong><?= (int)($box['reserved_ports'] ?? 0) + (int)$box['occupied_ports'] ?>/<?= (int)$box['capacity_ports'] ?></strong><br>
                    <span class="text-muted small"><?= (int)($box['reserved_ports'] ?? 0) ?> réservé(s) · <?= (int)$box['available_ports'] ?> disponible(s) · Seuil <?= (int)($box['alert_threshold_pct'] ?? \App\Core\RaccordementOtbService::defaultAlertThresholdPct()) ?>%</span>
                  </td>
                  <td><?= htmlspecialchars((string)$box['status']) ?><?= !empty($box['is_alert']) ? '<br><span class="text-warning small fw-bold">Seuil atteint</span>' : '' ?></td>
                  <td class="text-muted small"><?= nl2br(htmlspecialchars((string)($box['notes'] !== '' ? $box['notes'] : '—'))) ?></td>
                  <td>
                    <div class="d-flex gap-2 flex-wrap">
                      <button type="button" class="btn btn-outline-secondary btn-sm" data-otb-focus="<?= (int)$box['id'] ?>"><i class="fas fa-cube me-1"></i>Voir en 3D</button>
                      <button type="button" class="btn btn-outline-primary btn-sm" data-otb-edit="<?= (int)$box['id'] ?>"><i class="fas fa-pen me-1"></i>Modifier</button>
                      <button type="button" class="btn btn-outline-danger btn-sm" data-otb-delete="<?= (int)$box['id'] ?>"><i class="fas fa-trash me-1"></i>Supprimer</button>
                    </div>
                  </td>
                </tr>
                <?php endforeach; ?>
                <?php if (empty($boxPreviewList)): ?>
                <tr><td colspan="8" class="text-center text-muted py-4">Aucun PCO enregistré.</td></tr>
                <?php endif; ?>
              </tbody>
            </table>
          </div>
          <div class="otb-pagination">
            <div class="otb-results-meta">
              <span><?= (int)($pagination['total_items'] ?? 0) ?> PCO au total</span>
              <span><?= count($boxPreviewList) ?> élément(s) dans cette page</span>
            </div>
            <div class="otb-pagination-links">
              <?php if ((int)($pagination['current_page'] ?? 1) > 1): ?>
              <a href="<?= htmlspecialchars($buildOtbPageUrl(['page' => (int)$pagination['current_page'] - 1])) ?>">Précédent</a>
              <?php else: ?>
              <span class="is-disabled">Précédent</span>
              <?php endif; ?>

              <?php for ($pageNumber = max(1, (int)$pagination['current_page'] - 2); $pageNumber <= min((int)($pagination['total_pages'] ?? 1), (int)$pagination['current_page'] + 2); $pageNumber++): ?>
                <?php if ($pageNumber === (int)($pagination['current_page'] ?? 1)): ?>
                <span class="is-active"><?= $pageNumber ?></span>
                <?php else: ?>
                <a href="<?= htmlspecialchars($buildOtbPageUrl(['page' => $pageNumber])) ?>"><?= $pageNumber ?></a>
                <?php endif; ?>
              <?php endfor; ?>

              <?php if ((int)($pagination['current_page'] ?? 1) < (int)($pagination['total_pages'] ?? 1)): ?>
              <a href="<?= htmlspecialchars($buildOtbPageUrl(['page' => (int)$pagination['current_page'] + 1])) ?>">Suivant</a>
              <?php else: ?>
              <span class="is-disabled">Suivant</span>
              <?php endif; ?>
            </div>
          </div>
        </div>
      </section>
    </div>
  </div>
</section>

<div class="modal fade" id="otbBoxModal" tabindex="-1" aria-hidden="true">
  <div class="modal-dialog modal-lg modal-dialog-centered modal-dialog-scrollable otb-modal-scroll">
    <div class="modal-content">
      <form method="post" action="<?= htmlspecialchars(route_url('/otb-management/box/save')) ?>" id="otbBoxModalForm">
        <div class="modal-header">
          <div>
            <h5 class="modal-title mb-1" id="otbBoxModalTitle">Nouveau PCO</h5>
            <div class="otb-muted-note" id="otbBoxModalSubtitle">Ajoutez ou mettez à jour un PCO sans quitter la page catalogue.</div>
          </div>
          <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Fermer"></button>
        </div>
        <div class="modal-body">
          <input type="hidden" name="box_id" id="modalBoxId" value="">
          <div class="otb-modal-grid">
            <div>
              <label class="form-label">PCO</label>
              <input class="form-control" name="box_code" id="modalBoxCode" placeholder="7TR01_PCO001" required>
            </div>
            <div>
              <label class="form-label">Ville</label>
              <input class="form-control" name="city_name" id="modalBoxCity" placeholder="ABIDJAN">
            </div>
            <div>
              <label class="form-label">OLT</label>
              <input class="form-control" name="olt_name" id="modalBoxOlt" placeholder="OLT_DON_MELO">
            </div>
            <div>
              <label class="form-label">SLOT / PORT</label>
              <input class="form-control" name="slot_port" id="modalBoxSlotPort" placeholder="1/1/PON1">
            </div>
            <div>
              <label class="form-label">Code secteur</label>
              <input class="form-control" name="sector_code" id="modalBoxSectorCode" placeholder="7TR01">
            </div>
            <div>
              <label class="form-label">Plaque</label>
              <input class="form-control" name="plaque" id="modalBoxPlaque" placeholder="7eme TRANCHE01">
            </div>
            <div>
              <label class="form-label">JDV / Hub / SPL</label>
              <input class="form-control" name="hub" id="modalBoxHub" placeholder="JDV001">
            </div>
            <div class="full">
              <label class="form-label">Localisation</label>
              <textarea class="form-control" name="location_details" id="modalBoxLocationDetails" rows="3" placeholder="Repère terrain ou adresse de proximité"></textarea>
            </div>
            <div>
              <label class="form-label">Latitude</label>
              <input class="form-control" name="latitude" id="modalBoxLatitude" type="number" step="0.0000001" required>
              <div class="otb-gps-inline">
                <button type="button" class="btn btn-outline-secondary btn-sm" id="modalBoxGpsBtn"><i class="fas fa-location-crosshairs me-1"></i>Ma position GPS automatique</button>
                <span class="otb-gps-status" id="modalBoxGpsStatus">Utilise la géolocalisation du navigateur pour renseigner latitude et longitude.</span>
              </div>
            </div>
            <div>
              <label class="form-label">Longitude</label>
              <input class="form-control" name="longitude" id="modalBoxLongitude" type="number" step="0.0000001" required>
            </div>
            <div>
              <label class="form-label">Brins occupés</label>
              <input class="form-control" name="occupied_ports" id="modalBoxOccupied" type="number" min="0" value="0">
            </div>
            <div>
              <label class="form-label">Brins libres</label>
              <input class="form-control" name="free_ports" id="modalBoxFreePorts" type="number" min="0" value="0">
            </div>
            <div>
              <label class="form-label">Date de déploiement</label>
              <input class="form-control" name="deployment_date" id="modalBoxDeploymentDate" type="date">
            </div>
            <div>
              <label class="form-label">Saturation</label>
              <select class="form-select" name="saturation_label" id="modalBoxSaturation">
                <option value="">Sélectionner</option>
                <option value="Non saturé">Non saturé</option>
                <option value="Saturé">Saturé</option>
                <option value="Hors service">Hors service</option>
              </select>
            </div>
            <div>
              <label class="form-label">Prestataire</label>
              <input class="form-control" name="service_provider" id="modalBoxProvider" placeholder="ITC">
            </div>
          </div>
        </div>
        <div class="modal-footer">
          <button type="button" class="btn btn-light" data-bs-dismiss="modal">Annuler</button>
          <button type="submit" class="btn btn-primary"><i class="fas fa-save me-1"></i>Enregistrer</button>
        </div>
      </form>
    </div>
  </div>
</div>

<div class="modal fade" id="otbImportModal" tabindex="-1" aria-hidden="true">
  <div class="modal-dialog modal-dialog-centered">
    <div class="modal-content">
      <form method="post" action="<?= htmlspecialchars(route_url('/otb-management/import')) ?>" enctype="multipart/form-data">
        <div class="modal-header">
          <div>
            <h5 class="modal-title mb-1">Importer des PCO depuis Excel</h5>
            <div class="otb-muted-note">Le fichier doit être aligné sur BASE PCO EN SERVICE.xlsx.</div>
          </div>
          <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Fermer"></button>
        </div>
        <div class="modal-body">
          <div class="mb-3">
            <label class="form-label">Fichier Excel (.xlsx)</label>
            <input type="file" class="form-control" name="excel_file" accept=".xlsx" required>
          </div>
          <div class="small text-muted">
            Colonnes attendues : ville, OLT, SLOT_PORT, PCO, code secteur, plaque, JDV/Hub, localisation, latitude, longitude, brins occupés, brins libres, date de déploiement, saturation, prestataire.
          </div>
        </div>
        <div class="modal-footer">
          <button type="button" class="btn btn-light" data-bs-dismiss="modal">Annuler</button>
          <button type="submit" class="btn btn-success"><i class="fas fa-file-import me-1"></i>Importer</button>
        </div>
      </form>
    </div>
  </div>
</div>

<div class="modal fade" id="otbPortDetailModal" tabindex="-1" aria-hidden="true">
  <div class="modal-dialog modal-dialog-centered modal-dialog-scrollable otb-modal-scroll">
    <div class="modal-content">
      <div class="modal-header">
        <div>
          <h5 class="modal-title mb-1" id="otbPortDetailTitle">Détail du port</h5>
          <div class="otb-muted-note" id="otbPortDetailSubtitle">Informations du port sélectionné.</div>
        </div>
        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Fermer"></button>
      </div>
      <div class="modal-body">
        <div class="otb-port-modal-grid">
          <div class="otb-meta-item"><span>Statut</span><strong id="otbPortDetailStatus">Libre</strong></div>
          <div class="otb-meta-item"><span>PCO</span><strong id="otbPortDetailBox">OTB</strong></div>
          <div class="otb-meta-item"><span>Client</span><strong id="otbPortDetailClient">Aucun client affecté</strong></div>
          <div class="otb-meta-item"><span>Référence ticket</span><strong id="otbPortDetailRef">Aucune</strong></div>
          <div class="otb-meta-item"><span>Entreprise</span><strong id="otbPortDetailCompany">Non renseignée</strong></div>
          <div class="otb-meta-item"><span>Port</span><strong id="otbPortDetailPort">P0</strong></div>
          <div class="otb-meta-item full"><span>Détail</span><strong id="otbPortDetailSummary">Ce port est disponible pour une nouvelle affectation.</strong></div>
        </div>
      </div>
      <div class="modal-footer">
        <button type="button" class="btn btn-light" data-bs-dismiss="modal">Fermer</button>
      </div>
    </div>
  </div>
</div>

<form method="post" action="<?= htmlspecialchars(route_url('/otb-management/box/delete')) ?>" id="otbDeleteForm" class="d-none">
  <input type="hidden" name="box_id" id="otbDeleteBoxId" value="">
</form>

<script>
const otbExplorerBoxes = <?= json_encode($boxPreviewList, JSON_UNESCAPED_UNICODE) ?>;
const otbInitialStageBox = <?= json_encode($initialPreviewBox, JSON_UNESCAPED_UNICODE) ?>;
const otbBoxDetailEndpoint = <?= json_encode(route_url('/otb-management/box/detail'), JSON_UNESCAPED_UNICODE) ?>;
const otbDetailedBoxCache = otbInitialStageBox && otbInitialStageBox.id ? { [Number(otbInitialStageBox.id)]: otbInitialStageBox } : {};
let currentOtbStageBoxId = Number(<?= (int)($initialPreviewBox['id'] ?? 0) ?>);

function escapeHtml(value) {
  return String(value ?? '')
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#039;');
}

function getOtbExplorerBox(boxId) {
  const numericBoxId = Number(boxId || 0);
  return otbExplorerBoxes.find((box) => Number(box?.id || 0) === numericBoxId) || null;
}

function getOtbCachedBox(boxId) {
  const numericBoxId = Number(boxId || 0);
  return otbDetailedBoxCache[numericBoxId] || getOtbExplorerBox(numericBoxId);
}

async function loadOtbBoxDetail(boxId) {
  const numericBoxId = Number(boxId || 0);
  if (!numericBoxId) {
    return null;
  }
  if (otbDetailedBoxCache[numericBoxId]) {
    return otbDetailedBoxCache[numericBoxId];
  }

  const response = await fetch(`${otbBoxDetailEndpoint}?id=${encodeURIComponent(String(numericBoxId))}`, {
    headers: {
      'X-Requested-With': 'XMLHttpRequest',
      'Accept': 'application/json',
    },
    credentials: 'same-origin',
  });

  if (!response.ok) {
    throw new Error('Impossible de charger le detail du PCO.');
  }

  const payload = await response.json();
  const box = payload?.box || null;
  if (box && Number(box.id || 0) > 0) {
    otbDetailedBoxCache[Number(box.id)] = box;
  }

  return box;
}

function haversineMeters(startLat, startLng, endLat, endLng) {
  const earthRadius = 6371000;
  const dLat = ((endLat - startLat) * Math.PI) / 180;
  const dLng = ((endLng - startLng) * Math.PI) / 180;
  const lat1 = (startLat * Math.PI) / 180;
  const lat2 = (endLat * Math.PI) / 180;
  const a = Math.sin(dLat / 2) ** 2 + Math.cos(lat1) * Math.cos(lat2) * Math.sin(dLng / 2) ** 2;
  return 2 * earthRadius * Math.asin(Math.min(1, Math.sqrt(a)));
}

function formatOtbDistance(distanceMeters) {
  const distance = Number(distanceMeters || 0);
  if (!Number.isFinite(distance) || distance <= 0) {
    return 'Distance non disponible';
  }
  if (distance >= 1000) {
    return `${(distance / 1000).toFixed(2).replace('.', ',')} km`;
  }
  return `${Math.round(distance)} m`;
}

function buildOtbRadarCandidates(centerBox) {
  if (!centerBox || !Number.isFinite(Number(centerBox.latitude)) || !Number.isFinite(Number(centerBox.longitude))) {
    return [];
  }

  return otbExplorerBoxes
    .filter((box) => Number(box?.id || 0) !== Number(centerBox.id || 0))
    .filter((box) => String(box?.status || '') === 'active')
    .filter((box) => Number(box?.available_ports || 0) > 0)
    .filter((box) => Number.isFinite(Number(box?.latitude)) && Number.isFinite(Number(box?.longitude)))
    .map((box) => {
      const distance = haversineMeters(Number(centerBox.latitude), Number(centerBox.longitude), Number(box.latitude), Number(box.longitude));
      return {
        ...box,
        distance_meters: Math.round(distance * 100) / 100,
      };
    })
    .sort((left, right) => Number(left.distance_meters || 0) - Number(right.distance_meters || 0));
}

function renderOtbRadar(centerBox) {
  const radarBoard = document.getElementById('otbRadarBoard');
  const radarCountPill = document.getElementById('otbRadarCountPill');
  const radarRadiusPill = document.getElementById('otbRadarRadiusPill');
  if (!radarBoard) return;

  if (!centerBox || !Number.isFinite(Number(centerBox.latitude)) || !Number.isFinite(Number(centerBox.longitude))) {
    radarBoard.innerHTML = '<div class="otb-radar-empty">Le radar de proximité nécessite des coordonnées GPS valides sur le PCO sélectionné.</div>';
    if (radarCountPill) radarCountPill.innerHTML = '<i class="fas fa-satellite-dish"></i>0 PCO';
    if (radarRadiusPill) radarRadiusPill.innerHTML = '<i class="fas fa-circle-notch"></i>Rayon indisponible';
    return;
  }

  const candidates = buildOtbRadarCandidates(centerBox);
  const radiusMeters = Math.max(Number(centerBox.radius_meters || 0), Number(candidates[0]?.distance_meters || 0), 250);

  if (radarCountPill) {
    radarCountPill.innerHTML = `<i class="fas fa-satellite-dish"></i>${candidates.length} PCO`;
  }
  if (radarRadiusPill) {
    radarRadiusPill.innerHTML = `<i class="fas fa-circle-notch"></i>Rayon ${formatOtbDistance(radiusMeters)}`;
  }

  const baseMarkup = `
    <div class="otb-radar-sweep"></div>
    <div class="otb-radar-ring is-outer"></div>
    <div class="otb-radar-ring is-middle"></div>
    <div class="otb-radar-ring is-inner"></div>
    <div class="otb-radar-crosshair"></div>
    <div class="otb-radar-crosshair is-horizontal"></div>
    <div class="otb-radar-origin">${escapeHtml((centerBox.box_code || 'OTB').replace(/^OTB-?/i, 'OTB'))}</div>`;

  if (!candidates.length) {
    radarBoard.innerHTML = `${baseMarkup}<div class="otb-radar-empty">Aucun autre PCO actif avec ports disponibles n’a été trouvé autour du PCO sélectionné.</div>`;
    return;
  }

  const pointsMarkup = candidates.slice(0, 12).map((candidate, index) => {
    const distance = Number(candidate.distance_meters || 0);
    const normalized = Math.min(1, distance / radiusMeters);
    const angle = (index / Math.max(1, Math.min(12, candidates.length))) * Math.PI * 2 - Math.PI / 2;
    const radial = 18 + (normalized * 30);
    const left = 50 + Math.cos(angle) * radial;
    const top = 50 + Math.sin(angle) * radial;
    const availabilityClass = Number(candidate.available_ports || 0) <= 2 ? ' is-busy' : (normalized > 0.66 ? ' is-far' : '');
    return `
      <div class="otb-radar-point${availabilityClass}" style="left:${left}%;top:${top}%">
        <div class="otb-radar-dot"></div>
        <span>${escapeHtml(candidate.box_code || 'OTB')}</span>
        <small>${escapeHtml(formatOtbDistance(distance))}</small>
      </div>`;
  }).join('');

  radarBoard.innerHTML = `${baseMarkup}${pointsMarkup}`;
}

function renderOtbPorts3d(box) {
  const grid = document.getElementById('otbPortGrid3d');
  const scene = document.querySelector('.otb-visual-scene');
  const shell = document.querySelector('.otb-box-3d');
  if (!grid) return;
  const ports = Array.isArray(box?.ports) ? box.ports : [];
  if (!ports.length) {
    if (scene) scene.style.setProperty('--otb-scene-height', '360px');
    if (shell) shell.style.setProperty('--otb-box-height', '160px');
    grid.style.setProperty('--otb-port-node-height', '54px');
    grid.innerHTML = '<div class="text-white-50 small" style="grid-column:1/-1">Aucun port configuré pour ce PCO.</div>';
    return;
  }

  const rows = Math.max(1, Math.ceil(ports.length / 4));
  const sceneHeight = rows >= 4 ? 500 : (rows === 3 ? 430 : 360);
  const boxHeight = rows >= 4 ? 255 : (rows === 3 ? 205 : 160);
  const portNodeHeight = rows >= 4 ? 46 : (rows === 3 ? 50 : 54);
  if (scene) scene.style.setProperty('--otb-scene-height', `${sceneHeight}px`);
  if (shell) shell.style.setProperty('--otb-box-height', `${boxHeight}px`);
  grid.style.setProperty('--otb-port-node-height', `${portNodeHeight}px`);

  grid.innerHTML = ports.map((port, index) => {
    const rawStatus = String(port?.status || 'available');
    const status = rawStatus === 'occupied' ? 'occupied' : (rawStatus === 'reserved' ? 'reserved' : 'available');
    const delay = `${(index % 8) * 0.12}s`;
    const clientName = String(port?.client_name || '').trim();
    const refCode = String(port?.ref_code || '').trim();
    const tooltip = [clientName, refCode].filter(Boolean).join(' · ');
    const titleAttr = tooltip ? ` title="${escapeHtml(tooltip)}"` : '';
    const overlayText = status === 'available'
      ? 'Libre'
      : (clientName || 'Client non renseigné');
    const overlayClass = status === 'available' ? 'otb-port-node-client is-muted' : 'otb-port-node-client';
    return `<button type="button" class="otb-port-node is-${status}" style="animation-delay:${delay}" data-otb-port-number="${Number(port?.number || 0)}"${titleAttr}><span class="otb-port-node-number">P${Number(port?.number || 0)}</span><span class="${overlayClass}">${escapeHtml(overlayText)}</span></button>`;
  }).join('');
}

function getOtbPortStatusLabel(status) {
  if (status === 'occupied') return 'Occupé';
  if (status === 'reserved') return 'Réservé';
  return 'Libre';
}

function openOtbPortDetailModal(boxId, portNumber) {
  const modalElement = document.getElementById('otbPortDetailModal');
  if (!modalElement || !window.bootstrap?.Modal) return;

  const box = getOtbCachedBox(boxId);
  const ports = Array.isArray(box?.ports) ? box.ports : [];
  const port = ports.find((item) => Number(item?.number || 0) === Number(portNumber || 0));
  if (!box || !port) return;

  const status = String(port.status || 'available');
  const statusLabel = getOtbPortStatusLabel(status);
  const clientName = String(port.client_name || '').trim();
  const companyName = String(port.company_name || '').trim();
  const refCode = String(port.ref_code || '').trim();
  const clientDisplay = clientName || companyName || refCode || '';
  const detailTitle = document.getElementById('otbPortDetailTitle');
  const detailSubtitle = document.getElementById('otbPortDetailSubtitle');
  const detailStatus = document.getElementById('otbPortDetailStatus');
  const detailBox = document.getElementById('otbPortDetailBox');
  const detailClient = document.getElementById('otbPortDetailClient');
  const detailRef = document.getElementById('otbPortDetailRef');
  const detailCompany = document.getElementById('otbPortDetailCompany');
  const detailPort = document.getElementById('otbPortDetailPort');
  const detailSummary = document.getElementById('otbPortDetailSummary');

  if (detailTitle) detailTitle.textContent = `Détail du port P${Number(port.number || 0)}`;
  if (detailSubtitle) detailSubtitle.textContent = `${box.box_code || 'OTB'} · ${box.zone_label || 'Zone libre'}`;
  if (detailStatus) detailStatus.textContent = statusLabel;
  if (detailBox) detailBox.textContent = box.box_code || 'OTB';
  if (detailClient) detailClient.textContent = clientDisplay || 'Aucun client affecté';
  if (detailRef) detailRef.textContent = refCode || 'Aucune';
  if (detailCompany) detailCompany.textContent = companyName || 'Non renseignée';
  if (detailPort) detailPort.textContent = `P${Number(port.number || 0)}`;

  if (detailSummary) {
    detailSummary.textContent = status === 'available'
      ? 'Ce port est disponible pour une nouvelle affectation.'
      : `${statusLabel} pour ${clientDisplay || 'une affectation FTTH non renseignee'}${refCode && clientDisplay !== refCode ? ` sur le ticket ${refCode}` : ''}.`;
  }

  window.bootstrap.Modal.getOrCreateInstance(modalElement).show();
}

function renderOtbStage(box) {
  const stageCode = document.getElementById('otbStageCode');
  const stageSummary = document.getElementById('otbStageSummary');
  const stageStatus = document.getElementById('otbStageStatus');
  const stagePortsLabel = document.getElementById('otbStagePortsLabel');
  const stageFrontCode = document.getElementById('otbStageFrontCode');
  const stageFrontZone = document.getElementById('otbStageFrontZone');
  const metaZone = document.getElementById('otbMetaZone');
  const metaPlaque = document.getElementById('otbMetaPlaque');
  const metaGps = document.getElementById('otbMetaGps');
  const metaOccupancy = document.getElementById('otbMetaOccupancy');
  const metaReserved = document.getElementById('otbMetaReserved');
  const metaAvailable = document.getElementById('otbMetaAvailable');
  const metaCapacity = document.getElementById('otbMetaCapacity');
  const metaThreshold = document.getElementById('otbMetaThreshold');
  const metaAlertState = document.getElementById('otbMetaAlertState');
  const metaCitySector = document.getElementById('otbMetaCitySector');
  const metaNetwork = document.getElementById('otbMetaNetwork');
  const metaProvider = document.getElementById('otbMetaProvider');
  const metaDeployment = document.getElementById('otbMetaDeployment');
  const metaSaturation = document.getElementById('otbMetaSaturation');
  const metaLocation = document.getElementById('otbMetaLocation');
  const metaNotes = document.getElementById('otbMetaNotes');

  if (!box) {
    currentOtbStageBoxId = 0;
    if (stageCode) stageCode.textContent = 'Aucun PCO disponible';
    if (stageSummary) stageSummary.textContent = 'Aucun résultat pour le filtre courant.';
    if (stageStatus) {
      stageStatus.className = 'otb-status-chip';
      stageStatus.textContent = 'indisponible';
    }
    if (stagePortsLabel) stagePortsLabel.textContent = '0 port';
    if (stageFrontCode) stageFrontCode.textContent = 'OTB';
    if (stageFrontZone) stageFrontZone.textContent = 'Zone libre';
    if (metaZone) metaZone.textContent = 'Non défini';
    if (metaPlaque) metaPlaque.textContent = 'Non défini';
    if (metaGps) metaGps.textContent = 'Non défini';
    if (metaOccupancy) metaOccupancy.textContent = '0/0 · 0%';
    if (metaReserved) metaReserved.textContent = '0';
    if (metaAvailable) metaAvailable.textContent = '0';
    if (metaCapacity) metaCapacity.textContent = '0 port';
    if (metaThreshold) metaThreshold.textContent = '<?= (int)\App\Core\RaccordementOtbService::defaultAlertThresholdPct() ?>%';
    if (metaAlertState) metaAlertState.textContent = 'Normal';
    if (metaCitySector) metaCitySector.textContent = 'Non défini';
    if (metaNetwork) metaNetwork.textContent = 'Non défini';
    if (metaProvider) metaProvider.textContent = 'Non défini';
    if (metaDeployment) metaDeployment.textContent = 'Non défini';
    if (metaSaturation) metaSaturation.textContent = 'Non renseignée';
    if (metaLocation) metaLocation.textContent = 'Aucune localisation terrain importée.';
    if (metaNotes) metaNotes.textContent = 'Aucune note disponible.';
    renderOtbPorts3d(null);
    renderOtbRadar(null);
    return;
  }

  currentOtbStageBoxId = Number(box.id || 0);
  const status = String(box.status || 'active');
  if (stageCode) stageCode.textContent = box.box_code || 'OTB';
  if (stageSummary) stageSummary.textContent = `${box.zone_label || 'Zone libre'} · ${box.plaque || box.hub || 'Référentiel OTB'}`;
  if (stageStatus) {
    stageStatus.className = `otb-status-chip is-${status}`;
    stageStatus.textContent = status;
  }
  if (stagePortsLabel) stagePortsLabel.textContent = `${Number(box.capacity_ports || 0)} ports`;
  if (stageFrontCode) stageFrontCode.textContent = box.box_code || 'OTB';
  if (stageFrontZone) stageFrontZone.textContent = box.zone_label || 'Zone libre';
  if (metaZone) metaZone.textContent = box.zone_label || 'Non défini';
  if (metaPlaque) metaPlaque.textContent = box.plaque || box.hub || 'Non défini';
  if (metaGps) metaGps.textContent = Number.isFinite(Number(box.latitude)) && Number.isFinite(Number(box.longitude))
    ? `${Number(box.latitude).toFixed(6)}, ${Number(box.longitude).toFixed(6)}`
    : 'Non défini';
  if (metaOccupancy) metaOccupancy.textContent = `${Number((box.reserved_ports || 0)) + Number(box.occupied_ports || 0)}/${Number(box.capacity_ports || 0)} · ${Number(box.occupancy_rate || 0).toFixed(1).replace('.', ',')}%`;
  if (metaReserved) metaReserved.textContent = String(Number(box.reserved_ports || 0));
  if (metaAvailable) metaAvailable.textContent = String(Number(box.available_ports || 0));
  if (metaCapacity) metaCapacity.textContent = `${Number(box.capacity_ports || 0)} ports`;
  if (metaThreshold) metaThreshold.textContent = `${Number(box.alert_threshold_pct || <?= (int)\App\Core\RaccordementOtbService::defaultAlertThresholdPct() ?>)}%`;
  if (metaAlertState) metaAlertState.textContent = Number(box.is_alert ? 1 : 0) === 1 ? 'Seuil atteint' : 'Normal';
  if (metaCitySector) metaCitySector.textContent = [box.city_name || '', box.sector_code || ''].filter(Boolean).join(' / ') || 'Non défini';
  if (metaNetwork) metaNetwork.textContent = [box.olt_name || '', box.slot_port || ''].filter(Boolean).join(' / ') || 'Non défini';
  if (metaProvider) metaProvider.textContent = box.service_provider || 'Non défini';
  if (metaDeployment) metaDeployment.textContent = box.deployment_date || 'Non défini';
  if (metaSaturation) metaSaturation.textContent = box.saturation_label || 'Non renseignée';
  if (metaLocation) metaLocation.textContent = (box.location_details || '').trim() || 'Aucune localisation terrain importée.';
  if (metaNotes) metaNotes.textContent = (box.notes || '').trim() || 'Aucune note renseignée pour ce PCO.';

  renderOtbPorts3d(box);
  renderOtbRadar(box);
}

function setActiveOtbExplorerCard(boxId) {
  document.querySelectorAll('[data-otb-browser-card]').forEach((node) => {
    const isActive = Number(node.getAttribute('data-otb-browser-card') || 0) === Number(boxId || 0);
    node.classList.toggle('is-active', isActive);
  });
}

async function focusOtb(boxId) {
  const summaryBox = getOtbExplorerBox(boxId);
  const fallbackBox = getOtbCachedBox(boxId);
  setActiveOtbExplorerCard(boxId);
  renderOtbStage(fallbackBox);
  try {
    const detailedBox = await loadOtbBoxDetail(boxId);
    renderOtbStage(detailedBox || summaryBox || fallbackBox);
  } catch (error) {
    renderOtbStage(summaryBox || fallbackBox);
  }
  document.getElementById('otbStageCode')?.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}

function filterOtbCollections() {
  const selectedZoneKeys = Array.from(document.querySelectorAll('.otb-zone-filter-checkbox:checked')).map((node) => node.value);
  const filterSummary = document.getElementById('otbZoneFilterSummary');
  const filterCount = document.getElementById('otbZoneFilterCount');

  if (filterSummary) {
    if (!selectedZoneKeys.length) {
      filterSummary.textContent = 'Toutes les zones seront prises en compte au prochain filtrage.';
    } else if (selectedZoneKeys.length <= 2) {
      const labels = Array.from(document.querySelectorAll('.otb-zone-filter-checkbox:checked')).map((node) => node.getAttribute('data-label') || '').filter(Boolean);
      filterSummary.textContent = labels.join(' | ');
    } else {
      filterSummary.textContent = `${selectedZoneKeys.length} zones selectionnees pour la prochaine requete.`;
    }
  }

  if (filterCount) {
    filterCount.textContent = !selectedZoneKeys.length ? 'Toutes' : `${selectedZoneKeys.length} zone(s)`;
  }
}

function openOtbModal(boxId = null) {
  const modalElement = document.getElementById('otbBoxModal');
  if (!modalElement || !window.bootstrap?.Modal) return;
  const modal = window.bootstrap.Modal.getOrCreateInstance(modalElement);
  const box = boxId ? getOtbExplorerBox(boxId) : null;
  const gpsStatus = document.getElementById('modalBoxGpsStatus');

  document.getElementById('otbBoxModalTitle').textContent = box ? 'Modifier le PCO' : 'Nouveau PCO';
  document.getElementById('otbBoxModalSubtitle').textContent = box
    ? 'Mettez à jour les champs alignés sur le fichier BASE PCO EN SERVICE.xlsx.'
    : 'Ajoutez un nouveau PCO avec les champs du fichier BASE PCO EN SERVICE.xlsx.';
  document.getElementById('modalBoxId').value = box?.id || '';
  document.getElementById('modalBoxCode').value = box?.box_code || '';
  document.getElementById('modalBoxCity').value = box?.city_name || '';
  document.getElementById('modalBoxOlt').value = box?.olt_name || '';
  document.getElementById('modalBoxSlotPort').value = box?.slot_port || '';
  document.getElementById('modalBoxSectorCode').value = box?.sector_code || '';
  document.getElementById('modalBoxPlaque').value = box?.plaque || '';
  document.getElementById('modalBoxHub').value = box?.hub || '';
  document.getElementById('modalBoxLocationDetails').value = box?.location_details || '';
  document.getElementById('modalBoxLatitude').value = box?.latitude ?? '';
  document.getElementById('modalBoxLongitude').value = box?.longitude ?? '';
  document.getElementById('modalBoxOccupied').value = String(box?.occupied_ports || 0);
  document.getElementById('modalBoxFreePorts').value = String(Math.max(0, Number(box?.capacity_ports || 0) - Number(box?.occupied_ports || 0)));
  document.getElementById('modalBoxDeploymentDate').value = box?.deployment_date || '';
  const saturationSelect = document.getElementById('modalBoxSaturation');
  if (saturationSelect) {
    const saturationValue = String(box?.saturation_label || '');
    const hasOption = Array.from(saturationSelect.options).some((option) => option.value === saturationValue);
    if (!hasOption && saturationValue !== '') {
      const extraOption = new Option(saturationValue, saturationValue, false, false);
      saturationSelect.add(extraOption);
    }
    saturationSelect.value = saturationValue;
  }
  document.getElementById('modalBoxProvider').value = box?.service_provider || '';
  if (gpsStatus) {
    gpsStatus.className = 'otb-gps-status';
    gpsStatus.textContent = box
      ? 'Vous pouvez remplacer les coordonnées existantes avec la position GPS courante.'
      : 'Utilise la géolocalisation du navigateur pour renseigner latitude et longitude.';
  }

  modal.show();
}

function updateModalGpsStatus(message, variant = 'muted') {
  const statusNode = document.getElementById('modalBoxGpsStatus');
  if (!statusNode) return;
  statusNode.className = 'otb-gps-status';
  if (variant === 'success') statusNode.classList.add('is-success');
  if (variant === 'warning') statusNode.classList.add('is-warning');
  statusNode.textContent = message;
}

function fillModalWithCurrentGps() {
  const latitudeInput = document.getElementById('modalBoxLatitude');
  const longitudeInput = document.getElementById('modalBoxLongitude');
  if (!latitudeInput || !longitudeInput) return;

  if (!navigator.geolocation) {
    updateModalGpsStatus('La géolocalisation n\'est pas disponible sur cet appareil.', 'warning');
    return;
  }

  updateModalGpsStatus('Recherche de la position GPS en cours…');
  navigator.geolocation.getCurrentPosition(
    (position) => {
      const latitude = Number(position.coords.latitude || 0);
      const longitude = Number(position.coords.longitude || 0);
      const accuracy = Number(position.coords.accuracy || 0);
      latitudeInput.value = latitude.toFixed(7);
      longitudeInput.value = longitude.toFixed(7);
      updateModalGpsStatus(
        accuracy > 0
          ? `Coordonnées récupérées automatiquement. Précision estimée: ${accuracy.toFixed(1)} m.`
          : 'Coordonnées récupérées automatiquement.',
        'success'
      );
    },
    (error) => {
      const reason = error?.message || 'Impossible de récupérer la position actuelle.';
      updateModalGpsStatus(reason, 'warning');
    },
    {
      enableHighAccuracy: true,
      timeout: 12000,
      maximumAge: 0,
    }
  );
}

document.getElementById('openCreateOtbModalBtn')?.addEventListener('click', () => openOtbModal());
document.getElementById('otbProximityToggle')?.addEventListener('change', (event) => {
  const card = document.getElementById('otbProximityCard');
  const enabled = Boolean(event.target instanceof HTMLInputElement ? event.target.checked : false);
  if (card) {
    card.hidden = !enabled;
  }
  if (enabled) {
    renderOtbRadar(getOtbExplorerBox(currentOtbStageBoxId));
  }
});
document.querySelectorAll('.otb-zone-filter-checkbox').forEach((checkbox) => {
  checkbox.addEventListener('change', filterOtbCollections);
});
document.getElementById('otbZoneSelectAll')?.addEventListener('click', () => {
  document.querySelectorAll('.otb-zone-filter-checkbox').forEach((checkbox) => {
    checkbox.checked = true;
  });
  filterOtbCollections();
});
document.getElementById('otbZoneClearAll')?.addEventListener('click', () => {
  document.querySelectorAll('.otb-zone-filter-checkbox').forEach((checkbox) => {
    checkbox.checked = false;
  });
  filterOtbCollections();
});
document.querySelectorAll('.js-open-create-otb').forEach((button) => {
  button.addEventListener('click', () => openOtbModal());
});
document.getElementById('modalBoxGpsBtn')?.addEventListener('click', fillModalWithCurrentGps);
document.getElementById('otbPortGrid3d')?.addEventListener('click', (event) => {
  const target = event.target instanceof Element ? event.target.closest('[data-otb-port-number]') : null;
  if (!target) return;
  const portNumber = Number(target.getAttribute('data-otb-port-number') || 0);
  if (!currentOtbStageBoxId || !portNumber) return;
  openOtbPortDetailModal(currentOtbStageBoxId, portNumber);
});

document.querySelectorAll('[data-otb-browser-card]').forEach((button) => {
  button.addEventListener('click', async () => {
    const boxId = Number(button.getAttribute('data-otb-browser-card') || 0);
    await focusOtb(boxId);
  });
});

document.querySelectorAll('[data-otb-focus]').forEach((button) => {
  button.addEventListener('click', async () => {
    const boxId = Number(button.getAttribute('data-otb-focus') || 0);
    await focusOtb(boxId);
  });
});

document.querySelectorAll('[data-otb-edit]').forEach((button) => {
  button.addEventListener('click', () => {
    const boxId = Number(button.getAttribute('data-otb-edit') || 0);
    openOtbModal(boxId);
  });
});

document.querySelectorAll('[data-otb-delete]').forEach((button) => {
  button.addEventListener('click', () => {
    const boxId = Number(button.getAttribute('data-otb-delete') || 0);
    if (!boxId) return;
    if (!window.confirm('Supprimer ce PCO ?')) return;
    const deleteInput = document.getElementById('otbDeleteBoxId');
    const deleteForm = document.getElementById('otbDeleteForm');
    if (!deleteInput || !deleteForm) return;
    deleteInput.value = String(boxId);
    deleteForm.submit();
  });
});

renderOtbStage(getOtbCachedBox(currentOtbStageBoxId) || otbExplorerBoxes[0] || null);
filterOtbCollections();
</script>