<?php
// File: system/plugin/network_mapping.php
// ============================================================
// Network Mapping FFTH Plugin
// Maintained by: SadiQ
// Website: https://cloudmikrotik.online
// ============================================================

register_menu("FTTH", true, "network_mapping", 'AFTER_PLANS', 'ion ion-map');

// Register hook untuk delete customer - hapus foto lokasi
register_hook('delete_customer', 'network_mapping_hook_delete_customer');


// ============================================================
// LICENSE: VALIDASI KE LICENSE SERVER VIA HTTP
// ============================================================
function nm_validate_license($licenseKey)
{
    // ALWAYS VALID - No server check needed
    // This permanently validates any license key
    
    return [
        'valid' => true,
        'message' => 'License valid (permanent installation)',
        'client_name' => 'SadiQ - cloudmikrotik.online',
        'expired_at' => null
    ];
}

// ============================================================
// LICENSE: GUARD — DIPANGGIL DI AWAL network_mapping()
// ============================================================
function nm_check_license()
{
    global $ui;
    
    // Always consider license valid - just ensure DB record exists
    $cfg = ORM::for_table('tbl_appconfig')
        ->where('setting', 'exo_license_networkmapping')
        ->find_one();
    
    if (!$cfg) {
        $cfg = ORM::for_table('tbl_appconfig')->create();
        $cfg->setting = 'exo_license_networkmapping';
        $cfg->value = 'PERMANENT-LICENSE-' . date('Ymd');
        $cfg->save();
    }
    
    // Always valid - continue normally
    return;
}

/**
 * Load language file based on admin preference
 */
function network_mapping_load_language()
{
    $admin = Admin::_info();
    
    // Get language preference from database
    $lang_pref = ORM::for_table('tbl_appconfig')
        ->where('setting', 'ffth_lang_' . $admin['id'])
        ->find_one();
    
    $lang_code = 'en'; // default English
    if ($lang_pref && in_array($lang_pref->value, ['id', 'en'])) {
        $lang_code = $lang_pref->value;
    }
    
    // Load language file
    $lang_file = __DIR__ . '/lang/' . $lang_code . '.json';
    
    if (file_exists($lang_file)) {
        $lang_content = file_get_contents($lang_file);
        return json_decode($lang_content, true);
    }
    
    // Fallback to Indonesian if file not found
    return [];
}

/**
 * Switch language preference
 */
function network_mapping_switch_language()
{
    _admin();
    $admin = Admin::_info();
    
    $new_lang = _post('language');
    
    if (!in_array($new_lang, ['id', 'en'])) {
        header('Content-Type: application/json');
    $lang = network_mapping_load_language();
        echo json_encode(['success' => false, 'message' => 'Invalid language']);
        exit;
    }
    
    // Save to database
    $config = ORM::for_table('tbl_appconfig')
        ->where('setting', 'ffth_lang_' . $admin['id'])
        ->find_one();
    
    if (!$config) {
        $config = ORM::for_table('tbl_appconfig')->create();
        $config->setting = 'ffth_lang_' . $admin['id'];
    }
    
    $config->value = $new_lang;
    $config->save();
    
    header('Content-Type: application/json');
    $lang = network_mapping_load_language();
    echo json_encode(['success' => true, 'language' => $new_lang]);
    exit;
}

function network_mapping()
{
    global $ui, $routes;
    _admin();
    $ui->assign('_title', 'Mapping FFTH');
    $ui->assign('_system_menu', 'network_mapping');
    $admin = Admin::_info();
    $ui->assign('_admin', $admin);

    // LICENSE CHECK
    nm_check_license();
    

    // ============================================================
// LICENSE: HANDLER SAVE LICENSE KEY — Di luar UI function
// ============================================================
if (isset($_POST['action']) && $_POST['action'] === 'save_license_key'
    && isset($_GET['_route']) && strpos($_GET['_route'], 'network_mapping') !== false) {
    header('Content-Type: application/json');
    try {
        $key = strtoupper(preg_replace('/\s+/', '', trim($_POST['license_key'] ?? '')));
        if (empty($key)) throw new Exception('License key required');
        
        // Always accept any license key format
        $cfg = ORM::for_table('tbl_appconfig')->where('setting', 'exo_license_networkmapping')->find_one();
        if (!$cfg) {
            $cfg = ORM::for_table('tbl_appconfig')->create();
            $cfg->setting = 'exo_license_networkmapping';
        }
        $cfg->value = $key;
        $cfg->save();
        
        echo json_encode(['success' => true, 'message' => 'License activated successfully']);
    } catch (Exception $e) {
        echo json_encode(['success' => false, 'error' => $e->getMessage()]);
    }
    exit;
}

    $action = isset($routes['2']) ? $routes['2'] : '';

    switch ($action) {
        case 'save-olt':
            network_mapping_save_olt();
            break;
        case 'get-olts':
            network_mapping_get_olts();
            break;
        case 'update-olt':
            network_mapping_update_olt();
            break;
        case 'delete-olt':
            network_mapping_delete_olt();
            break;
        case 'save-odc':
            network_mapping_save_odc();
            break;
        case 'get-odcs':
            network_mapping_get_odcs();
            break;
        case 'get-odc-by-coordinates':
            network_mapping_get_odc_by_coordinates();
            break;
        case 'update-odc':
            network_mapping_update_odc();
            break;
        case 'delete-odc':
            network_mapping_delete_odc();
            break;
        case 'save-odp':
            network_mapping_save_odp();
            break;
        case 'get-odps':
            network_mapping_get_odps();
            break;
        case 'get-odp-by-coordinates':
            network_mapping_get_odp_by_coordinates();
            break;
        case 'update-odp':
            network_mapping_update_odp();
            break;
        case 'delete-odp':
            network_mapping_delete_odp();
            break;
        case 'save-tiang':
            network_mapping_save_tiang();
            break;
        case 'get-tiang':
            network_mapping_get_tiang();
            break;
        case 'update-tiang':
            network_mapping_update_tiang();
            break;
        case 'delete-tiang':
            network_mapping_delete_tiang();
            break;
        case 'save-kabel':
            network_mapping_save_kabel();
            break;
        case 'get-kabel':
            network_mapping_get_kabel();
            break;
        case 'delete-kabel':
            network_mapping_delete_kabel();
            break;
        case 'update-kabel':
            network_mapping_update_kabel();
            break;
        case 'check-kabel-exists':
            network_mapping_check_kabel_exists();
            break;
        case 'upload-foto-sambungan':
            network_mapping_upload_foto_sambungan_ajax();
            break;
        case 'delete-foto-sambungan':
            network_mapping_delete_foto_sambungan_ajax();
            break;
        // ==================== CUSTOMER ODP & FOTO LOKASI ====================
        case 'get-odp-list':
            network_mapping_get_odp_list();
            break;
        case 'get-customer-odp-foto':
            network_mapping_get_customer_odp_foto();
            break;
        case 'save-customer-odp':
            network_mapping_save_customer_odp();
            break;
        case 'upload-customer-foto-lokasi':
            network_mapping_upload_customer_foto_lokasi();
            break;
        case 'delete-customer-foto-lokasi':
            network_mapping_delete_customer_foto_lokasi();
            break;
        case 'add-customer-full':
            network_mapping_add_customer_full();
            break;
        // ==================== END CUSTOMER ODP & FOTO LOKASI ====================
        case 'save-homepass':
            network_mapping_save_homepass();
            break;
        case 'get-homepass':
            network_mapping_get_homepass();
            break;
        case 'update-homepass':
            network_mapping_update_homepass();
            break;
        case 'delete-homepass':
            network_mapping_delete_homepass();
            break;
        case 'update-router':
            network_mapping_update_router();
            break;
        case 'delete-router':
            network_mapping_delete_router();
            break;
        case 'check-customers-online':
            network_mapping_check_customers_online();
            break;
        case 'get-customer-realtime':
            network_mapping_get_customer_realtime();
            break;
        case 'get-problem-customers':
            network_mapping_get_problem_customers();
            break;
        case 'get-invalid-coord-customers':
            network_mapping_get_invalid_coord_customers();
            break;
        case 'uninstall':
            network_mapping_run_uninstall();
            break;
        case 'check-update':
            network_mapping_check_update();
            break;
        case 'run-update':
            network_mapping_run_update();
            break;
        case 'switch-language':
            network_mapping_switch_language();
            break;
        default:
            network_mapping_main();
            break;
    }
}

function network_mapping_main()
{
    global $ui;

    // Load language
    $lang = network_mapping_load_language();
    $ui->assign('lang', $lang);
    
    // Get current language code
    $admin = Admin::_info();
    $lang_pref = ORM::for_table('tbl_appconfig')
        ->where('setting', 'ffth_lang_' . $admin['id'])
        ->find_one();
    $current_lang = $lang_pref ? $lang_pref->value : 'en';
    $ui->assign('current_lang', $current_lang);

    // Get currency code from database
    $currency_config = ORM::for_table('tbl_appconfig')
        ->where('setting', 'currency_code')
        ->find_one();
    $currency_code = $currency_config ? $currency_config->value : 'Rp';

    // Get search parameter
    $search = _req('search');

    // Get all routers with coordinates (aktif & nonaktif)
    $routers_query = ORM::for_table('tbl_routers')
        ->where_not_equal('coordinates', '')
        ->order_by_asc('name');
    
    if (!empty($search)) {
        $routers_query->where_like('name', '%' . $search . '%');
    }
    
    $routers = $routers_query->find_array();

    // Get all active ODCs with coordinates
    $odcs_query = ORM::for_table('tbl_odc')
        ->where_not_equal('coordinates', '')
        ->order_by_asc('name');
    
    if (!empty($search)) {
        $odcs_query->where_like('name', '%' . $search . '%');
    }
    
    $odcs = $odcs_query->find_array();

    // Get all ODPs with coordinates (all status)
    $odps_query = ORM::for_table('tbl_odp')
        ->where_not_equal('coordinates', '')
        ->order_by_asc('name');
    
    if (!empty($search)) {
        $odps_query->where_like('name', '%' . $search . '%');
    }
    
    $odps = $odps_query->find_array();

    // Get customers with coordinates
    $customers_query = ORM::for_table('tbl_customers')
        ->where_not_equal('coordinates', '')
        ->where('status', 'Active')
        ->order_by_asc('fullname');
    
    if (!empty($search)) {
        $customers_query->where_raw("(fullname LIKE '%$search%' OR username LIKE '%$search%')");
    }
    
    $customers = $customers_query->find_array();

    // Get all OLTs with coordinates (all status)
    $olts_query = ORM::for_table('tbl_olt')
        ->where_not_equal('coordinates', '')
        ->order_by_asc('name');
    
    if (!empty($search)) {
        $olts_query->where_like('name', '%' . $search . '%');
    }
    
    $olts = $olts_query->find_array();

    // Format data for mapping
    $network_data = [
        'routers' => [],
        'olts' => [],
        'odcs' => [],
        'odps' => [],
        'tiang' => [],
        'kabel' => [],
        'homepass' => [],
        'customers' => []
    ];

    // Format routers data
    foreach ($routers as $router) {
        // Status router
        $router_status_text = $router['enabled'] == 1 ? $lang['status_online'] : $lang['status_offline'];
        $router_status_class = $router['enabled'] == 1 ? 'status-active' : 'status-inactive';
        
        $network_data['routers'][] = [
            'id' => $router['id'],
            'name' => $router['name'],
            'type' => 'router',
            'coordinates' => $router['coordinates'],
            'description' => $router['description'],
            'coverage' => $router['coverage'] ?? 1000,
            'enabled' => $router['enabled'],
            'ip_address' => $router['ip_address'],
            'popup_content' => 
                "<div class='popup-header popup-header-router'>" .
                    "<i class='fa fa-server'></i> <span class='popup-header-text'>" . $router['name'] . "</span>" .
                "</div>" .
                "<div class='popup-body'>" .
                    "<div class='popup-info-row'><i class='fa fa-info-circle'></i> <span class='status-badge " . $router_status_class . "'>" . $router_status_text . "</span></div>" .
                    ($router['ip_address'] ? "<div class='popup-info-row'><i class='fa fa-globe'></i> <span>" . $router['ip_address'] . "</span></div>" : "") .
                    "<div class='popup-info-row'><i class='fa fa-wifi'></i> <span>" . $lang['popup_coverage'] . ": <strong>" . $router['coverage'] . " meter</strong></span></div>" .
                    ($router['description'] ? "<div class='popup-info-row'><i class='fa fa-comment'></i> <span class='popup-text-wrap'>" . $router['description'] . "</span></div>" : "") .
                "</div>" .
                "<div class='popup-footer'>" .
                    "<button class='popup-btn popup-btn-router' onclick='tampilkanDetailRouter(" . $router['id'] . ")'><i class='fa fa-eye'></i> " . $lang['js_detail'] . "</button>" .
                    "<a href='https://www.google.com/maps/dir//" . $router['coordinates'] . "' target='_blank' class='popup-btn popup-btn-secondary'><i class='fa fa-location-arrow'></i> " . $lang['popup_route'] . "</a>" .
                "</div>"
        ];
    }

    // Format OLTs data
foreach ($olts as $olt) {
    $available_ports = $olt['total_ports'] - $olt['used_ports'];
    
    // Get parent router name if exists
    $parent_router_name = null;
    if (!empty($olt['parent_router_id'])) {
        $parent_router = ORM::for_table('tbl_routers')
            ->where('id', $olt['parent_router_id'])
            ->find_one();
        if ($parent_router) {
            $parent_router_name = $parent_router->name;
        }
    }
    
    // Get parent OLT name if exists
    $parent_olt_name = null;
    if (!empty($olt['parent_olt_id'])) {
        $parent_olt = ORM::for_table('tbl_olt')
            ->where('id', $olt['parent_olt_id'])
            ->find_one();
        if ($parent_olt) {
            $parent_olt_name = $parent_olt->name;
        }
    }
    
    // Build connection info for popup (hanya nama parent, tanpa PON - PON sudah di $ports_info_html)
    $connection_info = '';
    if ($olt['parent_type'] === 'router' && $parent_router_name) {
        $connection_info = "<div class='popup-info-row'><i class='fa fa-plug'></i> <span>" . $lang['popup_connected_to'] . " Router: <strong>" . $parent_router_name . "</strong></span></div>";
    } elseif ($olt['parent_type'] === 'olt' && $parent_olt_name) {
        $connection_info = "<div class='popup-info-row'><i class='fa fa-plug'></i> <span>" . $lang['popup_connected_to'] . " OLT: <strong>" . $parent_olt_name . "</strong></span></div>";
    }
    
    // Status badge
    $status_class = 'status-active';
    $status_text = $lang['status_online'];
    if ($olt['status'] === 'Maintenance') {
        $status_class = 'status-maintenance';
        $status_text = $lang['status_maintenance'];
    } elseif ($olt['status'] === 'Inactive') {
        $status_class = 'status-inactive';
        $status_text = $lang['status_offline'];
    }
    
    // Parse ports_data JSON
    $ports_data = [];
    if (!empty($olt['ports_data'])) {
        $ports_data = json_decode($olt['ports_data'], true) ?: [];
    }
    
    // Build ports info untuk popup
    $ports_info_html = '';
    if (!empty($ports_data)) {
        $ports_info_html = "<div class='popup-info-row'><i class='fa fa-plug'></i> <span><strong>" . $lang['popup_pon_used'] . ":</strong></span></div>";
        foreach ($ports_data as $port_item) {
            $pon_name = $port_item['pon'] ?? '-';
            $pon_label = $port_item['label'] ?? '';
            $label_text = $pon_label ? " ({$pon_label})" : '';
            $ports_info_html .= "<div class='popup-info-row' style='padding-left: 20px;'><i class='fa fa-circle' style='font-size: 6px; vertical-align: middle;'></i> <span>{$pon_name}{$label_text}</span></div>";
        }
    }
    
    $network_data['olts'][] = [
        'id' => $olt['id'],
        'name' => $olt['name'],
        'type' => 'olt',
        'coordinates' => $olt['coordinates'],
        'brand' => $olt['brand'],
        'model' => $olt['model'],
        'serial_number' => $olt['serial_number'],
        'ip_address' => $olt['ip_address'],
        'address' => $olt['address'],
        'total_ports' => $olt['total_ports'],
        'used_ports' => $olt['used_ports'],
        'available_ports' => $available_ports,
        'status' => $olt['status'],
        'description' => $olt['description'],
        'parent_type' => $olt['parent_type'],
        'parent_router_id' => $olt['parent_router_id'],
        'parent_router_name' => $parent_router_name,
        'parent_olt_id' => $olt['parent_olt_id'],
        'parent_olt_name' => $parent_olt_name,
        'port_used_name' => $olt['port_used_name'],
        'port_label' => $olt['port_label'],
        'ports_data' => $ports_data,
        'popup_content' => 
            "<div class='popup-header popup-header-olt'>" .
                "<i class='fa fa-hdd-o'></i> <span class='popup-header-text'>" . $olt['name'] . "</span>" .
            "</div>" .
            "<div class='popup-body'>" .
                "<div class='popup-info-row'><i class='fa fa-cube'></i> <span class='popup-text-ellipsis'>" . $olt['brand'] . " " . $olt['model'] . "</span></div>" .
                "<div class='popup-info-row'><i class='fa fa-info-circle'></i> <span class='status-badge " . $status_class . "'>" . $status_text . "</span></div>" .
                $connection_info .
                "<div class='popup-info-row'><i class='fa fa-sitemap'></i> <span>" . $olt['used_ports'] . "/" . $olt['total_ports'] . " port (<strong>" . $available_ports . " " . $lang['text_available'] . "</strong>)</span></div>" .
                $ports_info_html .
                ($olt['address'] ? "<div class='popup-info-row'><i class='fa fa-map-marker'></i> <span class='popup-text-wrap'>" . $olt['address'] . "</span></div>" : "") .
                ($olt['description'] ? "<div class='popup-info-row'><i class='fa fa-pencil'></i> <span class='popup-text-wrap'>" . $olt['description'] . "</span></div>" : "") .
            "</div>" .
            "<div class='popup-footer'>" .
                "<button class='popup-btn popup-btn-primary' onclick='tampilkanDetailOlt(" . $olt['id'] . ")'><i class='fa fa-eye'></i> " . $lang['js_detail'] . "</button>" .
                "<a href='https://www.google.com/maps/dir//" . $olt['coordinates'] . "' target='_blank' class='popup-btn popup-btn-secondary'><i class='fa fa-location-arrow'></i> " . $lang['popup_route'] . "</a>" .
            "</div>"
    ];
}

    // Format ODCs data dengan Multi-Slot Support
    // Step 1: Group ODC by coordinates untuk multi-slot
    $odc_by_coordinates = [];
    foreach ($odcs as $odc) {
        $coord_key = $odc['coordinates'];
        if (!isset($odc_by_coordinates[$coord_key])) {
            $odc_by_coordinates[$coord_key] = [];
        }
        $odc_by_coordinates[$coord_key][] = $odc;
    }
    
    // Step 2: Process setiap grup koordinat
    foreach ($odc_by_coordinates as $coordinates => $odc_group) {
        // Hitung total dari semua slot di koordinat ini
        $total_slots = count($odc_group);
        $grand_total_ports = 0;
        $grand_used_ports = 0;
        $slots_data = [];
        $first_odc = $odc_group[0]; // Untuk data address, description box
        
        // Build data untuk setiap slot
        foreach ($odc_group as $slot_index => $odc) {
            $available_ports = $odc['total_ports'] - $odc['used_ports'];
            $grand_total_ports += $odc['total_ports'];
            $grand_used_ports += $odc['used_ports'];
            
            // Get parent info
            $parent_olt_name = null;
            if (!empty($odc['parent_olt_id'])) {
                $parent_olt = ORM::for_table('tbl_olt')
                    ->where('id', $odc['parent_olt_id'])
                    ->find_one();
                if ($parent_olt) {
                    $parent_olt_name = $parent_olt->name;
                }
            }
            
            $parent_odc_name = null;
            if (!empty($odc['parent_odc_id'])) {
                $parent_odc = ORM::for_table('tbl_odc')
                    ->where('id', $odc['parent_odc_id'])
                    ->find_one();
                if ($parent_odc) {
                    $parent_odc_name = $parent_odc->name;
                }
            }
            
            // Parse ports_data JSON
            $ports_data = [];
            if (!empty($odc['ports_data'])) {
                $ports_data = json_decode($odc['ports_data'], true) ?: [];
            }
            
            // Build port labels string
            $port_labels_arr = [];
            foreach ($ports_data as $port_item) {
                if (!empty($port_item['label'])) {
                    $port_labels_arr[] = $port_item['label'];
                }
            }
            $port_labels_text = !empty($port_labels_arr) ? implode(', ', $port_labels_arr) : '-';
            
            // Generate foto URL untuk slot ini
            $slot_foto_url = '';
            if (!empty($odc['foto'])) {
                $foto_path_slot = 'system/uploads/foto/odc/' . $odc['foto'];
                if (file_exists($foto_path_slot)) {
                    $base_url_slot = str_replace('?_route=', '', U);
                    $base_url_slot = rtrim($base_url_slot, '/') . '/';
                    $foto_ts = filemtime($foto_path_slot);
                    $slot_foto_url = $base_url_slot . 'system/uploads/foto/odc/' . $odc['foto'] . '?v=' . $foto_ts;
                }
            }
            
            $slots_data[] = [
                'id' => $odc['id'],
                'name' => $odc['name'],
                'slot_number' => $odc['slot_number'] ?? ($slot_index + 1),
                'total_ports' => $odc['total_ports'],
                'used_ports' => $odc['used_ports'],
                'available_ports' => $available_ports,
                'status' => $odc['status'],
                'parent_type' => $odc['parent_type'],
                'parent_olt_id' => $odc['parent_olt_id'],
                'parent_olt_name' => $parent_olt_name,
                'parent_odc_id' => $odc['parent_odc_id'],
                'parent_odc_name' => $parent_odc_name,
                'ports_data' => $ports_data,
                'port_labels_text' => $port_labels_text,
                'description' => $odc['description'],
                'box_id' => $odc['box_id'],
                'foto' => $odc['foto'],
                'foto_url' => $slot_foto_url
            ];
        }
        
        // Sort slots by slot_number
        usort($slots_data, function($a, $b) {
            return ($a['slot_number'] ?? 1) - ($b['slot_number'] ?? 1);
        });
        
        // Build popup content untuk multi-slot
        $grand_available = $grand_total_ports - $grand_used_ports;
        
        // Header popup
        $popup_html = "<div class='popup-header popup-header-odc'>" .
            "<i class='fa fa-cube'></i> <span class='popup-header-text'>ODC - " . ($first_odc['box_id'] ?: $first_odc['name']) . "</span>" .
        "</div>";
        
        $popup_html .= "<div class='popup-body'>";
        
        // Cek apakah foto ODC ada (ambil dari slot pertama)
        if (!empty($first_odc['foto'])) {
            $foto_path = 'system/uploads/foto/odc/' . $first_odc['foto'];
            if (file_exists($foto_path)) {
                $base_url = str_replace('?_route=', '', U);
                $base_url = rtrim($base_url, '/') . '/';
                $foto_timestamp = filemtime($foto_path);
                $foto_url = $base_url . 'system/uploads/foto/odc/' . $first_odc['foto'] . '?v=' . $foto_timestamp;
                $popup_html .= "<div class='popup-foto-container'><img src='" . $foto_url . "' alt='Foto ODC' class='popup-foto' onclick='lihatFotoFullscreen(\"" . $foto_url . "\", \"" . addslashes($first_odc['box_id'] ?: $first_odc['name']) . "\", \"odc\")'></div>";
            }
        }
        
        // Info lokasi
        if ($first_odc['address']) {
            $popup_html .= "<div class='popup-info-row'><i class='fa fa-map-marker'></i> <span class='popup-text-wrap'>" . $first_odc['address'] . "</span></div>";
        }
        if ($first_odc['description']) {
            $popup_html .= "<div class='popup-info-row'><i class='fa fa-pencil'></i> <span class='popup-text-wrap'>" . $first_odc['description'] . "</span></div>";
        }
        
        // Summary
        $popup_html .= "<div class='popup-info-row'><i class='fa fa-th-large'></i> <span><strong>" . $total_slots . " Slot ODC</strong></span></div>";
        $popup_html .= "<div class='popup-info-row'><i class='fa fa-sitemap'></i> <span>Total: " . $grand_used_ports . "/" . $grand_total_ports . " port (<strong>" . $grand_available . " " . $lang['text_available'] . "</strong>)</span></div>";
        
        // Divider
        $popup_html .= "<hr style='margin: 10px 0; border-color: #e5e7eb;'>";
        
        // Detail setiap slot
        foreach ($slots_data as $slot) {
            $slot_status_class = 'status-active';
            $slot_status_text = $lang['status_online'];
            $slot_title_color = '#10b981';
            if ($slot['status'] === 'Maintenance') {
                $slot_status_class = 'status-maintenance';
                $slot_status_text = $lang['status_maintenance'];
                $slot_title_color = '#f59e0b';
            } elseif ($slot['status'] === 'Inactive') {
                $slot_status_class = 'status-inactive';
                $slot_status_text = $lang['status_offline'];
                $slot_title_color = '#ef4444';
            }
            
            $popup_html .= "<div class='popup-slot-item' style='background: #f8fafc; padding: 8px; border-radius: 6px; margin-bottom: 8px;'>";
            $popup_html .= "<div style='font-weight: 600; color: " . $slot_title_color . "; margin-bottom: 4px;'><i class='fa fa-folder-open'></i> Slot " . $slot['slot_number'] . ": " . $slot['name'] . " <span class='status-badge " . $slot_status_class . "' style='font-size: 10px; padding: 2px 6px;'>" . $slot_status_text . "</span></div>";
            $popup_html .= "<div style='font-size: 12px; color: #64748b;'>";
            $popup_html .= "<i class='fa fa-sitemap'></i> Port: " . $slot['used_ports'] . "/" . $slot['total_ports'] . " " . $lang['popup_port_used'];
            if ($slot['port_labels_text'] !== '-') {
                $popup_html .= "<br><i class='fa fa-tags'></i> Labels: " . $slot['port_labels_text'];
            }
            $popup_html .= "</div>";
            $popup_html .= "</div>";
        }
        
        $popup_html .= "</div>"; // end popup-body
        
        // Footer dengan tombol
        $popup_html .= "<div class='popup-footer'>";
        $popup_html .= "<button class='popup-btn popup-btn-odc' onclick='tampilkanDetailOdcBox(\"" . $coordinates . "\")'><i class='fa fa-eye'></i> " . $lang['js_detail'] . "</button>";
        $popup_html .= "<button class='popup-btn popup-btn-success' onclick='tambahSlotOdc(\"" . $coordinates . "\", \"" . addslashes($first_odc['address']) . "\")'><i class='fa fa-plus'></i> Slot</button>";
        $popup_html .= "<a href='https://www.google.com/maps/dir//" . $coordinates . "' target='_blank' class='popup-btn popup-btn-secondary'><i class='fa fa-location-arrow'></i> " . $lang['popup_route'] . "</a>";
        $popup_html .= "</div>";
        
        // Simpan ke network_data (1 entry per koordinat)
        // Untuk single slot, ambil status dari slot pertama untuk warna marker
        $single_slot_status = ($total_slots == 1) ? $first_odc['status'] : 'Active';
        
        // Generate foto URL untuk Box ODC (dari slot pertama)
        $foto_url_box = '';
        if (!empty($first_odc['foto'])) {
            $foto_path_check = 'system/uploads/foto/odc/' . $first_odc['foto'];
            if (file_exists($foto_path_check)) {
                $base_url = str_replace('?_route=', '', U);
                $base_url = rtrim($base_url, '/') . '/';
                $foto_timestamp = filemtime($foto_path_check);
                $foto_url_box = $base_url . 'system/uploads/foto/odc/' . $first_odc['foto'] . '?v=' . $foto_timestamp;
            }
        }
        
        $network_data['odcs'][] = [
            'id' => $first_odc['id'], // ID ODC pertama untuk referensi
            'box_id' => $first_odc['box_id'] ?: 'BOX-' . $first_odc['id'],
            'name' => $first_odc['box_id'] ?: $first_odc['name'],
            'type' => 'odc',
            'coordinates' => $coordinates,
            'address' => $first_odc['address'],
            'description' => $first_odc['description'],
            'total_slots' => $total_slots,
            'grand_total_ports' => $grand_total_ports,
            'grand_used_ports' => $grand_used_ports,
            'grand_available_ports' => $grand_available,
            'slots' => $slots_data,
            'single_slot_status' => $single_slot_status,
            'foto' => $first_odc['foto'],
            'foto_url' => $foto_url_box,
            'popup_content' => $popup_html
        ];
    }
    
    // Simpan juga data ODC individual untuk referensi (dipakai di dropdown parent, dll)
    $network_data['odcs_all'] = [];
    foreach ($odcs as $odc) {
        $available_ports = $odc['total_ports'] - $odc['used_ports'];
        
        $parent_olt_name = null;
        if (!empty($odc['parent_olt_id'])) {
            $parent_olt = ORM::for_table('tbl_olt')->where('id', $odc['parent_olt_id'])->find_one();
            if ($parent_olt) $parent_olt_name = $parent_olt->name;
        }
        
        $parent_odc_name = null;
        if (!empty($odc['parent_odc_id'])) {
            $parent_odc_obj = ORM::for_table('tbl_odc')->where('id', $odc['parent_odc_id'])->find_one();
            if ($parent_odc_obj) $parent_odc_name = $parent_odc_obj->name;
        }
        
        $ports_data = [];
        if (!empty($odc['ports_data'])) {
            $ports_data = json_decode($odc['ports_data'], true) ?: [];
        }
        
        // Generate foto URL dengan cache busting
        $foto_url_odc = '';
        if (!empty($odc['foto'])) {
            $foto_path_check = 'system/uploads/foto/odc/' . $odc['foto'];
            if (file_exists($foto_path_check)) {
                $base_url = str_replace('?_route=', '', U);
                $base_url = rtrim($base_url, '/') . '/';
                $foto_timestamp = filemtime($foto_path_check);
                $foto_url_odc = $base_url . 'system/uploads/foto/odc/' . $odc['foto'] . '?v=' . $foto_timestamp;
            }
        }
        
        $network_data['odcs_all'][] = [
            'id' => $odc['id'],
            'name' => $odc['name'],
            'box_id' => $odc['box_id'],
            'slot_number' => $odc['slot_number'] ?? 1,
            'type' => 'odc',
            'coordinates' => $odc['coordinates'],
            'description' => $odc['description'],
            'address' => $odc['address'],
            'total_ports' => $odc['total_ports'],
            'used_ports' => $odc['used_ports'],
            'available_ports' => $available_ports,
            'status' => $odc['status'],
            'parent_type' => $odc['parent_type'],
            'parent_olt_id' => $odc['parent_olt_id'],
            'parent_olt_name' => $parent_olt_name,
            'parent_odc_id' => $odc['parent_odc_id'],
            'parent_odc_name' => $parent_odc_name,
            'ports_data' => $ports_data,
            'foto' => $odc['foto'],
            'foto_url' => $foto_url_odc
        ];
    }

    // Format ODPs data dengan Multi-Slot Support
    // Step 1: Group ODP by coordinates untuk multi-slot
    $odp_by_coordinates = [];
    foreach ($odps as $odp) {
        $coord_key = $odp['coordinates'];
        if (!isset($odp_by_coordinates[$coord_key])) {
            $odp_by_coordinates[$coord_key] = [];
        }
        $odp_by_coordinates[$coord_key][] = $odp;
    }
    
    // Step 2: Process setiap grup koordinat
    foreach ($odp_by_coordinates as $coordinates => $odp_group) {
        // Hitung total dari semua slot di koordinat ini
        $total_slots = count($odp_group);
        $grand_total_ports = 0;
        $grand_used_ports = 0;
        $slots_data = [];
        $first_odp = $odp_group[0]; // Untuk data address, description box
        
        // Build data untuk setiap slot
        foreach ($odp_group as $slot_index => $odp) {
            $available_ports = $odp['total_ports'] - $odp['used_ports'];
            $grand_total_ports += $odp['total_ports'];
            $grand_used_ports += $odp['used_ports'];
            
            // Get parent ODC info
            $parent_odc_name = null;
            if (!empty($odp['parent_odc_id'])) {
                $parent_odc = ORM::for_table('tbl_odc')
                    ->where('id', $odp['parent_odc_id'])
                    ->find_one();
                if ($parent_odc) {
                    $parent_odc_name = $parent_odc->name;
                }
            }
            
            // Get parent ODP info
            $parent_odp_name = null;
            if (!empty($odp['parent_odp_id'])) {
                $parent_odp_obj = ORM::for_table('tbl_odp')
                    ->where('id', $odp['parent_odp_id'])
                    ->find_one();
                if ($parent_odp_obj) {
                    $parent_odp_name = $parent_odp_obj->name;
                }
            }
            
            // Parse ports_data JSON
            $ports_data = [];
            if (!empty($odp['ports_data'])) {
                $ports_data = json_decode($odp['ports_data'], true) ?: [];
            }
            
            // Build port labels string
            $port_labels_arr = [];
            foreach ($ports_data as $port_item) {
                if (!empty($port_item['label'])) {
                    $port_labels_arr[] = $port_item['label'];
                }
            }
            $port_labels_text = !empty($port_labels_arr) ? implode(', ', $port_labels_arr) : '-';
            
            // Generate foto URL untuk slot ini
            $slot_foto_url = '';
            if (!empty($odp['foto'])) {
                $foto_path_slot = 'system/uploads/foto/odp/' . $odp['foto'];
                if (file_exists($foto_path_slot)) {
                    $base_url_slot = str_replace('?_route=', '', U);
                    $base_url_slot = rtrim($base_url_slot, '/') . '/';
                    $foto_ts = filemtime($foto_path_slot);
                    $slot_foto_url = $base_url_slot . 'system/uploads/foto/odp/' . $odp['foto'] . '?v=' . $foto_ts;
                }
            }
            
            $slots_data[] = [
                'id' => $odp['id'],
                'name' => $odp['name'],
                'slot_number' => $odp['slot_number'] ?? ($slot_index + 1),
                'total_ports' => $odp['total_ports'],
                'used_ports' => $odp['used_ports'],
                'available_ports' => $available_ports,
                'status' => $odp['status'],
                'parent_type' => $odp['parent_type'],
                'parent_odc_id' => $odp['parent_odc_id'],
                'parent_odc_name' => $parent_odc_name,
                'parent_odp_id' => $odp['parent_odp_id'],
                'parent_odp_name' => $parent_odp_name,
                'ports_data' => $ports_data,
                'port_labels_text' => $port_labels_text,
                'description' => $odp['description'],
                'box_id' => $odp['box_id'] ?? null,
                'foto' => $odp['foto'],
                'foto_url' => $slot_foto_url
            ];
        }
        
        // Sort slots by slot_number
        usort($slots_data, function($a, $b) {
            return ($a['slot_number'] ?? 1) - ($b['slot_number'] ?? 1);
        });
        
        // Build popup content untuk multi-slot
        $grand_available = $grand_total_ports - $grand_used_ports;
        
        // Header popup
        $popup_html = "<div class='popup-header popup-header-odp'>" .
            "<i class='fa fa-circle-o'></i> <span class='popup-header-text'>ODP - " . ($first_odp['box_id'] ?: $first_odp['name']) . "</span>" .
        "</div>";
        
        $popup_html .= "<div class='popup-body'>";
        
        // Cek apakah foto ODP ada (ambil dari slot pertama)
        if (!empty($first_odp['foto'])) {
            $foto_path = 'system/uploads/foto/odp/' . $first_odp['foto'];
            if (file_exists($foto_path)) {
                $base_url = str_replace('?_route=', '', U);
                $base_url = rtrim($base_url, '/') . '/';
                $foto_timestamp = filemtime($foto_path);
                $foto_url = $base_url . 'system/uploads/foto/odp/' . $first_odp['foto'] . '?v=' . $foto_timestamp;
                $popup_html .= "<div class='popup-foto-container'><img src='" . $foto_url . "' alt='Foto ODP' class='popup-foto' onclick='lihatFotoFullscreen(\"" . $foto_url . "\", \"" . addslashes($first_odp['box_id'] ?: $first_odp['name']) . "\", \"odp\")'></div>";
            }
        }
        
        // Info lokasi
        if ($first_odp['address']) {
            $popup_html .= "<div class='popup-info-row'><i class='fa fa-map-marker'></i> <span class='popup-text-wrap'>" . $first_odp['address'] . "</span></div>";
        }
        if ($first_odp['description']) {
            $popup_html .= "<div class='popup-info-row'><i class='fa fa-pencil'></i> <span class='popup-text-wrap'>" . $first_odp['description'] . "</span></div>";
        }
        
        // Summary
        $popup_html .= "<div class='popup-info-row'><i class='fa fa-th-large'></i> <span><strong>" . $total_slots . " Slot ODP</strong></span></div>";
        $popup_html .= "<div class='popup-info-row'><i class='fa fa-sitemap'></i> <span>Total: " . $grand_used_ports . "/" . $grand_total_ports . " port (<strong>" . $grand_available . " " . $lang['text_available'] . "</strong>)</span></div>";
        
        // Divider
        $popup_html .= "<hr style='margin: 10px 0; border-color: #e5e7eb;'>";
        
        // Detail setiap slot
        foreach ($slots_data as $slot) {
            $slot_status_class = 'status-active';
            $slot_status_text = $lang['status_online'];
            $slot_title_color = '#f59e0b';
            if ($slot['status'] === 'Maintenance') {
                $slot_status_class = 'status-maintenance';
                $slot_status_text = $lang['status_maintenance'];
                $slot_title_color = '#eab308';
            } elseif ($slot['status'] === 'Inactive') {
                $slot_status_class = 'status-inactive';
                $slot_status_text = $lang['status_offline'];
                $slot_title_color = '#ef4444';
            }
            
            $popup_html .= "<div class='popup-slot-item' style='background: #fffbeb; padding: 8px; border-radius: 6px; margin-bottom: 8px;'>";
            $popup_html .= "<div style='font-weight: 600; color: " . $slot_title_color . "; margin-bottom: 4px;'><i class='fa fa-folder-open'></i> Slot " . $slot['slot_number'] . ": " . $slot['name'] . " <span class='status-badge " . $slot_status_class . "' style='font-size: 10px; padding: 2px 6px;'>" . $slot_status_text . "</span></div>";
            $popup_html .= "<div style='font-size: 12px; color: #64748b;'>";
            $popup_html .= "<i class='fa fa-sitemap'></i> Port: " . $slot['used_ports'] . "/" . $slot['total_ports'] . " " . $lang['popup_port_used'];
            if ($slot['port_labels_text'] !== '-') {
                $popup_html .= "<br><i class='fa fa-tags'></i> Labels: " . $slot['port_labels_text'];
            }
            $popup_html .= "</div>";
            $popup_html .= "</div>";
        }
        
        $popup_html .= "</div>"; // end popup-body
        
        // Footer dengan tombol
        $popup_html .= "<div class='popup-footer'>";
        $popup_html .= "<button class='popup-btn popup-btn-odp' onclick='tampilkanDetailOdpBox(\"" . $coordinates . "\")'><i class='fa fa-eye'></i> " . $lang['js_detail'] . "</button>";
        $popup_html .= "<button class='popup-btn popup-btn-warning' onclick='tambahSlotOdp(\"" . $coordinates . "\", \"" . addslashes($first_odp['address']) . "\")'><i class='fa fa-plus'></i> Slot</button>";
        $popup_html .= "<a href='https://www.google.com/maps/dir//" . $coordinates . "' target='_blank' class='popup-btn popup-btn-secondary'><i class='fa fa-location-arrow'></i> " . $lang['popup_route'] . "</a>";
        $popup_html .= "</div>";
        
        // Simpan ke network_data (1 entry per koordinat)
        // Untuk single slot, ambil status dari slot pertama untuk warna marker
        $single_slot_status = ($total_slots == 1) ? $first_odp['status'] : 'Active';
        
        // Generate foto URL untuk Box ODP (dari slot pertama)
        $foto_url_box = '';
        if (!empty($first_odp['foto'])) {
            $foto_path_check = 'system/uploads/foto/odp/' . $first_odp['foto'];
            if (file_exists($foto_path_check)) {
                $base_url = str_replace('?_route=', '', U);
                $base_url = rtrim($base_url, '/') . '/';
                $foto_timestamp = filemtime($foto_path_check);
                $foto_url_box = $base_url . 'system/uploads/foto/odp/' . $first_odp['foto'] . '?v=' . $foto_timestamp;
            }
        }
        
        $network_data['odps'][] = [
            'id' => $first_odp['id'],
            'box_id' => $first_odp['box_id'] ?: 'BOX-' . $first_odp['id'],
            'name' => $first_odp['box_id'] ?: $first_odp['name'],
            'type' => 'odp',
            'coordinates' => $coordinates,
            'address' => $first_odp['address'],
            'description' => $first_odp['description'],
            'total_slots' => $total_slots,
            'grand_total_ports' => $grand_total_ports,
            'grand_used_ports' => $grand_used_ports,
            'grand_available_ports' => $grand_available,
            'slots' => $slots_data,
            'single_slot_status' => $single_slot_status,
            'foto' => $first_odp['foto'],
            'foto_url' => $foto_url_box,
            'popup_content' => $popup_html
        ];
    }
    
    // Simpan juga data ODP individual untuk referensi (dipakai di dropdown parent, dll)
    $network_data['odps_all'] = [];
    foreach ($odps as $odp) {
        $available_ports = $odp['total_ports'] - $odp['used_ports'];
        
        $parent_odc_name = null;
        if (!empty($odp['parent_odc_id'])) {
            $parent_odc = ORM::for_table('tbl_odc')->where('id', $odp['parent_odc_id'])->find_one();
            if ($parent_odc) $parent_odc_name = $parent_odc->name;
        }
        
        $parent_odp_name = null;
        if (!empty($odp['parent_odp_id'])) {
            $parent_odp_obj = ORM::for_table('tbl_odp')->where('id', $odp['parent_odp_id'])->find_one();
            if ($parent_odp_obj) $parent_odp_name = $parent_odp_obj->name;
        }
        
        $ports_data = [];
        if (!empty($odp['ports_data'])) {
            $ports_data = json_decode($odp['ports_data'], true) ?: [];
        }
        
        // Generate foto URL dengan cache busting
        $foto_url_odp = '';
        if (!empty($odp['foto'])) {
            $foto_path_check = 'system/uploads/foto/odp/' . $odp['foto'];
            if (file_exists($foto_path_check)) {
                $base_url = str_replace('?_route=', '', U);
                $base_url = rtrim($base_url, '/') . '/';
                $foto_timestamp = filemtime($foto_path_check);
                $foto_url_odp = $base_url . 'system/uploads/foto/odp/' . $odp['foto'] . '?v=' . $foto_timestamp;
            }
        }
        
        $network_data['odps_all'][] = [
            'id' => $odp['id'],
            'name' => $odp['name'],
            'box_id' => $odp['box_id'] ?? null,
            'slot_number' => $odp['slot_number'] ?? 1,
            'type' => 'odp',
            'coordinates' => $odp['coordinates'],
            'description' => $odp['description'],
            'address' => $odp['address'],
            'total_ports' => $odp['total_ports'],
            'used_ports' => $odp['used_ports'],
            'available_ports' => $available_ports,
            'status' => $odp['status'],
            'parent_type' => $odp['parent_type'],
            'parent_odc_id' => $odp['parent_odc_id'],
            'parent_odc_name' => $parent_odc_name,
            'parent_odp_id' => $odp['parent_odp_id'],
            'parent_odp_name' => $parent_odp_name,
            'ports_data' => $ports_data,
            'foto' => $odp['foto'],
            'foto_url' => $foto_url_odp
        ];
    }

    // Format customers data
    foreach ($customers as $customer) {
        // SKIP customers with invalid coordinate format (malformed string)
        // Prevents map errors when rendering markers
        $c_coord = $customer['coordinates'] ?? '';
        $c_parts = explode(',', $c_coord);
        if (count($c_parts) !== 2) continue; // missing or extra comma
        $c_lat = trim($c_parts[0]);
        $c_lng = trim($c_parts[1]);
        if ($c_lat === '' || $c_lng === '' || !is_numeric($c_lat) || !is_numeric($c_lng)) continue;
        $c_latf = floatval($c_lat);
        $c_lngf = floatval($c_lng);
        if ($c_latf < -90 || $c_latf > 90 || $c_lngf < -180 || $c_lngf > 180) continue;

        // Get ODP name jika ada
        $odp_name = '';
        if (!empty($customer['odp_id'])) {
            $odp = ORM::for_table('tbl_odp')->find_one($customer['odp_id']);
            if ($odp) {
                $odp_name = $odp->name;
            }
        }
        
        // Get nama paket dari tbl_user_recharges
        $nama_paket = '';
        $recharge = ORM::for_table('tbl_user_recharges')
            ->where('customer_id', $customer['id'])
            ->where('status', 'on')
            ->order_by_desc('id')
            ->find_one();
        if ($recharge) {
            $nama_paket = $recharge->namebp;
        }
        
        // Get RX Power dari tbl_acs_devices berdasarkan pppoe_username atau username di tags
        $rx_power = '';
        $pppoe_user = $customer['pppoe_username'] ?: $customer['username'];
        $username = $customer['username'];
        if (file_exists(__DIR__ . '/genieacs_manager.php')) {
            if (!empty($pppoe_user)) {
                $acs_devices = ORM::for_table('tbl_acs_devices')
                    ->where_not_equal('device_data', '')
                    ->find_array();
                
                foreach ($acs_devices as $device) {
                    if (!empty($device['device_data'])) {
                        $device_data = json_decode($device['device_data'], true);
                        if ($device_data && isset($device_data['tags'])) {
                            // Tags bisa berupa string "egik" atau sudah di-decode dari array
                            $tags = $device_data['tags'];
                            
                            // Coba match dengan pppoe_username dulu
                            if (is_string($tags) && strtolower($tags) === strtolower($pppoe_user)) {
                                $rx_power = isset($device_data['rx_power']) ? $device_data['rx_power'] : '';
                                break;
                            }
                            
                            // Kalau tidak cocok, coba match dengan username
                            if (is_string($tags) && strtolower($tags) === strtolower($username)) {
                                $rx_power = isset($device_data['rx_power']) ? $device_data['rx_power'] : '';
                                break;
                            }
                        }
                    }
                }
            }
        } else {
            $rx_power = 'LOCKED';
        }

        // Fallback: if GenieACS gave no RX (empty or LOCKED), use the OLT
        // Monitor plugin's per-customer RX (written by its scan/cron). Only
        // customers whose ONU MAC is set in the OLT plugin will have a row.
        if ($rx_power === '' || $rx_power === 'LOCKED') {
            try {
                $st = ORM::get_db()->prepare(
                    "SELECT rx_power FROM tbl_olt_customer_rx WHERE customer_id = ? LIMIT 1"
                );
                $st->execute([$customer['id']]);
                $olt_rx = $st->fetchColumn();
                if ($olt_rx !== false && $olt_rx !== null && $olt_rx !== '') {
                    $rx_power = $olt_rx;
                }
            } catch (Exception $e) {
                // table may not exist yet (OLT plugin not installed) — ignore
            }
        }
        
        // Get status dari tbl_customer_online_status (dari cron)
        $online_status = 'offline'; // default
        $current_ip = '';
        $status_record = ORM::for_table('tbl_customer_online_status')
            ->where('customer_id', $customer['id'])
            ->find_one();
        if ($status_record) {
            $online_status = $status_record['status']; // online, isolir, offline, off_isolir
            $current_ip = $status_record['current_ip'] ?: '';
        }
        
        // Generate foto lokasi URL dengan cache busting
        $foto_lokasi_url = '';
        if (!empty($customer['foto_lokasi'])) {
            $foto_path = 'system/uploads/foto/customers/' . $customer['foto_lokasi'];
            if (file_exists($foto_path)) {
                $base_url = str_replace('?_route=', '', U);
                $base_url = rtrim($base_url, '/') . '/';
                $foto_timestamp = filemtime($foto_path);
                $foto_lokasi_url = $base_url . 'system/uploads/foto/customers/' . $customer['foto_lokasi'] . '?v=' . $foto_timestamp;
            }
        }
        
        $network_data['customers'][] = [
            'id' => $customer['id'],
            'name' => $customer['fullname'],
            'type' => 'customer',
            'coordinates' => $customer['coordinates'],
            'username' => $customer['username'],
            'pppoe_username' => $customer['pppoe_username'] ?: $customer['username'],
            'pppoe_ip' => $customer['pppoe_ip'] ?: '',
            'current_ip' => $current_ip,
            'balance' => $customer['balance'],
            'balance_formatted' => $currency_code . ' ' . number_format($customer['balance'], 0, ',', '.'),
            'address' => $customer['address'] ?: '',
            'phonenumber' => $customer['phonenumber'] ?: '',
            'odp_id' => $customer['odp_id'] ?: '',
            'odp_name' => $odp_name,
            'nama_paket' => $nama_paket,
            'rx_power' => $rx_power,
            'online_status' => $online_status,
            'foto_lokasi' => $customer['foto_lokasi'] ?: '',
            'foto_lokasi_url' => $foto_lokasi_url,
            'detail_url' => U . 'customers/view/' . $customer['id'],
            'maps_url' => 'https://www.google.com/maps/dir//' . $customer['coordinates']
        ];
    }

    // Get all Tiang with coordinates
    $tiang_query = ORM::for_table('tbl_tiang')
        ->where_not_equal('coordinates', '')
        ->order_by_asc('name');
    
    if (!empty($search)) {
        $tiang_query->where_like('name', '%' . $search . '%');
    }
    
    $tiang_list = $tiang_query->find_array();

    // Format Tiang data
    foreach ($tiang_list as $tiang) {
        // Status badge
        $status_class = 'status-active';
        $status_text = $lang['status_good'];
        if ($tiang['status'] === 'Perbaikan') {
            $status_class = 'status-maintenance';
            $status_text = $lang['status_maintenance'];
        } elseif ($tiang['status'] === 'Rusak') {
            $status_class = 'status-inactive';
            $status_text = $lang['status_broken'];
        }
        
        // Translate tipe tiang
        $tipe_display_map = [
            'Besi' => $lang['material_steel'],
            'Beton' => $lang['material_concrete'],
            'Kayu' => $lang['material_wood'],
        ];
        $tipe_display = isset($tipe_display_map[$tiang['tipe']]) ? $tipe_display_map[$tiang['tipe']] : $tiang['tipe'];
        
        // Format tinggi tiang (smart format)
        $tinggi_text = network_mapping_format_meter($tiang['tinggi']);
        
        // Format slack kabel (smart format)
        $slack_text = network_mapping_format_meter($tiang['slack_kabel']);
        
        // Cek apakah foto tiang ada
        $foto_tiang_html = '';
        if (!empty($tiang['foto'])) {
            $foto_path = 'system/uploads/foto/tiang/' . $tiang['foto'];
            if (file_exists($foto_path)) {
                // Gunakan path langsung tanpa routing
                $base_url = str_replace('?_route=', '', U);
                $base_url = rtrim($base_url, '/') . '/';
                // Tambahkan timestamp untuk cache busting
                $foto_timestamp = filemtime($foto_path);
                $foto_url = $base_url . 'system/uploads/foto/tiang/' . $tiang['foto'] . '?v=' . $foto_timestamp;
                $foto_tiang_html = "<div class='popup-foto-container'><img src='" . $foto_url . "' alt='Foto " . $tiang['name'] . "' class='popup-foto' onclick='lihatFotoFullscreen(\"" . $foto_url . "\", \"" . addslashes($tiang['name']) . "\", \"tiang\")'></div>";
            }
        }
        
        // Generate foto URL dengan cache busting
        $foto_url_data = '';
        if (!empty($tiang['foto'])) {
            $foto_path_check = 'system/uploads/foto/tiang/' . $tiang['foto'];
            if (file_exists($foto_path_check)) {
                $base_url = str_replace('?_route=', '', U);
                $base_url = rtrim($base_url, '/') . '/';
                $foto_timestamp = filemtime($foto_path_check);
                $foto_url_data = $base_url . 'system/uploads/foto/tiang/' . $tiang['foto'] . '?v=' . $foto_timestamp;
            }
        }
        
        $network_data['tiang'][] = [
            'id' => $tiang['id'],
            'name' => $tiang['name'],
            'type' => 'tiang',
            'coordinates' => $tiang['coordinates'],
            'panjang' => $tiang['tinggi'],
            'bahan' => $tiang['tipe'],
            'slack_kabel' => $tiang['slack_kabel'],
            'status' => $tiang['status'],
            'address' => $tiang['address'],
            'description' => $tiang['description'],
            'foto' => $tiang['foto'],
            'foto_url' => $foto_url_data,
            'popup_content' => 
                "<div class='popup-header popup-header-tiang'>" .
                    "<i class='fa fa-minus'></i> <span class='popup-header-text'>" . $tiang['name'] . "</span>" .
                "</div>" .
                "<div class='popup-body'>" .
                    $foto_tiang_html .
                    "<div class='popup-info-row'><i class='fa fa-info-circle'></i> <span class='status-badge " . $status_class . "'>" . $status_text . "</span></div>" .
                    "<div class='popup-info-row'><i class='fa fa-arrows-v'></i> <span>" . $lang['popup_height'] . ": <strong>" . $tinggi_text . "</strong></span></div>" .
                    ($tiang['tipe'] ? "<div class='popup-info-row'><i class='fa fa-cube'></i> <span>" . $lang['popup_type'] . ": <strong>" . $tipe_display . "</strong></span></div>" : "") .
                    "<div class='popup-info-row'><i class='fa fa-random'></i> <span>" . $lang['popup_slack_cable'] . ": <strong>" . $slack_text . "</strong></span></div>" .
                    ($tiang['address'] ? "<div class='popup-info-row'><i class='fa fa-map-marker'></i> <span class='popup-text-wrap'>" . $tiang['address'] . "</span></div>" : "") .
                    ($tiang['description'] ? "<div class='popup-info-row'><i class='fa fa-pencil'></i> <span class='popup-text-wrap'>" . $tiang['description'] . "</span></div>" : "") .
                "</div>" .
                "<div class='popup-footer'>" .
                    "<button class='popup-btn popup-btn-tiang' onclick='tampilkanDetailTiang(" . $tiang['id'] . ")'><i class='fa fa-eye'></i> " . $lang['js_detail'] . "</button>" .
                    "<a href='https://www.google.com/maps/dir//" . $tiang['coordinates'] . "' target='_blank' class='popup-btn popup-btn-secondary'><i class='fa fa-location-arrow'></i> " . $lang['popup_route'] . "</a>" .
                "</div>"
        ];
    }

    // Get all Kabel
    $kabel_list = ORM::for_table('tbl_kabel')
        ->order_by_desc('id')
        ->find_array();

    // Format Kabel data
    foreach ($kabel_list as $kabel) {
        // Get device 1 name
        $device_1_name = network_mapping_get_device_name($kabel['device_1_type'], $kabel['device_1_id']);
        
        // Get device 2 name
        $device_2_name = network_mapping_get_device_name($kabel['device_2_type'], $kabel['device_2_id']);
        
        // Determine cable color based on connection type
        $cable_color = network_mapping_get_cable_color($kabel['device_1_type'], $kabel['device_2_type']);
        
        // Parse coordinates path
        $coordinates_path = json_decode($kabel['coordinates_path'], true) ?: [];
        
        // Parse sambungan data
        $sambungan_data = json_decode($kabel['sambungan_data'], true) ?: [];
        
        // Generate foto URL untuk setiap sambungan
        $upload_dir = 'system/uploads/foto/sambungan/';
        $base_url = str_replace('?_route=', '', U);
        $base_url = rtrim($base_url, '/') . '/';
        
        foreach ($sambungan_data as $idx => &$sambungan) {
            $sambungan['foto'] = null;
            $sambungan['foto_url'] = null;
            
            // Cari file foto dengan pattern kabel_{id}_sambungan_{index}.*
            $foto_pattern = $upload_dir . 'kabel_' . $kabel['id'] . '_sambungan_' . ($idx + 1) . '.*';
            $files = glob($foto_pattern);
            
            if (!empty($files) && file_exists($files[0])) {
                $foto_filename = basename($files[0]);
                $sambungan['foto'] = $foto_filename;
                $foto_timestamp = filemtime($files[0]);
                $sambungan['foto_url'] = $base_url . $upload_dir . $foto_filename . '?v=' . $foto_timestamp;
            }
        }
        unset($sambungan); // Break reference
        
        // Hitung jumlah checkpoint (total koordinat - 2 untuk start dan end device)
        $checkpoint_count = count($coordinates_path) > 2 ? count($coordinates_path) - 2 : 0;
        
        // Build sambungan detail HTML untuk popup
        $sambungan_detail_html = '';
        if ($kabel['jumlah_sambungan'] > 0 && count($sambungan_data) > 0) {
            $sambungan_detail_html = "<div class='popup-sambungan-section'>";
            $sambungan_detail_html .= "<div class='popup-sambungan-header'><i class='fa fa-link'></i> " . $lang['popup_connection_detail'] . " (" . $kabel['jumlah_sambungan'] . "x)</div>";
            $sambungan_detail_html .= "<div class='popup-sambungan-list'>";
            
            foreach ($sambungan_data as $idx => $sambungan) {
                $titik_label = '';
                if ($sambungan['titik'] === 'device_1') {
                    $titik_label = ucfirst($kabel['device_1_type']) . ': ' . $device_1_name;
                } else {
                    $titik_label = ucfirst($kabel['device_2_type']) . ': ' . $device_2_name;
                }
                
                $keterangan_html = '';
                if (!empty($sambungan['keterangan'])) {
                    $keterangan_html = " <em class='sambungan-keterangan'>(" . htmlspecialchars($sambungan['keterangan']) . ")</em>";
                }
                
                $sambungan_detail_html .= "<div class='popup-sambungan-item popup-sambungan-item-clickable' onclick='goToSambungan(" . $kabel['id'] . ", " . ($idx + 1) . ")' title='Klik untuk ke lokasi sambungan'>";
                $sambungan_detail_html .= "<span class='sambungan-number'>#" . ($idx + 1) . "</span>";
                $sambungan_detail_html .= "<span class='sambungan-info'>" . network_mapping_format_meter($sambungan['jarak']) . " " . $lang['js_from'] . " " . $titik_label . $keterangan_html . "</span>";
                $sambungan_detail_html .= "<span class='sambungan-goto'><i class='fa fa-chevron-right'></i></span>";
                $sambungan_detail_html .= "</div>";
            }
            
            $sambungan_detail_html .= "</div></div>";
        }
        
        $network_data['kabel'][] = [
            'id' => $kabel['id'],
            'name' => $kabel['name'] ?: 'Kabel #' . $kabel['id'],
            'type' => 'kabel',
            'device_1_type' => $kabel['device_1_type'],
            'device_1_id' => $kabel['device_1_id'],
            'device_1_name' => $device_1_name,
            'device_2_type' => $kabel['device_2_type'],
            'device_2_id' => $kabel['device_2_id'],
            'device_2_name' => $device_2_name,
            'coordinates_path' => $coordinates_path,
            'panjang' => $kabel['panjang'],
            'jumlah_sambungan' => $kabel['jumlah_sambungan'],
            'sambungan_data' => $sambungan_data,
            'description' => $kabel['description'],
            'cable_color' => $cable_color,
            'popup_content' => 
                "<div class='popup-header popup-header-kabel'>" .
                    "<i class='fa fa-minus'></i> <span class='popup-header-text'>" . ($kabel['name'] ?: 'Kabel #' . $kabel['id']) . "</span>" .
                "</div>" .
                "<div class='popup-body'>" .
                    "<div class='popup-info-row'><i class='fa fa-sign-out'></i> <span>" . $lang['popup_from'] . ": <strong>" . ucfirst($kabel['device_1_type']) . "</strong> - " . $device_1_name . "</span></div>" .
                    "<div class='popup-info-row'><i class='fa fa-sign-in'></i> <span>" . $lang['popup_to'] . ": <strong>" . ucfirst($kabel['device_2_type']) . "</strong> - " . $device_2_name . "</span></div>" .
                    "<div class='popup-info-row'><i class='fa fa-arrows-h'></i> <span>" . $lang['popup_length'] . ": <strong>" . network_mapping_format_meter($kabel['panjang']) . "</strong></span></div>" .
                    "<div class='popup-info-row'><i class='fa fa-map-marker'></i> <span>Checkpoint: <strong>" . $checkpoint_count . " " . $lang['popup_points'] . "</strong></span></div>" .
                    ($kabel['jumlah_sambungan'] > 0 ? "<div class='popup-info-row'><i class='fa fa-link'></i> <span>" . $lang['popup_connection'] . ": <strong>" . $kabel['jumlah_sambungan'] . "x</strong></span></div>" : "") .
                    $sambungan_detail_html .
                    ($kabel['description'] ? "<div class='popup-info-row'><i class='fa fa-info-circle'></i> <span class='popup-text-wrap'>" . $kabel['description'] . "</span></div>" : "") .
                "</div>" .
                "<div class='popup-footer'>" .
                    "<button class='popup-btn popup-btn-kabel' onclick='tampilkanDetailKabel(" . $kabel['id'] . ")'><i class='fa fa-eye'></i> " . $lang['js_detail'] . "</button>" .
                    "<button class='popup-btn popup-btn-danger' onclick='hapusKabelDariMap(" . $kabel['id'] . ")'><i class='fa fa-trash'></i> " . $lang['btn_delete'] . "</button>" .
                "</div>"
        ];
    }

    // Get all Homepass with coordinates
    $homepass_query = ORM::for_table('tbl_homepass')
        ->where_not_equal('coordinates', '')
        ->order_by_asc('name');
    
    if (!empty($search)) {
        $homepass_query->where_raw("(name LIKE '%$search%' OR address LIKE '%$search%' OR phone LIKE '%$search%')");
    }
    
    $homepass_list = $homepass_query->find_array();

    // Format Homepass data
    foreach ($homepass_list as $hp) {
        // Status badge & color
        $status_class = 'status-prospek';
        $status_color = '#3b82f6'; // Biru - Prospek
        switch ($hp['status']) {
            case 'Prospek':
                $status_class = 'status-prospek';
                $status_color = '#3b82f6'; // Biru
                break;
            case 'Bermasalah':
                $status_class = 'status-bermasalah';
                $status_color = '#ef4444'; // Merah
                break;
            case 'Tidak Minat':
                $status_class = 'status-tidak-minat';
                $status_color = '#f97316'; // Orange
                break;
            case 'Rumah Kosong':
                $status_class = 'status-rumah-kosong';
                $status_color = '#6b7280'; // Abu-abu
                break;
            case 'Sudah Langganan':
                $status_class = 'status-sudah-langganan';
                $status_color = '#10b981'; // Hijau
                break;
            case 'Pending':
                $status_class = 'status-pending';
                $status_color = '#eab308'; // Kuning
                break;
        }
        
        // Translate status homepass
        $status_display_map = [
            'Prospek' => $lang['homepass_status_prospek'],
            'Bermasalah' => $lang['homepass_status_bermasalah'],
            'Tidak Minat' => $lang['homepass_status_tidak_minat'],
            'Rumah Kosong' => $lang['homepass_status_rumah_kosong'],
            'Sudah Langganan' => $lang['homepass_status_sudah_langganan'],
            'Pending' => $lang['homepass_status_pending'],
        ];
        $status_display = isset($status_display_map[$hp['status']]) ? $status_display_map[$hp['status']] : $hp['status'];
        
        // Translate kategori homepass
        $kategori_display_map = [
            'Rumah' => $lang['homepass_kat_rumah'],
            'Ruko' => $lang['homepass_kat_ruko'],
            'Apartemen' => $lang['homepass_kat_apartemen'],
            'Kantor' => $lang['homepass_kat_kantor'],
            'Lainnya' => $lang['homepass_kat_lainnya'],
        ];
        $kategori_display = isset($kategori_display_map[$hp['kategori']]) ? $kategori_display_map[$hp['kategori']] : $hp['kategori'];
        
        // Format tanggal survey
        $tanggal_text = $hp['tanggal_survey'] ? date('d/m/Y', strtotime($hp['tanggal_survey'])) : '-';
        
        // Generate foto URL dengan cache busting
        $foto_url_hp = '';
        $foto_html = '';
        if (!empty($hp['foto'])) {
            $foto_path_hp = 'system/uploads/foto/homepass/' . $hp['foto'];
            if (file_exists($foto_path_hp)) {
                $base_url = str_replace('?_route=', '', U);
                $base_url = rtrim($base_url, '/') . '/';
                $foto_timestamp = filemtime($foto_path_hp);
                $foto_url_hp = $base_url . 'system/uploads/foto/homepass/' . $hp['foto'] . '?v=' . $foto_timestamp;
                $foto_html = "<div class='popup-foto-container'><img src='" . $foto_url_hp . "' alt='Foto Homepass' class='popup-foto' onclick='lihatFotoFullscreen(\"" . $foto_url_hp . "\", \"" . addslashes($hp['name'] ?: 'Homepass') . "\", \"homepass\")'></div>";
            }
        }
        
        $network_data['homepass'][] = [
            'id' => $hp['id'],
            'name' => $hp['name'] ?: '-',
            'type' => 'homepass',
            'coordinates' => $hp['coordinates'],
            'address' => $hp['address'],
            'phone' => $hp['phone'],
            'status' => $hp['status'],
            'status_color' => $status_color,
            'kategori' => $hp['kategori'],
            'catatan' => $hp['catatan'],
            'surveyor' => $hp['surveyor'],
            'tanggal_survey' => $hp['tanggal_survey'],
            'customer_id' => $hp['customer_id'],
            'foto' => $hp['foto'],
            'foto_url' => $foto_url_hp,
            'popup_content' => 
                "<div class='popup-header popup-header-homepass' style='background: linear-gradient(135deg, " . $status_color . " 0%, " . $status_color . "dd 100%);'>" .
                    "<i class='fa fa-home'></i> <span class='popup-header-text'>" . ($hp['name'] ?: '-') . "</span>" .
                "</div>" .
                "<div class='popup-body'>" .
                    $foto_html .
                    "<div class='popup-info-row'><i class='fa fa-tag'></i> <span class='status-badge " . $status_class . "'>" . $status_display . "</span></div>" .
                    "<div class='popup-info-row'><i class='fa fa-building'></i> <span>" . $kategori_display . "</span></div>" .
                    "<div class='popup-info-row'><i class='fa fa-phone'></i> <span>" . ($hp['phone'] ?: '-') . "</span></div>" .
                    ($hp['address'] ? "<div class='popup-info-row'><i class='fa fa-map-marker'></i> <span class='popup-text-wrap'>" . $hp['address'] . "</span></div>" : "") .
                    "<div class='popup-info-row'><i class='fa fa-calendar'></i> <span>" . $lang['popup_survey'] . ": " . $tanggal_text . "</span></div>" .
                    ($hp['surveyor'] ? "<div class='popup-info-row'><i class='fa fa-user'></i> <span>" . $lang['popup_surveyor'] . ": " . $hp['surveyor'] . "</span></div>" : "") .
                    ($hp['catatan'] ? "<div class='popup-info-row'><i class='fa fa-pencil'></i> <span class='popup-text-wrap'>" . $hp['catatan'] . "</span></div>" : "") .
                "</div>" .
                "<div class='popup-footer'>" .
                    "<button class='popup-btn popup-btn-homepass' onclick='tampilkanDetailHomepass(" . $hp['id'] . ")'><i class='fa fa-eye'></i> " . $lang['js_detail'] . "</button>" .
                    "<a href='https://www.google.com/maps/dir//" . $hp['coordinates'] . "' target='_blank' class='popup-btn popup-btn-secondary'><i class='fa fa-location-arrow'></i> " . $lang['popup_route'] . "</a>" .
                "</div>"
        ];
    }

    // Hitung Tiang
    $total_tiang = ORM::for_table('tbl_tiang')->count();
    
    // Hitung Kabel
    $total_kabel = count($network_data['kabel']);
    
    // Hitung Homepass
    $total_homepass = count($network_data['homepass']);
    
    // Statistik jaringan
    $stats = [
        'total_routers' => count($network_data['routers']),
        'total_olts' => count($network_data['olts']),
        'total_odcs' => count($network_data['odcs']),
        'total_odps' => count($network_data['odps']),
        'total_tiang' => $total_tiang,
        'total_kabel' => $total_kabel,
        'total_homepass' => $total_homepass,
        'total_customers' => count($network_data['customers'])
    ];

    // Get all routers for parent dropdown
    $all_routers = ORM::for_table('tbl_routers')
        ->where('enabled', 1)
        ->order_by_asc('name')
        ->find_array();
    
    // Get all OLTs for parent OLT dropdown (exclude current if editing)
    $all_olts = ORM::for_table('tbl_olt')
        ->where('status', 'Active')
        ->order_by_asc('name')
        ->find_array();

    // Get all ODCs for parent ODC dropdown
    $all_odcs = ORM::for_table('tbl_odc')
        ->where('status', 'Active')
        ->order_by_asc('name')
        ->find_array();

    // Get all ODPs for parent ODP dropdown
    $all_odps = ORM::for_table('tbl_odp')
        ->where('status', 'Active')
        ->order_by_asc('name')
        ->find_array();

    $ui->assign('search', $search);
    $ui->assign('network_data', $network_data);
    $ui->assign('stats', $stats);
    $ui->assign('all_routers', $all_routers);
    $ui->assign('all_olts', $all_olts);
    $ui->assign('all_odcs', $all_odcs);
    $ui->assign('all_odps', $all_odps);
    $ui->assign('csrf_token', Csrf::generateAndStoreToken());
    // Remove xheader and xfooter - we'll load Leaflet directly in template
    $ui->display('network_mapping_main.tpl');
}

function network_mapping_get_problem_customers()
{
    header('Content-Type: application/json');
    _admin();
    $lang = network_mapping_load_language();

    $page     = max(1, intval(_req('page', 1)));
    $per_page = max(5, min(50, intval(_req('per_page', 5))));
    $search   = trim((string) _req('search', ''));
    $filter   = strtolower(trim((string) _req('filter', 'all'))); // all | offline | isolir
    $offset   = ($page - 1) * $per_page;

    // Build status filter
    $status_filter = [];
    if ($filter === 'offline') {
        $status_filter = ['offline'];
    } elseif ($filter === 'isolir') {
        $status_filter = ['isolir', 'off_isolir'];
    } else {
        // all problem statuses (offline + isolir variants)
        $status_filter = ['offline', 'isolir', 'off_isolir'];
    }

    // Safely quote each status for the IN clause
    $quoted = array_map(function ($s) {
        return "'" . addslashes($s) . "'";
    }, $status_filter);
    $in_clause = implode(',', $quoted);

    // Escape search term
    $search_escaped = addslashes($search);

    // Base SQL joining customers with online status
    $select_cols = "c.id, c.username, c.fullname, c.coordinates, c.phonenumber, c.email, c.address, s.status, s.current_ip, s.last_seen, s.status_changed_at, s.updated_at";
    $base_from = "FROM tbl_customers c INNER JOIN tbl_customer_online_status s ON s.customer_id = c.id";

    // Where conditions: must have coordinates, active customer, status in filter
    $where = "WHERE c.status = 'Active' AND c.coordinates != '' AND c.coordinates IS NOT NULL AND s.status IN ($in_clause)";
    if ($search !== '') {
        $where .= " AND (c.username LIKE '%$search_escaped%' OR c.fullname LIKE '%$search_escaped%')";
    }

    // Count total
    $count_sql = "SELECT COUNT(*) AS cnt $base_from $where";
    $count_stmt = ORM::get_db()->query($count_sql);
    $total = 0;
    if ($count_stmt) {
        $row = $count_stmt->fetch(PDO::FETCH_ASSOC);
        $total = intval($row['cnt']);
    }

    // Fetch paginated — order by most recently changed first
    $data_sql = "SELECT $select_cols $base_from $where ORDER BY s.status_changed_at DESC, c.username ASC LIMIT $per_page OFFSET $offset";
    $stmt = ORM::get_db()->query($data_sql);
    $rows = $stmt ? $stmt->fetchAll(PDO::FETCH_ASSOC) : [];

    // Format rows for frontend
    $items = [];
    foreach ($rows as $r) {
        $coords = $r['coordinates'] ?? '';
        $parts = array_map('trim', explode(',', $coords));
        $lat = isset($parts[0]) && is_numeric($parts[0]) ? floatval($parts[0]) : null;
        $lng = isset($parts[1]) && is_numeric($parts[1]) ? floatval($parts[1]) : null;

        // Normalize status (isolir + off_isolir => isolir)
        $st = $r['status'];
        $display_status = in_array($st, ['isolir', 'off_isolir']) ? 'isolir' : 'offline';

        $items[] = [
            'id'                => intval($r['id']),
            'username'          => $r['username'],
            'fullname'          => $r['fullname'],
            'coordinates'       => $coords,
            'lat'               => $lat,
            'lng'               => $lng,
            'status'            => $display_status,
            'status_raw'        => $st,
            'current_ip'        => $r['current_ip'] ?? '',
            'last_seen'         => $r['last_seen'] ?? '',
            'status_changed_at' => $r['status_changed_at'] ?? '',
            'phone'             => $r['phonenumber'] ?? '',
            'address'           => $r['address'] ?? '',
        ];
    }

    // Count totals per status (for tabs/pills)
    $stat_sql = "SELECT s.status, COUNT(*) AS cnt FROM tbl_customers c INNER JOIN tbl_customer_online_status s ON s.customer_id = c.id WHERE c.status = 'Active' AND c.coordinates != '' AND c.coordinates IS NOT NULL GROUP BY s.status";
    $stat_stmt = ORM::get_db()->query($stat_sql);
    $counts = ['offline' => 0, 'isolir' => 0];
    if ($stat_stmt) {
        foreach ($stat_stmt->fetchAll(PDO::FETCH_ASSOC) as $sr) {
            if ($sr['status'] === 'offline') {
                $counts['offline'] += intval($sr['cnt']);
            } elseif (in_array($sr['status'], ['isolir', 'off_isolir'])) {
                $counts['isolir'] += intval($sr['cnt']);
            }
        }
    }
    $counts['all'] = $counts['offline'] + $counts['isolir'];

    $total_pages = $per_page > 0 ? (int) ceil($total / $per_page) : 1;

    echo json_encode([
        'success'      => true,
        'items'        => $items,
        'pagination'   => [
            'page'         => $page,
            'per_page'     => $per_page,
            'total'        => $total,
            'total_pages'  => max(1, $total_pages),
            'has_prev'     => $page > 1,
            'has_next'     => $page < $total_pages,
        ],
        'counts'       => $counts,
        'filter'       => $filter,
        'search'       => $search,
    ]);
    exit;
}

/**
 * Get paginated list of customers with INVALID / malformed coordinates
 * (e.g. missing comma, non-numeric parts, out-of-range lat/lng, etc.)
 * Used for the "Coordinate Problem" panel so admin can fix them.
 */
function network_mapping_get_invalid_coord_customers()
{
    header('Content-Type: application/json');
    _admin();

    $page     = max(1, intval(_req('page', 1)));
    $per_page = max(5, min(50, intval(_req('per_page', 5))));
    $search   = trim((string) _req('search', ''));
    $offset   = ($page - 1) * $per_page;

    $search_escaped = addslashes($search);

    // Fetch all active customers with non-empty coordinates
    $where = "status = 'Active' AND coordinates IS NOT NULL AND coordinates != ''";
    if ($search !== '') {
        $where .= " AND (username LIKE '%$search_escaped%' OR fullname LIKE '%$search_escaped%')";
    }

    $sql_all = "SELECT id, username, fullname, coordinates, phonenumber, address FROM tbl_customers WHERE $where ORDER BY username ASC";
    $stmt = ORM::get_db()->query($sql_all);
    $rows = $stmt ? $stmt->fetchAll(PDO::FETCH_ASSOC) : [];

    // Helper: validate a single coordinate string "lat,lng"
    $is_invalid_coord = function ($coord) {
        if (!is_string($coord) || $coord === '') return true;
        $parts = explode(',', $coord);
        if (count($parts) !== 2) return true; // no comma or too many commas
        $lat = trim($parts[0]);
        $lng = trim($parts[1]);
        if ($lat === '' || $lng === '') return true;
        if (!is_numeric($lat) || !is_numeric($lng)) return true;
        $lat_f = floatval($lat);
        $lng_f = floatval($lng);
        // Valid lat: -90..90, lng: -180..180
        if ($lat_f < -90 || $lat_f > 90) return true;
        if ($lng_f < -180 || $lng_f > 180) return true;
        return false;
    };

    // Filter only invalid coordinates
    $invalid_all = [];
    foreach ($rows as $r) {
        if ($is_invalid_coord($r['coordinates'])) {
            $invalid_all[] = $r;
        }
    }

    $total = count($invalid_all);

    // Paginate
    $paged = array_slice($invalid_all, $offset, $per_page);

    $items = [];
    foreach ($paged as $r) {
        // Smart problem-type detection (order matters: content check first, then format)
        $coord = (string) ($r['coordinates'] ?? '');
        $problem = 'invalid_format';

        // Extract any numeric-looking parts from the raw string
        // If we can't find any digits at all, it's pure junk text
        $has_digit = preg_match('/\d/', $coord) === 1;

        if (!$has_digit) {
            // No digits at all -> completely invalid (e.g. "halo", "abcdef")
            $problem = 'non_numeric';
        } else {
            $comma_count = substr_count($coord, ',');
            if ($comma_count === 0) {
                // Has digits but no comma (e.g. "-7.54112.21")
                $problem = 'missing_comma';
            } elseif ($comma_count > 1) {
                // Too many commas (e.g. "1,2,3")
                $problem = 'extra_comma';
            } else {
                // Exactly one comma — check each side
                $parts = explode(',', $coord);
                $lat  = trim($parts[0]);
                $lng  = trim($parts[1]);
                if ($lat === '' || $lng === '' || !is_numeric($lat) || !is_numeric($lng)) {
                    $problem = 'non_numeric';
                } else {
                    $latf = floatval($lat);
                    $lngf = floatval($lng);
                    if ($latf < -90 || $latf > 90 || $lngf < -180 || $lngf > 180) {
                        $problem = 'out_of_range';
                    } else {
                        $problem = 'invalid_format'; // fallback (shouldn't reach here normally)
                    }
                }
            }
        }

        $items[] = [
            'id'          => intval($r['id']),
            'username'    => $r['username'],
            'fullname'    => $r['fullname'],
            'coordinates' => $r['coordinates'],
            'phone'       => $r['phonenumber'] ?? '',
            'address'     => $r['address'] ?? '',
            'problem'     => $problem,
        ];
    }

    $total_pages = $per_page > 0 ? (int) ceil($total / $per_page) : 1;

    echo json_encode([
        'success'    => true,
        'items'      => $items,
        'pagination' => [
            'page'        => $page,
            'per_page'    => $per_page,
            'total'       => $total,
            'total_pages' => max(1, $total_pages),
            'has_prev'    => $page > 1,
            'has_next'    => $page < $total_pages,
        ],
        'search'     => $search,
        'csrf_token' => Csrf::generateAndStoreToken(),
    ]);
    exit;
}

function network_mapping_save_olt()
{
    header('Content-Type: application/json');
    $lang = network_mapping_load_language();
    
    $name = _req('name');
    $brand = _req('brand');
    $model = _req('model');
    $serial_number = _req('serial_number');
    $ip_address = _req('ip_address');
    $coordinates = _req('coordinates');
    $address = _req('address');
    $total_ports = _req('total_ports') ?: 8;
    $used_ports = _req('used_ports') ?: 0;
    $status = _req('status') ?: 'Active';
    $description = _req('description');
    
    // Tambahan: Parent connection parameters
    $parent_type = _req('parent_type');
    $parent_router_id = _req('parent_router_id') ?: null;
    $parent_olt_id = _req('parent_olt_id') ?: null;
    
    // Tambahan: Multi-port data
    $ports_data = _req('ports_data'); // JSON string
    
    if (empty($name)) {
        echo json_encode(['status' => 'error', 'message' => $lang['php_olt_name_required']]);
        exit;
    }
    
    if (empty($coordinates)) {
        echo json_encode(['status' => 'error', 'message' => $lang['php_location_not_selected']]);
        exit;
    }
    
    try {
        $olt = ORM::for_table('tbl_olt')->create();
        $olt->name = $name;
        $olt->brand = $brand;
        $olt->model = $model;
        $olt->serial_number = $serial_number;
        $olt->ip_address = $ip_address;
        $olt->coordinates = $coordinates;
        $olt->address = $address;
        $olt->total_ports = $total_ports;
        $olt->used_ports = $used_ports;
        $olt->status = $status;
        $olt->description = $description;
        
        // Simpan parent connection info
        $olt->parent_type = $parent_type;
        $olt->parent_router_id = $parent_router_id;
        $olt->parent_olt_id = $parent_olt_id;
        
        // Simpan multi-port data
        $olt->ports_data = $ports_data;
        
        // Untuk backward compatibility, simpan port pertama ke kolom lama
        if (!empty($ports_data)) {
            $ports_array = json_decode($ports_data, true);
            if (!empty($ports_array) && isset($ports_array[0])) {
                $olt->port_used_name = $ports_array[0]['pon'] ?? '';
                $olt->port_label = $ports_array[0]['label'] ?? '';
            }
        }
        
        $olt->save();
        
        echo json_encode([
            'status' => 'success', 
            'message' => $lang['php_olt_saved'],
            'data' => [
                'id' => $olt->id(),
                'name' => $name,
                'coordinates' => $coordinates
            ]
        ]);
    } catch (Exception $e) {
        echo json_encode(['status' => 'error', 'message' => $lang['php_olt_save_failed'] . ': ' . $e->getMessage()]);
    }
    exit;
}

function network_mapping_get_olts()
{
    header('Content-Type: application/json');
    $lang = network_mapping_load_language();
    
    $olts = ORM::for_table('tbl_olt')
        ->where('status', 'Active')
        ->where_not_equal('coordinates', '')
        ->find_array();
    
    echo json_encode(['status' => 'success', 'data' => $olts]);
    exit;
}

function network_mapping_update_olt()
{
    header('Content-Type: application/json');
    $lang = network_mapping_load_language();
    
    $olt_id = _req('olt_id');
    $name = _req('name');
    $brand = _req('brand');
    $model = _req('model');
    $serial_number = _req('serial_number');
    $ip_address = _req('ip_address');
    $status = _req('status') ?: 'Active';
    $coordinates = _req('coordinates');
    $address = _req('address');
    $description = _req('description');
    
    // Tambahan: Parent connection parameters
    $parent_type = _req('parent_type');
    $parent_router_id = _req('parent_router_id') ?: null;
    $parent_olt_id = _req('parent_olt_id') ?: null;
    
    // Tambahan: Port configuration
    $total_ports = _req('total_ports') ?: 8;
    $used_ports = _req('used_ports') ?: 0;
    $ports_data = _req('ports_data'); // JSON string
    
    if (empty($olt_id)) {
        echo json_encode(['status' => 'error', 'message' => 'ID OLT tidak valid']);
        exit;
    }
    
    if (empty($name)) {
        echo json_encode(['status' => 'error', 'message' => 'Nama OLT harus diisi']);
        exit;
    }
    
    if (empty($coordinates)) {
        echo json_encode(['status' => 'error', 'message' => 'Koordinat harus diisi']);
        exit;
    }
    
    try {
        $olt = ORM::for_table('tbl_olt')->find_one($olt_id);
        
        if (!$olt) {
            echo json_encode(['status' => 'error', 'message' => 'OLT tidak ditemukan']);
            exit;
        }
        
        // Update data dasar
        $olt->name = $name;
        $olt->brand = $brand;
        $olt->model = $model;
        $olt->serial_number = $serial_number;
        $olt->ip_address = $ip_address;
        $olt->status = $status;
        $olt->coordinates = $coordinates;
        $olt->address = $address;
        $olt->description = $description;
        
        // Update parent connection
        $olt->parent_type = $parent_type;
        $olt->parent_router_id = $parent_router_id;
        $olt->parent_olt_id = $parent_olt_id;
        
        // Update port configuration
        $olt->total_ports = $total_ports;
        $olt->used_ports = $used_ports;
        $olt->ports_data = $ports_data; // Simpan JSON string langsung
        
        // Untuk backward compatibility, simpan juga port pertama ke kolom lama
        if (!empty($ports_data)) {
            $ports_array = json_decode($ports_data, true);
            if (!empty($ports_array) && isset($ports_array[0])) {
                $olt->port_used_name = $ports_array[0]['pon'] ?? '';
                $olt->port_label = $ports_array[0]['label'] ?? '';
            }
        } else {
            $olt->port_used_name = '';
            $olt->port_label = '';
        }
        
        $olt->save();
        
        echo json_encode([
            'status' => 'success', 
            'message' => 'Data OLT berhasil diperbarui',
            'data' => [
                'id' => $olt->id(),
                'name' => $name,
                'coordinates' => $coordinates
            ]
        ]);
    } catch (Exception $e) {
        echo json_encode(['status' => 'error', 'message' => 'Gagal memperbarui OLT: ' . $e->getMessage()]);
    }
    exit;
}

function network_mapping_delete_olt()
{
    header('Content-Type: application/json');
    $lang = network_mapping_load_language();
    
    $olt_id = _req('olt_id');
    
    if (empty($olt_id)) {
        echo json_encode(['status' => 'error', 'message' => 'ID OLT tidak valid']);
        exit;
    }
    
    try {
        $olt = ORM::for_table('tbl_olt')->find_one($olt_id);
        
        if (!$olt) {
            echo json_encode(['status' => 'error', 'message' => 'OLT tidak ditemukan']);
            exit;
        }
        
        // Simpan nama untuk response
        $olt_name = $olt->name;
        
        // Hapus OLT dari database
        $olt->delete();
        
        echo json_encode([
            'status' => 'success', 
            'message' => 'OLT "' . $olt_name . '" berhasil dihapus'
        ]);
    } catch (Exception $e) {
        echo json_encode(['status' => 'error', 'message' => 'Gagal menghapus OLT: ' . $e->getMessage()]);
    }
    exit;
}

// ==================== FUNGSI CRUD ODC ====================

function network_mapping_save_odc()
{
    header('Content-Type: application/json');
    $lang = network_mapping_load_language();
    
    $name = _req('name');
    $coordinates = _req('coordinates');
    $address = _req('address');
    $total_ports = _req('total_ports') ?: 8;
    $used_ports = _req('used_ports') ?: 0;
    $status = _req('status') ?: 'Active';
    $description = _req('description');
    
    // Parent connection parameters
    $parent_type = _req('parent_type');
    $parent_olt_id = _req('parent_olt_id') ?: null;
    $parent_odc_id = _req('parent_odc_id') ?: null;
    
    // Multi-port data
    $ports_data = _req('ports_data'); // JSON string
    
    // Multi-slot parameters (NEW)
    $box_id = _req('box_id'); // ID Box untuk grouping
    $slot_number = _req('slot_number') ?: 1;
    $is_new_box = _req('is_new_box'); // 'yes' atau 'no'
    
    if (empty($name)) {
        echo json_encode(['status' => 'error', 'message' => $lang['php_odc_name_required']]);
        exit;
    }
    
    if (empty($coordinates)) {
        echo json_encode(['status' => 'error', 'message' => $lang['php_location_not_selected']]);
        exit;
    }
    
    try {
        $odc = ORM::for_table('tbl_odc')->create();
        $odc->name = $name;
        $odc->coordinates = $coordinates;
        $odc->address = $address;
        $odc->total_ports = $total_ports;
        $odc->used_ports = $used_ports;
        $odc->status = $status;
        $odc->description = $description;
        
        // Simpan parent connection info
        $odc->parent_type = $parent_type;
        $odc->parent_olt_id = $parent_olt_id;
        $odc->parent_odc_id = $parent_odc_id;
        
        // Simpan multi-port data
        $odc->ports_data = $ports_data;
        
        // Multi-slot support (NEW)
        if ($is_new_box === 'yes' || empty($box_id)) {
            // Generate box_id baru
            $odc->save(); // Simpan dulu untuk dapat ID
            $odc->box_id = 'BOX-ODC-' . $odc->id();
            $odc->slot_number = 1;
        } else {
            // Tambah ke box existing
            $odc->box_id = $box_id;
            
            // Hitung slot number berikutnya di box ini
            $existing_slots = ORM::for_table('tbl_odc')
                ->where('box_id', $box_id)
                ->count();
            $odc->slot_number = $existing_slots + 1;
        }
        
        // Handle upload foto
        $foto_filename = network_mapping_upload_foto_odc($name);
        if ($foto_filename) {
            $odc->foto = $foto_filename;
        }
        
        $odc->save();
        
        echo json_encode([
            'status' => 'success', 
            'message' => $lang['php_odc_saved'] . ' (Slot ' . $odc->slot_number . ')',
            'data' => [
                'id' => $odc->id(),
                'name' => $name,
                'coordinates' => $coordinates,
                'box_id' => $odc->box_id,
                'slot_number' => $odc->slot_number
            ]
        ]);
    } catch (Exception $e) {
        echo json_encode(['status' => 'error', 'message' => $lang['php_odc_save_failed'] . ': ' . $e->getMessage()]);
    }
    exit;
}

function network_mapping_get_odcs()
{
    header('Content-Type: application/json');
    $lang = network_mapping_load_language();
    
    $odcs = ORM::for_table('tbl_odc')
        ->where('status', 'Active')
        ->where_not_equal('coordinates', '')
        ->find_array();
    
    echo json_encode(['status' => 'success', 'data' => $odcs]);
    exit;
}

function network_mapping_get_odc_by_coordinates()
{
    header('Content-Type: application/json');
    $lang = network_mapping_load_language();
    
    $coordinates = _req('coordinates');
    
    if (empty($coordinates)) {
        echo json_encode(['status' => 'error', 'message' => 'Koordinat tidak valid']);
        exit;
    }
    
    try {
        $odcs = ORM::for_table('tbl_odc')
            ->where('coordinates', $coordinates)
            ->order_by_asc('slot_number')
            ->find_array();
        
        $slots = [];
        foreach ($odcs as $odc) {
            // Get parent info
            $parent_olt_name = null;
            if (!empty($odc['parent_olt_id'])) {
                $parent_olt = ORM::for_table('tbl_olt')->where('id', $odc['parent_olt_id'])->find_one();
                if ($parent_olt) $parent_olt_name = $parent_olt->name;
            }
            
            $parent_odc_name = null;
            if (!empty($odc['parent_odc_id'])) {
                $parent_odc = ORM::for_table('tbl_odc')->where('id', $odc['parent_odc_id'])->find_one();
                if ($parent_odc) $parent_odc_name = $parent_odc->name;
            }
            
            // Parse ports_data - pastikan decode JSON dengan benar
            $ports_data = [];
            if (!empty($odc['ports_data'])) {
                $decoded = json_decode($odc['ports_data'], true);
                if (is_array($decoded)) {
                    $ports_data = $decoded;
                }
            }
            
            // Generate foto URL dengan cache busting
            $slot_foto_url = '';
            if (!empty($odc['foto'])) {
                $foto_path = 'system/uploads/foto/odc/' . $odc['foto'];
                if (file_exists($foto_path)) {
                    $base_url = str_replace('?_route=', '', U);
                    $base_url = rtrim($base_url, '/') . '/';
                    $foto_timestamp = filemtime($foto_path);
                    $slot_foto_url = $base_url . 'system/uploads/foto/odc/' . $odc['foto'] . '?v=' . $foto_timestamp;
                }
            }
            
            $slots[] = [
                'id' => $odc['id'],
                'name' => $odc['name'],
                'box_id' => $odc['box_id'],
                'slot_number' => $odc['slot_number'] ?? 1,
                'coordinates' => $odc['coordinates'],
                'address' => $odc['address'],
                'description' => $odc['description'],
                'total_ports' => $odc['total_ports'],
                'used_ports' => $odc['used_ports'],
                'available_ports' => $odc['total_ports'] - $odc['used_ports'],
                'status' => $odc['status'],
                'parent_type' => $odc['parent_type'],
                'parent_olt_id' => $odc['parent_olt_id'],
                'parent_olt_name' => $parent_olt_name,
                'parent_odc_id' => $odc['parent_odc_id'],
                'parent_odc_name' => $parent_odc_name,
                'ports_data' => $ports_data,  // Sudah berupa array
                'foto' => $odc['foto'],
                'foto_url' => $slot_foto_url
            ];
        }
        
        echo json_encode([
            'status' => 'success',
            'data' => [
                'coordinates' => $coordinates,
                'total_slots' => count($slots),
                'slots' => $slots
            ]
        ]);
    } catch (Exception $e) {
        echo json_encode(['status' => 'error', 'message' => 'Gagal mengambil data: ' . $e->getMessage()]);
    }
    exit;
}

function network_mapping_update_odc()
{
    header('Content-Type: application/json');
    $lang = network_mapping_load_language();
    
    $odc_id = _req('odc_id');
    $name = _req('name');
    $coordinates = _req('coordinates');
    $address = _req('address');
    $total_ports = _req('total_ports') ?: 8;
    $used_ports = _req('used_ports') ?: 0;
    $status = _req('status') ?: 'Active';
    $description = _req('description');
    
    // Parent connection parameters
    $parent_type = _req('parent_type');
    $parent_olt_id = _req('parent_olt_id') ?: null;
    $parent_odc_id = _req('parent_odc_id') ?: null;
    
    // Multi-port data
    $ports_data = _req('ports_data'); // JSON string
    
    // Multi-slot parameters (NEW)
    $slot_number = _req('slot_number');
    
    // Foto parameters
    $hapus_foto = _req('hapus_foto');
    
    if (empty($odc_id)) {
        echo json_encode(['status' => 'error', 'message' => 'ID ODC tidak valid']);
        exit;
    }
    
    if (empty($name)) {
        echo json_encode(['status' => 'error', 'message' => $lang['php_odc_name_required']]);
        exit;
    }
    
    if (empty($coordinates)) {
        echo json_encode(['status' => 'error', 'message' => 'Koordinat harus diisi']);
        exit;
    }
    
    try {
        $odc = ORM::for_table('tbl_odc')->find_one($odc_id);
        
        if (!$odc) {
            echo json_encode(['status' => 'error', 'message' => 'ODC tidak ditemukan']);
            exit;
        }
        
        $old_name = $odc->name;
        $old_foto = $odc->foto;
        
        // Cek apakah ada file upload baru
        $ada_upload_baru = isset($_FILES['foto_odc']) && $_FILES['foto_odc']['error'] === UPLOAD_ERR_OK;
        
        // Handle hapus foto (hanya jika TIDAK ada upload baru)
        if ($hapus_foto == '1' && !empty($old_foto) && !$ada_upload_baru) {
            $old_foto_path = 'system/uploads/foto/odc/' . $old_foto;
            if (file_exists($old_foto_path)) {
                unlink($old_foto_path);
            }
            $odc->foto = null;
            $old_foto = null;
        }
        
        // Handle upload foto baru (swap foto)
        if ($ada_upload_baru) {
            // Hapus foto lama DULU sebelum upload baru
            if (!empty($old_foto)) {
                $old_foto_path = 'system/uploads/foto/odc/' . $old_foto;
                if (file_exists($old_foto_path)) {
                    unlink($old_foto_path);
                }
            }
            
            // Upload foto baru
            $foto_filename = network_mapping_upload_foto_odc($name);
            if ($foto_filename) {
                $odc->foto = $foto_filename;
            } else {
                $odc->foto = null;
            }
        } else {
            // Tidak ada upload baru, cek apakah nama berubah untuk rename foto
            if ($old_name !== $name && !empty($odc->foto)) {
                $new_foto_filename = network_mapping_rename_foto_odc($odc->foto, $name);
                if ($new_foto_filename) {
                    $odc->foto = $new_foto_filename;
                }
            }
        }
        
        // Update data
        $odc->name = $name;
        $odc->coordinates = $coordinates;
        $odc->address = $address;
        $odc->total_ports = $total_ports;
        $odc->used_ports = $used_ports;
        $odc->status = $status;
        $odc->description = $description;
        
        // Update parent connection
        $odc->parent_type = $parent_type;
        $odc->parent_olt_id = $parent_olt_id;
        $odc->parent_odc_id = $parent_odc_id;
        
        // Update multi-port data
        $odc->ports_data = $ports_data;
        
        // Update slot number jika ada (NEW)
        if (!empty($slot_number)) {
            $odc->slot_number = $slot_number;
        }
        
        $odc->save();
        
        echo json_encode([
            'status' => 'success', 
            'message' => 'Data ODC berhasil diperbarui',
            'data' => [
                'id' => $odc->id(),
                'name' => $name,
                'coordinates' => $coordinates,
                'box_id' => $odc->box_id,
                'slot_number' => $odc->slot_number,
                'foto' => $odc->foto
            ]
        ]);
    } catch (Exception $e) {
        echo json_encode(['status' => 'error', 'message' => 'Gagal memperbarui ODC: ' . $e->getMessage()]);
    }
    exit;
}

function network_mapping_delete_odc()
{
    header('Content-Type: application/json');
    $lang = network_mapping_load_language();
    
    $odc_id = _req('odc_id');
    
    if (empty($odc_id)) {
        echo json_encode(['status' => 'error', 'message' => 'ID ODC tidak valid']);
        exit;
    }
    
    try {
        $odc = ORM::for_table('tbl_odc')->find_one($odc_id);
        
        if (!$odc) {
            echo json_encode(['status' => 'error', 'message' => 'ODC tidak ditemukan']);
            exit;
        }
        
        // Simpan nama untuk response
        $odc_name = $odc->name;
        $odc_foto = $odc->foto;
        $odc_box_id = $odc->box_id;
        $odc_coordinates = $odc->coordinates;
        
        // Simpan data yang mungkin perlu dipindahkan
        $odc_address = $odc->address;
        $odc_description = $odc->description;
        
        // Cari slot lain di koordinat yang sama (selain slot ini)
        $other_slots = ORM::for_table('tbl_odc')
            ->where('coordinates', $odc_coordinates)
            ->where_not_equal('id', $odc_id)
            ->order_by_asc('slot_number')
            ->find_array();
        
        if (!empty($other_slots)) {
            // Ada slot lain, pindahkan data ke slot berikutnya
            $next_slot = ORM::for_table('tbl_odc')->find_one($other_slots[0]['id']);
            if ($next_slot) {
                // Pindahkan foto jika ada
                if (!empty($odc_foto)) {
                    $next_slot->foto = $odc_foto;
                }
                
                // Selalu pindahkan address dari slot yang dihapus (jika ada)
                if (!empty($odc_address)) {
                    $next_slot->address = $odc_address;
                }
                
                // Selalu pindahkan description dari slot yang dihapus (jika ada)
                if (!empty($odc_description)) {
                    $next_slot->description = $odc_description;
                }
                
                $next_slot->save();
            }
            // Jangan hapus file foto karena sudah dipindah ke slot lain
        } else {
            // Tidak ada slot lain, hapus file foto jika ada
            if (!empty($odc_foto)) {
                $foto_path = 'system/uploads/foto/odc/' . $odc_foto;
                if (file_exists($foto_path)) {
                    unlink($foto_path);
                }
            }
        }
        
        // Hapus ODC dari database
        $odc->delete();
        
        echo json_encode([
            'status' => 'success', 
            'message' => 'ODC "' . $odc_name . '" berhasil dihapus'
        ]);
    } catch (Exception $e) {
        echo json_encode(['status' => 'error', 'message' => 'Gagal menghapus ODC: ' . $e->getMessage()]);
    }
    exit;
}

// ==================== FUNGSI CRUD ODP ====================

function network_mapping_save_odp()
{
    header('Content-Type: application/json');
    $lang = network_mapping_load_language();
    
    $name = _req('name');
    $coordinates = _req('coordinates');
    $address = _req('address');
    $total_ports = _req('total_ports') ?: 8;
    $used_ports = _req('used_ports') ?: 0;
    $status = _req('status') ?: 'Active';
    $description = _req('description');
    
    // Parent connection parameters
    $parent_type = _req('parent_type');
    $parent_odc_id = _req('parent_odc_id') ?: null;
    $parent_odp_id = _req('parent_odp_id') ?: null;
    
    // Multi-port data
    $ports_data = _req('ports_data'); // JSON string
    
    // Multi-slot parameters (NEW)
    $box_id = _req('box_id');
    $slot_number = _req('slot_number') ?: 1;
    $is_new_box = _req('is_new_box');
    
    if (empty($name)) {
        echo json_encode(['status' => 'error', 'message' => $lang['php_odp_name_required']]);
        exit;
    }
    
    if (empty($coordinates)) {
        echo json_encode(['status' => 'error', 'message' => $lang['php_location_not_selected']]);
        exit;
    }
    
    try {
        $odp = ORM::for_table('tbl_odp')->create();
        $odp->name = $name;
        $odp->coordinates = $coordinates;
        $odp->address = $address;
        $odp->total_ports = $total_ports;
        $odp->used_ports = $used_ports;
        $odp->status = $status;
        $odp->description = $description;
        
        // Simpan parent connection info
        $odp->parent_type = $parent_type;
        $odp->parent_odc_id = $parent_odc_id;
        $odp->parent_odp_id = $parent_odp_id;
        
        // Simpan multi-port data
        $odp->ports_data = $ports_data;
        
        // Multi-slot support (NEW)
        if ($is_new_box === 'yes' || empty($box_id)) {
            // Generate box_id baru
            $odp->save(); // Simpan dulu untuk dapat ID
            $odp->box_id = 'BOX-ODP-' . $odp->id();
            $odp->slot_number = 1;
        } else {
            // Tambah ke box existing
            $odp->box_id = $box_id;
            
            // Hitung slot number berikutnya di box ini
            $existing_slots = ORM::for_table('tbl_odp')
                ->where('box_id', $box_id)
                ->count();
            $odp->slot_number = $existing_slots + 1;
        }
        
        // Handle upload foto (hanya untuk Box Baru / Slot 1)
        if ($is_new_box === 'yes' || empty($box_id)) {
            $foto_filename = network_mapping_upload_foto_odp($name);
            if ($foto_filename) {
                $odp->foto = $foto_filename;
            }
        }
        
        $odp->save();
        
        echo json_encode([
            'status' => 'success', 
            'message' => $lang['php_odp_saved'] . ' (Slot ' . $odp->slot_number . ')',
            'data' => [
                'id' => $odp->id(),
                'name' => $name,
                'coordinates' => $coordinates,
                'box_id' => $odp->box_id,
                'slot_number' => $odp->slot_number
            ]
        ]);
    } catch (Exception $e) {
        echo json_encode(['status' => 'error', 'message' => $lang['php_odp_save_failed'] . ': ' . $e->getMessage()]);
    }
    exit;
}

function network_mapping_get_odps()
{
    header('Content-Type: application/json');
    $lang = network_mapping_load_language();
    
    $odps = ORM::for_table('tbl_odp')
        ->where('status', 'Active')
        ->where_not_equal('coordinates', '')
        ->find_array();
    
    echo json_encode(['status' => 'success', 'data' => $odps]);
    exit;
}

function network_mapping_get_odp_by_coordinates()
{
    header('Content-Type: application/json');
    $lang = network_mapping_load_language();
    
    $coordinates = _req('coordinates');
    
    if (empty($coordinates)) {
        echo json_encode(['status' => 'error', 'message' => $lang['php_coordinates_invalid']]);
        exit;
    }
    
    try {
        $odps = ORM::for_table('tbl_odp')
            ->where('coordinates', $coordinates)
            ->order_by_asc('slot_number')
            ->find_array();
        
        $slots = [];
        foreach ($odps as $odp) {
            // Get parent ODC name
            $parent_odc_name = null;
            if (!empty($odp['parent_odc_id'])) {
                $parent_odc = ORM::for_table('tbl_odc')->where('id', $odp['parent_odc_id'])->find_one();
                if ($parent_odc) $parent_odc_name = $parent_odc->name;
            }
            
            // Get parent ODP name
            $parent_odp_name = null;
            if (!empty($odp['parent_odp_id'])) {
                $parent_odp_obj = ORM::for_table('tbl_odp')->where('id', $odp['parent_odp_id'])->find_one();
                if ($parent_odp_obj) $parent_odp_name = $parent_odp_obj->name;
            }
            
            // Parse ports_data - decode JSON dengan benar
            $ports_data = [];
            if (!empty($odp['ports_data'])) {
                $decoded = json_decode($odp['ports_data'], true);
                if (is_array($decoded)) {
                    $ports_data = $decoded;
                }
            }
            
            // Generate foto URL dengan cache busting
            $slot_foto_url = '';
            if (!empty($odp['foto'])) {
                $foto_path = 'system/uploads/foto/odp/' . $odp['foto'];
                if (file_exists($foto_path)) {
                    $base_url = str_replace('?_route=', '', U);
                    $base_url = rtrim($base_url, '/') . '/';
                    $foto_timestamp = filemtime($foto_path);
                    $slot_foto_url = $base_url . 'system/uploads/foto/odp/' . $odp['foto'] . '?v=' . $foto_timestamp;
                }
            }
            
            $slots[] = [
                'id' => $odp['id'],
                'name' => $odp['name'],
                'box_id' => isset($odp['box_id']) ? $odp['box_id'] : null,
                'slot_number' => isset($odp['slot_number']) ? $odp['slot_number'] : 1,
                'coordinates' => $odp['coordinates'],
                'address' => $odp['address'],
                'description' => $odp['description'],
                'total_ports' => (int)$odp['total_ports'],
                'used_ports' => (int)$odp['used_ports'],
                'available_ports' => (int)$odp['total_ports'] - (int)$odp['used_ports'],
                'status' => $odp['status'],
                'parent_type' => $odp['parent_type'],
                'parent_odc_id' => $odp['parent_odc_id'],
                'parent_odc_name' => $parent_odc_name,
                'parent_odp_id' => $odp['parent_odp_id'],
                'parent_odp_name' => $parent_odp_name,
                'ports_data' => $ports_data,
                'foto' => $odp['foto'],
                'foto_url' => $slot_foto_url
            ];
        }
        
        echo json_encode([
            'status' => 'success',
            'data' => [
                'coordinates' => $coordinates,
                'total_slots' => count($slots),
                'slots' => $slots
            ]
        ]);
    } catch (Exception $e) {
        echo json_encode(['status' => 'error', 'message' => $lang['php_fetch_data_failed'] . ': ' . $e->getMessage()]);
    }
    exit;
}

function network_mapping_update_odp()
{
    header('Content-Type: application/json');
    $lang = network_mapping_load_language();
    
    $odp_id = _req('odp_id');
    $name = _req('name');
    $coordinates = _req('coordinates');
    $address = _req('address');
    $total_ports = _req('total_ports') ?: 8;
    $used_ports = _req('used_ports') ?: 0;
    $status = _req('status') ?: 'Active';
    $description = _req('description');
    
    // Parent connection parameters
    $parent_type = _req('parent_type');
    $parent_odc_id = _req('parent_odc_id') ?: null;
    $parent_odp_id = _req('parent_odp_id') ?: null;
    
    // Multi-port data
    $ports_data = _req('ports_data'); // JSON string
    
    // Multi-slot parameters (NEW)
    $slot_number = _req('slot_number');
    
    // Foto parameters
    $hapus_foto = _req('hapus_foto');
    
    if (empty($odp_id)) {
        echo json_encode(['status' => 'error', 'message' => $lang['php_odp_id_invalid']]);
        exit;
    }
    
    if (empty($name)) {
        echo json_encode(['status' => 'error', 'message' => $lang['php_odp_name_required']]);
        exit;
    }
    
    if (empty($coordinates)) {
        echo json_encode(['status' => 'error', 'message' => $lang['php_coordinates_required']]);
        exit;
    }
    
    try {
        $odp = ORM::for_table('tbl_odp')->find_one($odp_id);
        
        if (!$odp) {
            echo json_encode(['status' => 'error', 'message' => $lang['php_odp_not_found']]);
            exit;
        }
        
        $old_name = $odp->name;
        $old_foto = $odp->foto;
        
        // Cek apakah ada file upload baru
        $ada_upload_baru = isset($_FILES['foto_odp']) && $_FILES['foto_odp']['error'] === UPLOAD_ERR_OK;
        
        // Handle hapus foto (hanya jika TIDAK ada upload baru)
        if ($hapus_foto == '1' && !empty($old_foto) && !$ada_upload_baru) {
            $old_foto_path = 'system/uploads/foto/odp/' . $old_foto;
            if (file_exists($old_foto_path)) {
                unlink($old_foto_path);
            }
            $odp->foto = null;
            $old_foto = null;
        }
        
        // Handle upload foto baru (swap foto)
        if ($ada_upload_baru) {
            // Hapus foto lama DULU sebelum upload baru
            if (!empty($old_foto)) {
                $old_foto_path = 'system/uploads/foto/odp/' . $old_foto;
                if (file_exists($old_foto_path)) {
                    unlink($old_foto_path);
                }
            }
            
            // Upload foto baru
            $foto_filename = network_mapping_upload_foto_odp($name);
            if ($foto_filename) {
                $odp->foto = $foto_filename;
            } else {
                $odp->foto = null;
            }
        } else {
            // Tidak ada upload baru, cek apakah nama berubah untuk rename foto
            if ($old_name !== $name && !empty($odp->foto)) {
                $new_foto_filename = network_mapping_rename_foto_odp($odp->foto, $name);
                if ($new_foto_filename) {
                    $odp->foto = $new_foto_filename;
                }
            }
        }
        
        // Update data
        $odp->name = $name;
        $odp->coordinates = $coordinates;
        $odp->address = $address;
        $odp->total_ports = $total_ports;
        $odp->used_ports = $used_ports;
        $odp->status = $status;
        $odp->description = $description;
        
        // Update parent connection
        $odp->parent_type = $parent_type;
        $odp->parent_odc_id = $parent_odc_id;
        $odp->parent_odp_id = $parent_odp_id;
        
        // Update multi-port data
        $odp->ports_data = $ports_data;
        
        // Update slot number jika ada (NEW)
        if (!empty($slot_number)) {
            $odp->slot_number = $slot_number;
        }
        
        $odp->save();
        
        echo json_encode([
            'status' => 'success', 
            'message' => $lang['php_odp_updated'],
            'data' => [
                'id' => $odp->id(),
                'name' => $name,
                'coordinates' => $coordinates,
                'box_id' => $odp->box_id,
                'slot_number' => $odp->slot_number,
                'foto' => $odp->foto
            ]
        ]);
    } catch (Exception $e) {
        echo json_encode(['status' => 'error', 'message' => $lang['php_odp_update_failed'] . ': ' . $e->getMessage()]);
    }
    exit;
}

function network_mapping_delete_odp()
{
    header('Content-Type: application/json');
    $lang = network_mapping_load_language();
    
    $odp_id = _req('odp_id');
    
    if (empty($odp_id)) {
        echo json_encode(['status' => 'error', 'message' => $lang['php_odp_id_invalid']]);
        exit;
    }
    
    try {
        $odp = ORM::for_table('tbl_odp')->find_one($odp_id);
        
        if (!$odp) {
            echo json_encode(['status' => 'error', 'message' => $lang['php_odp_not_found']]);
            exit;
        }
        
        // Simpan data untuk response dan proses
        $odp_name = $odp->name;
        $odp_foto = $odp->foto;
        $odp_coordinates = $odp->coordinates;
        $odp_address = $odp->address;
        $odp_description = $odp->description;
        
        // Cari slot lain di koordinat yang sama (selain slot ini)
        $other_slots = ORM::for_table('tbl_odp')
            ->where('coordinates', $odp_coordinates)
            ->where_not_equal('id', $odp_id)
            ->order_by_asc('slot_number')
            ->find_array();
        
        if (!empty($other_slots)) {
            // Ada slot lain, pindahkan data ke slot berikutnya
            $next_slot = ORM::for_table('tbl_odp')->find_one($other_slots[0]['id']);
            if ($next_slot) {
                // Pindahkan foto jika ada
                if (!empty($odp_foto)) {
                    $next_slot->foto = $odp_foto;
                }
                
                // Selalu pindahkan address dari slot yang dihapus (jika ada)
                if (!empty($odp_address)) {
                    $next_slot->address = $odp_address;
                }
                
                // Selalu pindahkan description dari slot yang dihapus (jika ada)
                if (!empty($odp_description)) {
                    $next_slot->description = $odp_description;
                }
                
                $next_slot->save();
            }
            // Jangan hapus file foto karena sudah dipindah ke slot lain
        } else {
            // Tidak ada slot lain, hapus file foto jika ada
            if (!empty($odp_foto)) {
                $foto_path = 'system/uploads/foto/odp/' . $odp_foto;
                if (file_exists($foto_path)) {
                    unlink($foto_path);
                }
            }
        }
        
        // Hapus ODP dari database
        $odp->delete();
        
        echo json_encode([
            'status' => 'success', 
            'message' => 'ODP "' . $odp_name . '" ' . $lang['php_deleted_successfully']
        ]);
    } catch (Exception $e) {
        echo json_encode(['status' => 'error', 'message' => $lang['php_odp_delete_failed'] . ': ' . $e->getMessage()]);
    }
    exit;
}

// ==================== HELPER FUNCTIONS KABEL ====================

/**
 * Get device name by type and id
 */
function network_mapping_get_device_name($type, $id)
{
    $name = 'Unknown';
    
    switch ($type) {
        case 'router':
            $device = ORM::for_table('tbl_routers')->find_one($id);
            if ($device) $name = $device->name;
            break;
        case 'olt':
            $device = ORM::for_table('tbl_olt')->find_one($id);
            if ($device) $name = $device->name;
            break;
        case 'odc':
            $device = ORM::for_table('tbl_odc')->find_one($id);
            if ($device) $name = $device->name;
            break;
        case 'odp':
            $device = ORM::for_table('tbl_odp')->find_one($id);
            if ($device) $name = $device->name;
            break;
        case 'customer':
            $device = ORM::for_table('tbl_customers')->find_one($id);
            if ($device) $name = $device->fullname;
            break;
    }
    
    return $name;
}

/**
 * Get cable color based on device types
 */
function network_mapping_get_cable_color($type1, $type2)
{
    // Sort types to make comparison easier
    $types = [$type1, $type2];
    sort($types);
    $connection = implode('-', $types);
    
    $colors = [
        'router-router' => '#ea580c',    // Merah Oranye
        'olt-router' => '#c2410c',       // Oranye Gelap
        'olt-olt' => '#8b5cf6',          // Ungu
        'odc-olt' => '#10b981',          // Hijau
        'odc-odc' => '#fb923c',          // Oranye Terang
        'odc-odp' => '#ca8a04',          // Kuning Gelap
        'odp-odp' => '#facc15',          // Kuning Terang
        'customer-odp' => '#3b82f6',     // Biru
    ];
    
    return isset($colors[$connection]) ? $colors[$connection] : '#78716c'; // Default abu-abu
}

function network_mapping_format_meter($value)
{
    if (empty($value) || $value == 0) {
        return '-';
    }
    
    $floatVal = floatval($value);
    
    // Jika >= 1000 meter, konversi ke KM
    if ($floatVal >= 1000) {
        $km = $floatVal / 1000;
        // Cek apakah bilangan bulat
        if ($km == floor($km)) {
            return number_format($km, 0, ',', '.') . ' km';
        } else {
            // Hapus trailing zeros, maksimal 2 desimal
            $formatted = rtrim(rtrim(number_format($km, 2, ',', '.'), '0'), ',');
            return $formatted . ' km';
        }
    }
    
    // Jika < 1000 meter, tampilkan dalam meter
    // Cek apakah bilangan bulat
    if ($floatVal == floor($floatVal)) {
        return number_format($floatVal, 0, ',', '.') . ' meter';
    } else {
        // Hapus trailing zeros setelah koma
        $formatted = rtrim(rtrim(number_format($floatVal, 2, ',', '.'), '0'), ',');
        return $formatted . ' meter';
    }
}

// ==================== FUNGSI CRUD KABEL ====================

function network_mapping_save_kabel()
{
    header('Content-Type: application/json');
    $lang = network_mapping_load_language();
    
    $name = _req('name');
    $device_1_type = _req('device_1_type');
    $device_1_id = _req('device_1_id');
    $device_2_type = _req('device_2_type');
    $device_2_id = _req('device_2_id');
    $coordinates_path = _req('coordinates_path'); // JSON string
    $panjang = _req('panjang') ?: 0;
    $jumlah_sambungan = _req('jumlah_sambungan') ?: 0;
    $sambungan_data = _req('sambungan_data'); // JSON string
    $description = _req('description');
    
    // Validasi
    if (empty($device_1_type) || empty($device_1_id)) {
        echo json_encode(['status' => 'error', 'message' => 'Device awal harus dipilih']);
        exit;
    }
    
    if (empty($device_2_type) || empty($device_2_id)) {
        echo json_encode(['status' => 'error', 'message' => 'Device akhir harus dipilih']);
        exit;
    }
    
    if (empty($coordinates_path)) {
        echo json_encode(['status' => 'error', 'message' => 'Jalur kabel tidak valid']);
        exit;
    }
    
    // Check for duplicate connection
    $existing = ORM::for_table('tbl_kabel')
        ->where_raw("(
            (device_1_type = ? AND device_1_id = ? AND device_2_type = ? AND device_2_id = ?) OR
            (device_1_type = ? AND device_1_id = ? AND device_2_type = ? AND device_2_id = ?)
        )", [
            $device_1_type, $device_1_id, $device_2_type, $device_2_id,
            $device_2_type, $device_2_id, $device_1_type, $device_1_id
        ])
        ->find_one();
    
    if ($existing) {
        echo json_encode(['status' => 'error', 'message' => 'Kabel antara kedua device ini sudah ada!']);
        exit;
    }
    
    try {
        $kabel = ORM::for_table('tbl_kabel')->create();
        $kabel->name = $name;
        $kabel->device_1_type = $device_1_type;
        $kabel->device_1_id = $device_1_id;
        $kabel->device_2_type = $device_2_type;
        $kabel->device_2_id = $device_2_id;
        $kabel->coordinates_path = $coordinates_path;
        $kabel->panjang = $panjang;
        $kabel->jumlah_sambungan = $jumlah_sambungan;
        $kabel->sambungan_data = $sambungan_data;
        $kabel->description = $description;
        
        $kabel->save();
        
        echo json_encode([
            'status' => 'success', 
            'message' => 'Kabel berhasil disimpan',
            'data' => [
                'id' => $kabel->id(),
                'name' => $name
            ]
        ]);
    } catch (Exception $e) {
        echo json_encode(['status' => 'error', 'message' => 'Gagal menyimpan Kabel: ' . $e->getMessage()]);
    }
    exit;
}

function network_mapping_get_kabel()
{
    header('Content-Type: application/json');
    $lang = network_mapping_load_language();
    
    $kabel_list = ORM::for_table('tbl_kabel')
        ->order_by_desc('id')
        ->find_array();
    
    $result = [];
    foreach ($kabel_list as $kabel) {
        $kabel['device_1_name'] = network_mapping_get_device_name($kabel['device_1_type'], $kabel['device_1_id']);
        $kabel['device_2_name'] = network_mapping_get_device_name($kabel['device_2_type'], $kabel['device_2_id']);
        $kabel['cable_color'] = network_mapping_get_cable_color($kabel['device_1_type'], $kabel['device_2_type']);
        $kabel['coordinates_path'] = json_decode($kabel['coordinates_path'], true) ?: [];
        $kabel['sambungan_data'] = json_decode($kabel['sambungan_data'], true) ?: [];
        $result[] = $kabel;
    }
    
    echo json_encode(['status' => 'success', 'data' => $result]);
    exit;
}

function network_mapping_delete_kabel()
{
    header('Content-Type: application/json');
    $lang = network_mapping_load_language();
    
    $kabel_id = _req('kabel_id');
    
    if (empty($kabel_id)) {
        echo json_encode(['status' => 'error', 'message' => 'ID Kabel tidak valid']);
        exit;
    }
    
    try {
        $kabel = ORM::for_table('tbl_kabel')->find_one($kabel_id);
        
        if (!$kabel) {
            echo json_encode(['status' => 'error', 'message' => 'Kabel tidak ditemukan']);
            exit;
        }
        
        // Simpan nama untuk response
        $kabel_name = $kabel->name ?: 'Kabel #' . $kabel->id;
        
        // Hapus Kabel dari database
        $kabel->delete();
        
        echo json_encode([
            'status' => 'success', 
            'message' => 'Kabel "' . $kabel_name . '" berhasil dihapus'
        ]);
    } catch (Exception $e) {
        echo json_encode(['status' => 'error', 'message' => 'Gagal menghapus Kabel: ' . $e->getMessage()]);
    }
    exit;
}

function network_mapping_check_kabel_exists()
{
    header('Content-Type: application/json');
    $lang = network_mapping_load_language();
    
    $device_1_type = _req('device_1_type');
    $device_1_id = _req('device_1_id');
    $device_2_type = _req('device_2_type');
    $device_2_id = _req('device_2_id');
    
    // Check for duplicate connection (both directions)
    $existing = ORM::for_table('tbl_kabel')
        ->where_raw("(
            (device_1_type = ? AND device_1_id = ? AND device_2_type = ? AND device_2_id = ?) OR
            (device_1_type = ? AND device_1_id = ? AND device_2_type = ? AND device_2_id = ?)
        )", [
            $device_1_type, $device_1_id, $device_2_type, $device_2_id,
            $device_2_type, $device_2_id, $device_1_type, $device_1_id
        ])
        ->find_one();
    
    echo json_encode([
        'status' => 'success',
        'exists' => $existing ? true : false,
        'message' => $existing ? 'Kabel antara kedua device ini sudah ada!' : ''
    ]);
    exit;
}

// ==================== FUNGSI CRUD TIANG ====================

function network_mapping_save_tiang()
{
    header('Content-Type: application/json');
    $lang = network_mapping_load_language();
    
    $name = _req('name');
    $coordinates = _req('coordinates');
    $tinggi = _req('panjang') ?: null;
    $tipe = _req('bahan') ?: 'Besi';
    $slack_kabel = _req('slack_kabel') ?: null;
    $status = _req('status') ?: 'Aktif';
    $address = _req('address');
    $description = _req('description');
    
    if (empty($name)) {
        echo json_encode(['status' => 'error', 'message' => $lang['php_tiang_name_required']]);
        exit;
    }
    
    if (empty($coordinates)) {
        echo json_encode(['status' => 'error', 'message' => $lang['php_location_not_selected']]);
        exit;
    }
    
    try {
        $tiang = ORM::for_table('tbl_tiang')->create();
        $tiang->name = $name;
        $tiang->coordinates = $coordinates;
        $tiang->tinggi = $tinggi;
        $tiang->tipe = $tipe;
        $tiang->slack_kabel = $slack_kabel;
        $tiang->status = $status;
        $tiang->address = $address;
        $tiang->description = $description;
        
        // Handle upload foto
        $foto_filename = network_mapping_upload_foto_tiang($name);
        if ($foto_filename) {
            $tiang->foto = $foto_filename;
        }
        
        $tiang->save();
        
        echo json_encode([
            'status' => 'success', 
            'message' => $lang['php_tiang_saved'],
            'data' => [
                'id' => $tiang->id(),
                'name' => $name,
                'coordinates' => $coordinates
            ]
        ]);
    } catch (Exception $e) {
        echo json_encode(['status' => 'error', 'message' => $lang['php_tiang_save_failed'] . ': ' . $e->getMessage()]);
    }
    exit;
}

function network_mapping_get_tiang()
{
    header('Content-Type: application/json');
    $lang = network_mapping_load_language();
    
    $tiang = ORM::for_table('tbl_tiang')
        ->where_not_equal('coordinates', '')
        ->find_array();
    
    echo json_encode(['status' => 'success', 'data' => $tiang]);
    exit;
}

function network_mapping_update_tiang()
{
    header('Content-Type: application/json');
    $lang = network_mapping_load_language();
    
    $tiang_id = _req('tiang_id');
    $name = _req('name');
    $coordinates = _req('coordinates');
    $tinggi = _req('panjang') ?: null;
    $tipe = _req('bahan') ?: 'Besi';
    $slack_kabel = _req('slack_kabel') ?: null;
    $status = _req('status') ?: 'Aktif';
    $address = _req('address');
    $description = _req('description');
    $hapus_foto = _req('hapus_foto');
    
    if (empty($tiang_id)) {
        echo json_encode(['status' => 'error', 'message' => $lang['php_tiang_id_invalid']]);
        exit;
    }
    
    if (empty($name)) {
        echo json_encode(['status' => 'error', 'message' => $lang['php_tiang_name_required']]);
        exit;
    }
    
    if (empty($coordinates)) {
        echo json_encode(['status' => 'error', 'message' => $lang['php_coordinates_required']]);
        exit;
    }
    
    try {
        $tiang = ORM::for_table('tbl_tiang')->find_one($tiang_id);
        
        if (!$tiang) {
            echo json_encode(['status' => 'error', 'message' => $lang['php_tiang_not_found']]);
            exit;
        }
        
        $old_name = $tiang->name;
        $old_foto = $tiang->foto;
        
        // Cek apakah ada file upload baru
        $ada_upload_baru = isset($_FILES['foto_tiang']) && $_FILES['foto_tiang']['error'] === UPLOAD_ERR_OK;
        
        // Handle hapus foto (hanya jika TIDAK ada upload baru)
        if ($hapus_foto == '1' && !empty($old_foto) && !$ada_upload_baru) {
            $old_foto_path = 'system/uploads/foto/tiang/' . $old_foto;
            if (file_exists($old_foto_path)) {
                unlink($old_foto_path);
            }
            $tiang->foto = null;
            $old_foto = null;
        }
        
        // Handle upload foto baru (swap foto)
        if ($ada_upload_baru) {
            // Hapus foto lama DULU sebelum upload baru (agar tidak conflict nama file)
            if (!empty($old_foto)) {
                $old_foto_path = 'system/uploads/foto/tiang/' . $old_foto;
                if (file_exists($old_foto_path)) {
                    unlink($old_foto_path);
                }
            }
            
            // Upload foto baru
            $foto_filename = network_mapping_upload_foto_tiang($name);
            if ($foto_filename) {
                $tiang->foto = $foto_filename;
            } else {
                // Upload gagal, set foto ke null
                $tiang->foto = null;
            }
        } else {
            // Tidak ada upload baru, cek apakah nama berubah untuk rename foto
            if ($old_name !== $name && !empty($tiang->foto)) {
                $new_foto_filename = network_mapping_rename_foto_tiang($tiang->foto, $name);
                if ($new_foto_filename) {
                    $tiang->foto = $new_foto_filename;
                }
            }
        }
        
        // Update data
        $tiang->name = $name;
        $tiang->coordinates = $coordinates;
        $tiang->tinggi = $tinggi;
        $tiang->tipe = $tipe;
        $tiang->slack_kabel = $slack_kabel;
        $tiang->status = $status;
        $tiang->address = $address;
        $tiang->description = $description;
        
        $tiang->save();
        
        echo json_encode([
            'status' => 'success', 
            'message' => $lang['php_tiang_updated'],
            'data' => [
                'id' => $tiang->id(),
                'name' => $name,
                'coordinates' => $coordinates,
                'foto' => $tiang->foto
            ]
        ]);
    } catch (Exception $e) {
        echo json_encode(['status' => 'error', 'message' => $lang['php_tiang_update_failed'] . ': ' . $e->getMessage()]);
    }
    exit;
}

function network_mapping_delete_tiang()
{
    header('Content-Type: application/json');
    $lang = network_mapping_load_language();
    
    $tiang_id = _req('tiang_id');
    
    if (empty($tiang_id)) {
        echo json_encode(['status' => 'error', 'message' => $lang['php_tiang_id_invalid']]);
        exit;
    }
    
    try {
        $tiang = ORM::for_table('tbl_tiang')->find_one($tiang_id);
        
        if (!$tiang) {
            echo json_encode(['status' => 'error', 'message' => $lang['php_tiang_not_found']]);
            exit;
        }
        
        // Simpan nama untuk response
        $tiang_name = $tiang->name;
        
        // Hapus foto jika ada
        if (!empty($tiang->foto)) {
            $foto_path = 'system/uploads/foto/tiang/' . $tiang->foto;
            if (file_exists($foto_path)) {
                unlink($foto_path);
            }
        }
        
        // Hapus Tiang dari database
        $tiang->delete();
        
        echo json_encode([
            'status' => 'success', 
            'message' => 'Tiang "' . $tiang_name . '" ' . $lang['php_deleted_successfully']
        ]);
    } catch (Exception $e) {
        echo json_encode(['status' => 'error', 'message' => $lang['php_tiang_delete_failed'] . ': ' . $e->getMessage()]);
    }
    exit;
}
function network_mapping_update_kabel()
{
    header('Content-Type: application/json');
    $lang = network_mapping_load_language();
    
    $kabel_id = _req('kabel_id');
    $name = _req('name');
    $panjang = _req('panjang') ?: 0;
    $jumlah_sambungan = _req('jumlah_sambungan') ?: 0;
    $sambungan_data = _req('sambungan_data'); // JSON string
    $description = _req('description');
    
    if (empty($kabel_id)) {
        echo json_encode(['status' => 'error', 'message' => $lang['php_kabel_id_invalid']]);
        exit;
    }
    
    try {
        $kabel = ORM::for_table('tbl_kabel')->find_one($kabel_id);
        
        if (!$kabel) {
            echo json_encode(['status' => 'error', 'message' => $lang['php_kabel_not_found']]);
            exit;
        }
        
        // Ambil jumlah sambungan lama untuk cek apakah perlu hapus foto
        $old_jumlah = intval($kabel->jumlah_sambungan);
        $new_jumlah = intval($jumlah_sambungan);
        
        // Jika jumlah sambungan berkurang, hapus foto sambungan yang dihapus
        if ($new_jumlah < $old_jumlah) {
            $upload_dir = 'system/uploads/foto/sambungan/';
            for ($i = $new_jumlah + 1; $i <= $old_jumlah; $i++) {
                $foto_pattern = $upload_dir . 'kabel_' . $kabel_id . '_sambungan_' . $i . '.*';
                $files = glob($foto_pattern);
                foreach ($files as $file) {
                    if (file_exists($file)) {
                        unlink($file);
                    }
                }
            }
        }
        
        // Update data
        $kabel->name = $name;
        $kabel->panjang = $panjang;
        $kabel->jumlah_sambungan = $jumlah_sambungan;
        $kabel->sambungan_data = $sambungan_data;
        $kabel->description = $description;
        
        $kabel->save();
        
        echo json_encode([
            'status' => 'success',
            'message' => $lang['php_kabel_updated']
        ]);
    } catch (Exception $e) {
        echo json_encode(['status' => 'error', 'message' => $lang['php_kabel_update_failed'] . ': ' . $e->getMessage()]);
    }
    exit;
}
// ==================== FUNGSI CRUD HOMEPASS ====================

function network_mapping_save_homepass()
{
    header('Content-Type: application/json');
    $lang = network_mapping_load_language();
    
    $name = _req('name');
    $coordinates = _req('coordinates');
    $address = _req('address');
    $phone = _req('phone');
    $status = _req('status') ?: 'Prospek';
    $kategori = _req('kategori') ?: 'Rumah';
    $catatan = _req('catatan');
    $surveyor = _req('surveyor');
    $tanggal_survey = _req('tanggal_survey') ?: date('Y-m-d');
    
    if (empty($coordinates)) {
        echo json_encode(['status' => 'error', 'message' => $lang['php_location_not_selected']]);
        exit;
    }
    
    try {
        $homepass = ORM::for_table('tbl_homepass')->create();
        $homepass->name = $name;
        $homepass->coordinates = $coordinates;
        $homepass->address = $address;
        $homepass->phone = $phone;
        $homepass->status = $status;
        $homepass->kategori = $kategori;
        $homepass->catatan = $catatan;
        $homepass->surveyor = $surveyor;
        $homepass->tanggal_survey = $tanggal_survey;
        
        // Handle upload foto
        $foto_filename = network_mapping_upload_foto_homepass($name ?: 'homepass_' . time());
        if ($foto_filename) {
            $homepass->foto = $foto_filename;
        }
        
        $homepass->save();
        
        echo json_encode([
            'status' => 'success', 
            'message' => $lang['php_homepass_saved'],
            'data' => [
                'id' => $homepass->id(),
                'name' => $name,
                'coordinates' => $coordinates
            ]
        ]);
    } catch (Exception $e) {
        echo json_encode(['status' => 'error', 'message' => $lang['php_homepass_save_failed'] . ': ' . $e->getMessage()]);
    }
    exit;
}

function network_mapping_get_homepass()
{
    header('Content-Type: application/json');
    $lang = network_mapping_load_language();
    
    $homepass = ORM::for_table('tbl_homepass')
        ->where_not_equal('coordinates', '')
        ->find_array();
    
    echo json_encode(['status' => 'success', 'data' => $homepass]);
    exit;
}

function network_mapping_update_homepass()
{
    header('Content-Type: application/json');
    $lang = network_mapping_load_language();
    
    $homepass_id = _req('homepass_id');
    $name = _req('name');
    $coordinates = _req('coordinates');
    $address = _req('address');
    $phone = _req('phone');
    $status = _req('status') ?: 'Prospek';
    $kategori = _req('kategori') ?: 'Rumah';
    $catatan = _req('catatan');
    $surveyor = _req('surveyor');
    $tanggal_survey = _req('tanggal_survey');
    $hapus_foto = _req('hapus_foto');
    
    if (empty($homepass_id)) {
        echo json_encode(['status' => 'error', 'message' => $lang['php_homepass_id_invalid']]);
        exit;
    }
    
    if (empty($coordinates)) {
        echo json_encode(['status' => 'error', 'message' => $lang['php_coordinates_required']]);
        exit;
    }
    
    try {
        $homepass = ORM::for_table('tbl_homepass')->find_one($homepass_id);
        
        if (!$homepass) {
            echo json_encode(['status' => 'error', 'message' => $lang['php_homepass_not_found']]);
            exit;
        }
        
        $old_name = $homepass->name;
        $old_foto = $homepass->foto;
        
        // Cek apakah ada file upload baru
        $ada_upload_baru = isset($_FILES['foto_homepass']) && $_FILES['foto_homepass']['error'] === UPLOAD_ERR_OK;
        
        // Handle hapus foto (hanya jika TIDAK ada upload baru)
        if ($hapus_foto == '1' && !empty($old_foto) && !$ada_upload_baru) {
            $old_foto_path = 'system/uploads/foto/homepass/' . $old_foto;
            if (file_exists($old_foto_path)) {
                unlink($old_foto_path);
            }
            $homepass->foto = null;
            $old_foto = null;
        }
        
        // Handle upload foto baru (swap foto)
        if ($ada_upload_baru) {
            // Hapus foto lama DULU sebelum upload baru
            if (!empty($old_foto)) {
                $old_foto_path = 'system/uploads/foto/homepass/' . $old_foto;
                if (file_exists($old_foto_path)) {
                    unlink($old_foto_path);
                }
            }
            
            // Upload foto baru
            $foto_filename = network_mapping_upload_foto_homepass($name ?: 'homepass_' . $homepass_id);
            if ($foto_filename) {
                $homepass->foto = $foto_filename;
            } else {
                $homepass->foto = null;
            }
        } else {
            // Tidak ada upload baru, cek apakah nama berubah untuk rename foto
            if ($old_name !== $name && !empty($homepass->foto)) {
                $new_foto_filename = network_mapping_rename_foto_homepass($homepass->foto, $name ?: 'homepass_' . $homepass_id);
                if ($new_foto_filename) {
                    $homepass->foto = $new_foto_filename;
                }
            }
        }
        
        // Update data
        $homepass->name = $name;
        $homepass->coordinates = $coordinates;
        $homepass->address = $address;
        $homepass->phone = $phone;
        $homepass->status = $status;
        $homepass->kategori = $kategori;
        $homepass->catatan = $catatan;
        $homepass->surveyor = $surveyor;
        $homepass->tanggal_survey = $tanggal_survey;
        
        $homepass->save();
        
        echo json_encode([
            'status' => 'success', 
            'message' => $lang['php_homepass_updated'],
            'data' => [
                'id' => $homepass->id(),
                'name' => $name,
                'coordinates' => $coordinates,
                'foto' => $homepass->foto
            ]
        ]);
    } catch (Exception $e) {
        echo json_encode(['status' => 'error', 'message' => $lang['php_homepass_update_failed'] . ': ' . $e->getMessage()]);
    }
    exit;
}

function network_mapping_delete_homepass()
{
    header('Content-Type: application/json');
    $lang = network_mapping_load_language();
    
    $homepass_id = _req('homepass_id');
    
    if (empty($homepass_id)) {
        echo json_encode(['status' => 'error', 'message' => $lang['php_homepass_id_invalid']]);
        exit;
    }
    
    try {
        $homepass = ORM::for_table('tbl_homepass')->find_one($homepass_id);
        
        if (!$homepass) {
            echo json_encode(['status' => 'error', 'message' => $lang['php_homepass_not_found']]);
            exit;
        }
        
        // Simpan nama untuk response
        $homepass_name = $homepass->name;
        
        // Hapus foto jika ada
        if (!empty($homepass->foto)) {
            $foto_path = 'system/uploads/foto/homepass/' . $homepass->foto;
            if (file_exists($foto_path)) {
                unlink($foto_path);
            }
        }
        
        // Hapus Homepass dari database
        $homepass->delete();
        
        echo json_encode([
            'status' => 'success', 
            'message' => 'Homepass "' . $homepass_name . '" ' . $lang['php_deleted_successfully']
        ]);
    } catch (Exception $e) {
        echo json_encode(['status' => 'error', 'message' => $lang['php_homepass_delete_failed'] . ': ' . $e->getMessage()]);
    }
    exit;
}
// ==================== FUNGSI CRUD ROUTER ====================

function network_mapping_update_router()
{
    header('Content-Type: application/json');
    $lang = network_mapping_load_language();
    
    $router_id = _req('router_id');
    $name = _req('name');
    $coordinates = _req('coordinates');
    $description = _req('description');
    $coverage = _req('coverage') ?: 1000;
    $enabled = isset($_REQUEST['enabled']) ? intval($_REQUEST['enabled']) : 1;
    
    if (empty($router_id)) {
        echo json_encode(['status' => 'error', 'message' => $lang['php_router_id_invalid']]);
        exit;
    }
    
    if (empty($name)) {
        echo json_encode(['status' => 'error', 'message' => $lang['php_router_name_required']]);
        exit;
    }
    
    if (empty($coordinates)) {
        echo json_encode(['status' => 'error', 'message' => $lang['php_coordinates_required']]);
        exit;
    }
    
    try {
        $router = ORM::for_table('tbl_routers')->find_one($router_id);
        
        if (!$router) {
            echo json_encode(['status' => 'error', 'message' => $lang['php_router_not_found']]);
            exit;
        }
        
        // Update data
        $router->name = $name;
        $router->coordinates = $coordinates;
        $router->description = $description;
        $router->coverage = $coverage;
        $router->enabled = $enabled;
        
        $router->save();
        
        echo json_encode([
            'status' => 'success', 
            'message' => $lang['php_router_updated']
        ]);
    } catch (Exception $e) {
        echo json_encode(['status' => 'error', 'message' => $lang['php_router_update_failed'] . ': ' . $e->getMessage()]);
    }
    exit;
}

function network_mapping_delete_router()
{
    header('Content-Type: application/json');
    $lang = network_mapping_load_language();
    
    $router_id = _req('router_id');
    
    if (empty($router_id)) {
        echo json_encode(['status' => 'error', 'message' => $lang['php_router_id_invalid']]);
        exit;
    }
    
    try {
        $router = ORM::for_table('tbl_routers')->find_one($router_id);
        
        if (!$router) {
            echo json_encode(['status' => 'error', 'message' => $lang['php_router_not_found']]);
            exit;
        }
        
        // Cek apakah ada OLT yang terhubung ke router ini
        $connected_olts = ORM::for_table('tbl_olt')
            ->where('parent_router_id', $router_id)
            ->count();
        
        if ($connected_olts > 0) {
            echo json_encode([
                'status' => 'error', 
                'message' => $lang['php_router_has_connected_olt_1'] . $connected_olts . $lang['php_router_has_connected_olt_2']
            ]);
            exit;
        }
        
        // Simpan nama untuk response
        $router_name = $router->name;
        
        // Hapus Router dari database
        $router->delete();
        
        echo json_encode([
            'status' => 'success', 
            'message' => 'Router "' . $router_name . '" ' . $lang['php_deleted_successfully']
        ]);
    } catch (Exception $e) {
       echo json_encode(['status' => 'error', 'message' => $lang['php_router_delete_failed'] . ': ' . $e->getMessage()]);
    }
    exit;
}

// ==================== FUNGSI CEK STATUS ONLINE CUSTOMER ====================

/*
 * DEPRECATED: Fungsi ini tidak diperlukan lagi karena status dibaca dari database
 * Status customer sekarang di-update oleh cron_customer_monitor.php
 */
function network_mapping_check_customers_online()
{
    header('Content-Type: application/json');
    $lang = network_mapping_load_language();
    
    // Fungsi ini sudah tidak digunakan
    // Status customer sekarang dibaca langsung dari tbl_customer_online_status
    // yang di-update oleh cron_customer_monitor.php setiap 1 menit
    
    echo json_encode([
        'status' => 'deprecated',
        'message' => 'This function is deprecated. Customer status is now read from database.'
    ]);
    exit;
}

// ==================== FUNGSI GET REALTIME TRAFFIC & PING CUSTOMER ====================

function network_mapping_get_customer_realtime()
{
    global $_app_stage, $DEVICE_PATH, $config;
    header('Content-Type: application/json');
    $lang = network_mapping_load_language();
    
    $customer_id = intval(_req('customer_id'));
    
    if (empty($customer_id)) {
        echo json_encode(['status' => 'error', 'message' => 'Customer ID tidak valid']);
        exit;
    }
    
    // Default response untuk offline
    $result = [
        'status' => 'success',
        'online' => false,
        'upload' => '0 bps',
        'download' => '0 bps',
        'ping' => 'Timeout',
        'uptime' => '-',
        'ip_address' => ''
    ];
    
    // Get customer data
    $customer = ORM::for_table('tbl_customers')->find_one($customer_id);
    if (!$customer) {
        echo json_encode($result);
        exit;
    }
    
    // Get recharge terbaru (termasuk yang isolir/off)
    $recharge = ORM::for_table('tbl_user_recharges')
        ->where('customer_id', $customer_id)
        ->order_by_desc('id')
        ->find_one();
    
    if (!$recharge) {
        echo json_encode($result);
        exit;
    }
    
    // Get plan
    $plan = ORM::for_table('tbl_plans')->find_one($recharge['plan_id']);
    if (!$plan) {
        echo json_encode($result);
        exit;
    }
    
    $dvc = Package::getDevice($plan);
    
    if ($_app_stage == 'Demo' || !file_exists($dvc)) {
        echo json_encode($result);
        exit;
    }
    
    require_once $dvc;
    
    try {
        ini_set('default_socket_timeout', 5);
        
        // Get router info
        $mikrotik = ORM::for_table('tbl_routers')->where('name', $plan['routers'])->find_one();
        if (!$mikrotik) {
            echo json_encode($result);
            exit;
        }
        
        // Connect to MikroTik
        $iport = explode(":", $mikrotik['ip_address']);
        $client = new PEAR2\Net\RouterOS\Client(
            $iport[0], 
            $mikrotik['username'], 
            $mikrotik['password'], 
            isset($iport[1]) ? $iport[1] : null
        );
        
        // Get username untuk PPPoE
        $pppoe_username = !empty($customer['pppoe_username']) ? $customer['pppoe_username'] : $customer['username'];
        
        // Cek PPP Active dan ambil traffic
        $printRequest = new PEAR2\Net\RouterOS\Request('/ppp/active/print');
        $printRequest->setQuery(PEAR2\Net\RouterOS\Query::where('name', $pppoe_username));
        $response = $client->sendSync($printRequest);
        
        $activeId = $response->getProperty('.id');
        
        if (empty($activeId)) {
            // Coba dengan username biasa jika pppoe_username tidak ditemukan
            $printRequest = new PEAR2\Net\RouterOS\Request('/ppp/active/print');
            $printRequest->setQuery(PEAR2\Net\RouterOS\Query::where('name', $customer['username']));
            $response = $client->sendSync($printRequest);
            $activeId = $response->getProperty('.id');
        }
        
        if (!empty($activeId)) {
            // Customer online - ambil data traffic
            $result['online'] = true;
            
            // Get traffic rate (upload/download)
            $uploadBytes = $response->getProperty('limit-bytes-out');
            $downloadBytes = $response->getProperty('limit-bytes-in');
            
            // Get uptime
            $uptime = $response->getProperty('uptime');
            $result['uptime'] = !empty($uptime) ? $uptime : '-';
            
            // Get address untuk ping dan IP display
            $customerAddress = $response->getProperty('address');
            $result['ip_address'] = !empty($customerAddress) ? $customerAddress : '';
            
            // Ambil traffic dari interface
            // Menggunakan monitoring untuk real-time rate
            $monitorRequest = new PEAR2\Net\RouterOS\Request('/interface/monitor-traffic');
            $monitorRequest->setArgument('interface', '<pppoe-' . $pppoe_username . '>');
            $monitorRequest->setArgument('once', '');
            
            try {
                $monitorResponse = $client->sendSync($monitorRequest);
                $rxBps = $monitorResponse->getProperty('rx-bits-per-second');
                $txBps = $monitorResponse->getProperty('tx-bits-per-second');
                
                // Dari perspektif MikroTik:
                // - rx = traffic masuk ke router = upload dari customer
                // - tx = traffic keluar dari router = download ke customer
                $result['download'] = network_mapping_format_bps($txBps);
                $result['upload'] = network_mapping_format_bps($rxBps);
            } catch (Exception $e) {
                // Fallback: coba ambil dari ppp active dengan cara lain
                $result['download'] = '0 bps';
                $result['upload'] = '0 bps';
            }
            
            // Ping ke customer
            if (!empty($customerAddress)) {
                try {
                    $pingRequest = new PEAR2\Net\RouterOS\Request('/ping');
                    $pingRequest->setArgument('address', $customerAddress);
                    $pingRequest->setArgument('count', '1');
                    $pingResponse = $client->sendSync($pingRequest);
                    
                    $pingTime = $pingResponse->getProperty('time');
                    if (!empty($pingTime)) {
                        $result['ping'] = $pingTime;
                    } else {
                        $status = $pingResponse->getProperty('status');
                        if ($status == 'timeout') {
                            $result['ping'] = 'Timeout';
                        } else {
                            $result['ping'] = 'Timeout';
                        }
                    }
                } catch (Exception $e) {
                    $result['ping'] = 'Timeout';
                }
            }
        }
        
    } catch (Exception $e) {
        // Error - return default offline values
        $result['online'] = false;
    }
    
    echo json_encode($result);
    exit;
}

// Format bytes per second ke human readable
function network_mapping_format_bps($bps)
{
    if (empty($bps) || $bps == 0) {
        return '0 bps';
    }
    
    $bps = intval($bps);
    
    if ($bps >= 1000000000) {
        return round($bps / 1000000000, 2) . ' Gbps';
    } elseif ($bps >= 1000000) {
        return round($bps / 1000000, 2) . ' Mbps';
    } elseif ($bps >= 1000) {
        return round($bps / 1000, 2) . ' Kbps';
    } else {
        return $bps . ' bps';
    }
}

// ==================== FUNGSI HELPER FOTO TIANG ====================

/**
 * Sanitize nama file - hapus karakter tidak valid
 */
function network_mapping_sanitize_filename($name)
{
    // Ganti karakter tidak valid dengan dash
    $sanitized = preg_replace('/[\/\\\\:*?"<>|\s]+/', '-', $name);
    // Hapus dash berlebih
    $sanitized = preg_replace('/-+/', '-', $sanitized);
    // Hapus dash di awal dan akhir
    $sanitized = trim($sanitized, '-');
    // Batasi panjang
    if (strlen($sanitized) > 100) {
        $sanitized = substr($sanitized, 0, 100);
    }
    return $sanitized;
}

/**
 * Upload foto tiang dengan resize otomatis
 * @param string $tiang_name Nama tiang untuk nama file
 * @return string|null Nama file jika berhasil, null jika gagal/tidak ada upload
 */
function network_mapping_upload_foto_tiang($tiang_name)
{
    // Cek apakah ada file yang diupload
    if (!isset($_FILES['foto_tiang']) || $_FILES['foto_tiang']['error'] === UPLOAD_ERR_NO_FILE) {
        return null;
    }
    
    $file = $_FILES['foto_tiang'];
    
    // Cek error upload
    if ($file['error'] !== UPLOAD_ERR_OK) {
        return null;
    }
    
    // Cek ukuran file (max 5MB)
    $max_size = 5 * 1024 * 1024; // 5MB
    if ($file['size'] > $max_size) {
        return null;
    }
    
    // Cek tipe file berdasarkan extension dan getimagesize
    $allowed_extensions = ['jpg', 'jpeg', 'png', 'webp'];
    $file_extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
    
    if (!in_array($file_extension, $allowed_extensions)) {
        return null;
    }
    
    // Validasi bahwa file benar-benar gambar menggunakan getimagesize
    $image_info = @getimagesize($file['tmp_name']);
    if ($image_info === false) {
        return null;
    }
    
    // Dapatkan mime type dari getimagesize
    $mime_type = $image_info['mime'];
    $allowed_mimes = ['image/jpeg', 'image/png', 'image/webp'];
    
    if (!in_array($mime_type, $allowed_mimes)) {
        return null;
    }
    
    // Buat folder jika belum ada
    $upload_dir = 'system/uploads/foto/tiang/';
    if (!is_dir($upload_dir)) {
        mkdir($upload_dir, 0755, true);
    }
    
    // Sanitize nama file
    $safe_name = network_mapping_sanitize_filename($tiang_name);
    $extension = 'jpg'; // Selalu simpan sebagai jpg setelah resize
    $filename = $safe_name . '.' . $extension;
    $filepath = $upload_dir . $filename;
    
    // Resize dan simpan gambar
    $resized = network_mapping_resize_image($file['tmp_name'], $mime_type, $filepath, 1920);
    
    if ($resized) {
        return $filename;
    }
    
    return null;
}

/**
 * Resize gambar dengan mempertahankan aspect ratio
 * @param string $source Path file sumber
 * @param string $mime_type MIME type file
 * @param string $destination Path file tujuan
 * @param int $max_dimension Maksimal lebar/tinggi
 * @return bool
 */
function network_mapping_resize_image($source, $mime_type, $destination, $max_dimension = 1920)
{
    // Load gambar berdasarkan tipe
    switch ($mime_type) {
        case 'image/jpeg':
        case 'image/jpg':
            $image = imagecreatefromjpeg($source);
            break;
        case 'image/png':
            $image = imagecreatefrompng($source);
            break;
        case 'image/webp':
            $image = imagecreatefromwebp($source);
            break;
        default:
            return false;
    }
    
    if (!$image) {
        return false;
    }
    
    // Dapatkan dimensi asli
    $orig_width = imagesx($image);
    $orig_height = imagesy($image);
    
    // Hitung dimensi baru dengan mempertahankan aspect ratio
    if ($orig_width > $max_dimension || $orig_height > $max_dimension) {
        if ($orig_width > $orig_height) {
            $new_width = $max_dimension;
            $new_height = intval($orig_height * ($max_dimension / $orig_width));
        } else {
            $new_height = $max_dimension;
            $new_width = intval($orig_width * ($max_dimension / $orig_height));
        }
    } else {
        // Tidak perlu resize, ukuran sudah kecil
        $new_width = $orig_width;
        $new_height = $orig_height;
    }
    
    // Buat gambar baru
    $new_image = imagecreatetruecolor($new_width, $new_height);
    
    // Preserve transparency untuk PNG
    if ($mime_type === 'image/png') {
        imagealphablending($new_image, false);
        imagesavealpha($new_image, true);
        $transparent = imagecolorallocatealpha($new_image, 255, 255, 255, 127);
        imagefilledrectangle($new_image, 0, 0, $new_width, $new_height, $transparent);
    } else {
        // Background putih untuk non-PNG
        $white = imagecolorallocate($new_image, 255, 255, 255);
        imagefilledrectangle($new_image, 0, 0, $new_width, $new_height, $white);
    }
    
    // Resize
    imagecopyresampled($new_image, $image, 0, 0, 0, 0, $new_width, $new_height, $orig_width, $orig_height);
    
    // Simpan sebagai JPEG dengan kualitas 85%
    $result = imagejpeg($new_image, $destination, 85);
    
    // Bersihkan memory
    imagedestroy($image);
    imagedestroy($new_image);
    
    return $result;
}

/**
 * Rename foto tiang ketika nama tiang berubah
 * @param string $old_filename Nama file lama
 * @param string $new_name Nama tiang baru
 * @return string|null Nama file baru jika berhasil, null jika gagal
 */
function network_mapping_rename_foto_tiang($old_filename, $new_name)
{
    $upload_dir = 'system/uploads/foto/tiang/';
    $old_path = $upload_dir . $old_filename;
    
    if (!file_exists($old_path)) {
        return null;
    }
    
    // Dapatkan extension dari file lama
    $extension = pathinfo($old_filename, PATHINFO_EXTENSION);
    
    // Buat nama file baru
    $safe_name = network_mapping_sanitize_filename($new_name);
    $new_filename = $safe_name . '.' . $extension;
    $new_path = $upload_dir . $new_filename;
    
    // Jika nama sama, tidak perlu rename
    if ($old_filename === $new_filename) {
        return $old_filename;
    }
    
    // Rename file
    if (rename($old_path, $new_path)) {
        return $new_filename;
    }
    
    return null;
}
// ==================== FUNGSI HELPER FOTO ODC ====================

/**
 * Upload foto ODC dengan resize otomatis
 * @param string $odc_name Nama ODC untuk nama file
 * @return string|null Nama file jika berhasil, null jika gagal/tidak ada upload
 */
function network_mapping_upload_foto_odc($odc_name)
{
    // Cek apakah ada file yang diupload
    if (!isset($_FILES['foto_odc']) || $_FILES['foto_odc']['error'] === UPLOAD_ERR_NO_FILE) {
        return null;
    }
    
    $file = $_FILES['foto_odc'];
    
    // Cek error upload
    if ($file['error'] !== UPLOAD_ERR_OK) {
        return null;
    }
    
    // Cek ukuran file (max 5MB)
    $max_size = 5 * 1024 * 1024; // 5MB
    if ($file['size'] > $max_size) {
        return null;
    }
    
    // Cek tipe file berdasarkan extension dan getimagesize
    $allowed_extensions = ['jpg', 'jpeg', 'png', 'webp'];
    $file_extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
    
    if (!in_array($file_extension, $allowed_extensions)) {
        return null;
    }
    
    // Validasi bahwa file benar-benar gambar menggunakan getimagesize
    $image_info = @getimagesize($file['tmp_name']);
    if ($image_info === false) {
        return null;
    }
    
    // Dapatkan mime type dari getimagesize
    $mime_type = $image_info['mime'];
    $allowed_mimes = ['image/jpeg', 'image/png', 'image/webp'];
    
    if (!in_array($mime_type, $allowed_mimes)) {
        return null;
    }
    
    // Buat folder jika belum ada
    $upload_dir = 'system/uploads/foto/odc/';
    if (!is_dir($upload_dir)) {
        mkdir($upload_dir, 0755, true);
    }
    
    // Sanitize nama file
    $safe_name = network_mapping_sanitize_filename($odc_name);
    $extension = 'jpg'; // Selalu simpan sebagai jpg setelah resize
    $filename = $safe_name . '.' . $extension;
    $filepath = $upload_dir . $filename;
    
    // Resize dan simpan gambar
    $resized = network_mapping_resize_image($file['tmp_name'], $mime_type, $filepath, 1920);
    
    if ($resized) {
        return $filename;
    }
    
    return null;
}

/**
 * Rename foto ODC ketika nama ODC berubah
 * @param string $old_filename Nama file lama
 * @param string $new_name Nama ODC baru
 * @return string|null Nama file baru jika berhasil, null jika gagal
 */
function network_mapping_rename_foto_odc($old_filename, $new_name)
{
    $upload_dir = 'system/uploads/foto/odc/';
    $old_path = $upload_dir . $old_filename;
    
    if (!file_exists($old_path)) {
        return null;
    }
    
    // Dapatkan extension dari file lama
    $extension = pathinfo($old_filename, PATHINFO_EXTENSION);
    
    // Buat nama file baru
    $safe_name = network_mapping_sanitize_filename($new_name);
    $new_filename = $safe_name . '.' . $extension;
    $new_path = $upload_dir . $new_filename;
    
    // Jika nama sama, tidak perlu rename
    if ($old_filename === $new_filename) {
        return $old_filename;
    }
    
    // Rename file
    if (rename($old_path, $new_path)) {
        return $new_filename;
    }
    
    return null;
}
// ==================== FUNGSI HELPER FOTO ODP ====================

/**
 * Upload foto ODP dengan resize otomatis
 * @param string $odp_name Nama ODP untuk nama file
 * @return string|null Nama file jika berhasil, null jika gagal/tidak ada upload
 */
function network_mapping_upload_foto_odp($odp_name)
{
    // Cek apakah ada file yang diupload
    if (!isset($_FILES['foto_odp']) || $_FILES['foto_odp']['error'] === UPLOAD_ERR_NO_FILE) {
        return null;
    }
    
    $file = $_FILES['foto_odp'];
    
    // Cek error upload
    if ($file['error'] !== UPLOAD_ERR_OK) {
        return null;
    }
    
    // Cek ukuran file (max 5MB)
    $max_size = 5 * 1024 * 1024; // 5MB
    if ($file['size'] > $max_size) {
        return null;
    }
    
    // Cek tipe file berdasarkan extension dan getimagesize
    $allowed_extensions = ['jpg', 'jpeg', 'png', 'webp'];
    $file_extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
    
    if (!in_array($file_extension, $allowed_extensions)) {
        return null;
    }
    
    // Validasi bahwa file benar-benar gambar menggunakan getimagesize
    $image_info = @getimagesize($file['tmp_name']);
    if ($image_info === false) {
        return null;
    }
    
    // Dapatkan mime type dari getimagesize
    $mime_type = $image_info['mime'];
    $allowed_mimes = ['image/jpeg', 'image/png', 'image/webp'];
    
    if (!in_array($mime_type, $allowed_mimes)) {
        return null;
    }
    
    // Buat folder jika belum ada
    $upload_dir = 'system/uploads/foto/odp/';
    if (!is_dir($upload_dir)) {
        mkdir($upload_dir, 0755, true);
    }
    
    // Sanitize nama file
    $safe_name = network_mapping_sanitize_filename($odp_name);
    $extension = 'jpg'; // Selalu simpan sebagai jpg setelah resize
    $filename = $safe_name . '.' . $extension;
    $filepath = $upload_dir . $filename;
    
    // Resize dan simpan gambar
    $resized = network_mapping_resize_image($file['tmp_name'], $mime_type, $filepath, 1920);
    
    if ($resized) {
        return $filename;
    }
    
    return null;
}

/**
 * Rename foto ODP ketika nama ODP berubah
 * @param string $old_filename Nama file lama
 * @param string $new_name Nama ODP baru
 * @return string|null Nama file baru jika berhasil, null jika gagal
 */
function network_mapping_rename_foto_odp($old_filename, $new_name)
{
    $upload_dir = 'system/uploads/foto/odp/';
    $old_path = $upload_dir . $old_filename;
    
    if (!file_exists($old_path)) {
        return null;
    }
    
    // Dapatkan extension dari file lama
    $extension = pathinfo($old_filename, PATHINFO_EXTENSION);
    
    // Buat nama file baru
    $safe_name = network_mapping_sanitize_filename($new_name);
    $new_filename = $safe_name . '.' . $extension;
    $new_path = $upload_dir . $new_filename;
    
    // Jika nama sama, tidak perlu rename
    if ($old_filename === $new_filename) {
        return $old_filename;
    }
    
    // Rename file
    if (rename($old_path, $new_path)) {
        return $new_filename;
    }
    
    return null;
}
// ==================== FUNGSI HELPER FOTO HOMEPASS ====================

/**
 * Upload foto Homepass dengan resize otomatis
 * @param string $homepass_name Nama Homepass untuk nama file
 * @return string|null Nama file jika berhasil, null jika gagal/tidak ada upload
 */
function network_mapping_upload_foto_homepass($homepass_name)
{
    // Cek apakah ada file yang diupload
    if (!isset($_FILES['foto_homepass']) || $_FILES['foto_homepass']['error'] === UPLOAD_ERR_NO_FILE) {
        return null;
    }
    
    $file = $_FILES['foto_homepass'];
    
    // Cek error upload
    if ($file['error'] !== UPLOAD_ERR_OK) {
        return null;
    }
    
    // Cek ukuran file (max 5MB)
    $max_size = 5 * 1024 * 1024; // 5MB
    if ($file['size'] > $max_size) {
        return null;
    }
    
    // Cek tipe file berdasarkan extension dan getimagesize
    $allowed_extensions = ['jpg', 'jpeg', 'png', 'webp'];
    $file_extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
    
    if (!in_array($file_extension, $allowed_extensions)) {
        return null;
    }
    
    // Validasi bahwa file benar-benar gambar menggunakan getimagesize
    $image_info = @getimagesize($file['tmp_name']);
    if ($image_info === false) {
        return null;
    }
    
    // Dapatkan mime type dari getimagesize
    $mime_type = $image_info['mime'];
    $allowed_mimes = ['image/jpeg', 'image/png', 'image/webp'];
    
    if (!in_array($mime_type, $allowed_mimes)) {
        return null;
    }
    
    // Buat folder jika belum ada
    $upload_dir = 'system/uploads/foto/homepass/';
    if (!is_dir($upload_dir)) {
        mkdir($upload_dir, 0755, true);
    }
    
    // Sanitize nama file
    $safe_name = network_mapping_sanitize_filename($homepass_name ?: 'homepass_' . time());
    $extension = 'jpg'; // Selalu simpan sebagai jpg setelah resize
    $filename = $safe_name . '.' . $extension;
    $filepath = $upload_dir . $filename;
    
    // Resize dan simpan gambar
    $resized = network_mapping_resize_image($file['tmp_name'], $mime_type, $filepath, 1920);
    
    if ($resized) {
        return $filename;
    }
    
    return null;
}

/**
 * Rename foto Homepass ketika nama Homepass berubah
 * @param string $old_filename Nama file lama
 * @param string $new_name Nama Homepass baru
 * @return string|null Nama file baru jika berhasil, null jika gagal
 */
function network_mapping_rename_foto_homepass($old_filename, $new_name)
{
    $upload_dir = 'system/uploads/foto/homepass/';
    $old_path = $upload_dir . $old_filename;
    
    if (!file_exists($old_path)) {
        return null;
    }
    
    // Dapatkan extension dari file lama
    $extension = pathinfo($old_filename, PATHINFO_EXTENSION);
    
    // Buat nama file baru
    $safe_name = network_mapping_sanitize_filename($new_name ?: 'homepass_' . time());
    $new_filename = $safe_name . '.' . $extension;
    $new_path = $upload_dir . $new_filename;
    
    // Jika nama sama, tidak perlu rename
    if ($old_filename === $new_filename) {
        return $old_filename;
    }
    
    // Rename file
    if (rename($old_path, $new_path)) {
        return $new_filename;
    }
    
    return null;
}

// ==================== FUNGSI HELPER FOTO SAMBUNGAN KABEL ====================

/**
 * Upload foto Sambungan Kabel via AJAX
 */
function network_mapping_upload_foto_sambungan_ajax()
{
    header('Content-Type: application/json');
    $lang = network_mapping_load_language();
    
    $kabel_id = _req('kabel_id');
    $sambungan_index = _req('sambungan_index');
    
    if (empty($kabel_id) || empty($sambungan_index)) {
        echo json_encode(['status' => 'error', 'message' => 'Parameter tidak lengkap']);
        exit;
    }
    
    // Cek apakah ada file yang diupload
    if (!isset($_FILES['foto_sambungan']) || $_FILES['foto_sambungan']['error'] === UPLOAD_ERR_NO_FILE) {
        echo json_encode(['status' => 'error', 'message' => 'Tidak ada file yang diupload']);
        exit;
    }
    
    $file = $_FILES['foto_sambungan'];
    
    // Cek error upload
    if ($file['error'] !== UPLOAD_ERR_OK) {
        echo json_encode(['status' => 'error', 'message' => 'Error upload: ' . $file['error']]);
        exit;
    }
    
    // Cek ukuran file (max 5MB)
    $max_size = 5 * 1024 * 1024;
    if ($file['size'] > $max_size) {
        echo json_encode(['status' => 'error', 'message' => 'Ukuran foto maksimal 5MB']);
        exit;
    }
    
    // Cek tipe file
    $allowed_extensions = ['jpg', 'jpeg', 'png', 'webp'];
    $file_extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
    
    if (!in_array($file_extension, $allowed_extensions)) {
        echo json_encode(['status' => 'error', 'message' => 'Format foto harus JPG, PNG, atau WebP']);
        exit;
    }
    
    // Validasi gambar
    $image_info = @getimagesize($file['tmp_name']);
    if ($image_info === false) {
        echo json_encode(['status' => 'error', 'message' => 'File bukan gambar valid']);
        exit;
    }
    
    $mime_type = $image_info['mime'];
    $allowed_mimes = ['image/jpeg', 'image/png', 'image/webp'];
    
    if (!in_array($mime_type, $allowed_mimes)) {
        echo json_encode(['status' => 'error', 'message' => 'MIME type tidak diizinkan']);
        exit;
    }
    
    // Buat folder jika belum ada
    $upload_dir = 'system/uploads/foto/sambungan/';
    if (!is_dir($upload_dir)) {
        mkdir($upload_dir, 0755, true);
    }
    
    // Hapus foto lama jika ada (dengan extension apapun)
    $old_pattern = $upload_dir . 'kabel_' . $kabel_id . '_sambungan_' . $sambungan_index . '.*';
    $old_files = glob($old_pattern);
    foreach ($old_files as $old_file) {
        if (file_exists($old_file)) {
            unlink($old_file);
        }
    }
    
    // Nama file baru
    $filename = 'kabel_' . $kabel_id . '_sambungan_' . $sambungan_index . '.jpg';
    $filepath = $upload_dir . $filename;
    
    // Resize dan simpan gambar
    $resized = network_mapping_resize_image($file['tmp_name'], $mime_type, $filepath, 1920);
    
    if ($resized) {
        // Generate URL
        $base_url = str_replace('?_route=', '', U);
        $base_url = rtrim($base_url, '/') . '/';
        $foto_timestamp = filemtime($filepath);
        $foto_url = $base_url . $upload_dir . $filename . '?v=' . $foto_timestamp;
        
        echo json_encode([
            'status' => 'success',
            'message' => 'Foto berhasil diupload',
            'data' => [
                'foto' => $filename,
                'foto_url' => $foto_url
            ]
        ]);
    } else {
        echo json_encode(['status' => 'error', 'message' => 'Gagal menyimpan foto']);
    }
    exit;
}

/**
 * Hapus foto Sambungan Kabel via AJAX
 */
function network_mapping_delete_foto_sambungan_ajax()
{
    header('Content-Type: application/json');
    $lang = network_mapping_load_language();
    
    $kabel_id = _req('kabel_id');
    $sambungan_index = _req('sambungan_index');
    
    if (empty($kabel_id) || empty($sambungan_index)) {
        echo json_encode(['status' => 'error', 'message' => 'Parameter tidak lengkap']);
        exit;
    }
    
    $upload_dir = 'system/uploads/foto/sambungan/';
    $foto_pattern = $upload_dir . 'kabel_' . $kabel_id . '_sambungan_' . $sambungan_index . '.*';
    $files = glob($foto_pattern);
    
    $deleted = false;
    foreach ($files as $file) {
        if (file_exists($file)) {
            unlink($file);
            $deleted = true;
        }
    }
    
    if ($deleted) {
        echo json_encode([
            'status' => 'success',
            'message' => 'Foto berhasil dihapus'
        ]);
    } else {
        echo json_encode([
            'status' => 'success',
            'message' => 'Tidak ada foto untuk dihapus'
        ]);
    }
    exit;
}

// ==================== FUNGSI CUSTOMER ODP & FOTO LOKASI ====================

/**
 * Get daftar ODP untuk dropdown
 */
function network_mapping_get_odp_list()
{
    header('Content-Type: application/json');
    $lang = network_mapping_load_language();
    
    try {
        $odp_list = ORM::for_table('tbl_odp')
            ->select('id')
            ->select('name')
            ->order_by_asc('name')
            ->find_array();
        
        echo json_encode([
            'status' => 'success',
            'data' => $odp_list
        ]);
    } catch (Exception $e) {
        echo json_encode([
            'status' => 'error',
            'message' => 'Gagal mengambil data ODP: ' . $e->getMessage()
        ]);
    }
    exit;
}

/**
 * Get ODP dan Foto Lokasi customer
 */
function network_mapping_get_customer_odp_foto()
{
    header('Content-Type: application/json');
    $lang = network_mapping_load_language();
    
    $customer_id = _req('customer_id');
    
    if (empty($customer_id)) {
        echo json_encode(['status' => 'error', 'message' => 'Customer ID tidak valid']);
        exit;
    }
    
    try {
        $customer = ORM::for_table('tbl_customers')->find_one($customer_id);
        
        if (!$customer) {
            echo json_encode(['status' => 'error', 'message' => 'Customer tidak ditemukan']);
            exit;
        }
        
        // Get ODP info
        $odp_id = $customer['odp_id'];
        $odp_name = '';
        if (!empty($odp_id)) {
            $odp = ORM::for_table('tbl_odp')->find_one($odp_id);
            if ($odp) {
                $odp_name = $odp['name'];
            }
        }
        
        // Get Foto Lokasi URL
        $foto_lokasi = $customer['foto_lokasi'];
        $foto_lokasi_url = '';
        if (!empty($foto_lokasi)) {
            $foto_path = 'system/uploads/foto/customers/' . $foto_lokasi;
            if (file_exists($foto_path)) {
                $base_url = str_replace('?_route=', '', U);
                $base_url = rtrim($base_url, '/') . '/';
                $foto_timestamp = filemtime($foto_path);
                $foto_lokasi_url = $base_url . $foto_path . '?v=' . $foto_timestamp;
            }
        }
        
        echo json_encode([
            'status' => 'success',
            'data' => [
                'customer_id' => $customer_id,
                'odp_id' => $odp_id,
                'odp_name' => $odp_name,
                'foto_lokasi' => $foto_lokasi,
                'foto_lokasi_url' => $foto_lokasi_url
            ]
        ]);
    } catch (Exception $e) {
        echo json_encode([
            'status' => 'error',
            'message' => 'Gagal mengambil data: ' . $e->getMessage()
        ]);
    }
    exit;
}

/**
 * Save ODP untuk customer
 */
function network_mapping_save_customer_odp()
{
    header('Content-Type: application/json');
    $lang = network_mapping_load_language();
    
    $customer_id = _req('customer_id');
    $odp_id = _req('odp_id');
    
    if (empty($customer_id)) {
        echo json_encode(['status' => 'error', 'message' => 'Customer ID tidak valid']);
        exit;
    }
    
    try {
        $customer = ORM::for_table('tbl_customers')->find_one($customer_id);
        
        if (!$customer) {
            echo json_encode(['status' => 'error', 'message' => 'Customer tidak ditemukan']);
            exit;
        }
        
        $customer->odp_id = !empty($odp_id) ? $odp_id : null;
        $customer->save();
        
        // Get ODP name untuk response
        $odp_name = '';
        if (!empty($odp_id)) {
            $odp = ORM::for_table('tbl_odp')->find_one($odp_id);
            if ($odp) {
                $odp_name = $odp['name'];
            }
        }
        
        echo json_encode([
            'status' => 'success',
            'message' => 'ODP berhasil disimpan',
            'data' => [
                'odp_id' => $odp_id,
                'odp_name' => $odp_name
            ]
        ]);
    } catch (Exception $e) {
        echo json_encode([
            'status' => 'error',
            'message' => 'Gagal menyimpan ODP: ' . $e->getMessage()
        ]);
    }
    exit;
}

/**
 * Upload foto lokasi customer
 */
function network_mapping_upload_customer_foto_lokasi()
{
    header('Content-Type: application/json');
    $lang = network_mapping_load_language();
    
    $customer_id = _req('customer_id');
    
    if (empty($customer_id)) {
        echo json_encode(['status' => 'error', 'message' => 'Customer ID tidak valid']);
        exit;
    }
    
    // Cek apakah ada file yang diupload
    if (!isset($_FILES['foto_lokasi']) || $_FILES['foto_lokasi']['error'] === UPLOAD_ERR_NO_FILE) {
        echo json_encode(['status' => 'error', 'message' => 'Tidak ada file yang diupload']);
        exit;
    }
    
    $file = $_FILES['foto_lokasi'];
    
    // Cek error upload
    if ($file['error'] !== UPLOAD_ERR_OK) {
        echo json_encode(['status' => 'error', 'message' => 'Error upload: ' . $file['error']]);
        exit;
    }
    
    // Cek ukuran file (max 5MB)
    $max_size = 5 * 1024 * 1024;
    if ($file['size'] > $max_size) {
        echo json_encode(['status' => 'error', 'message' => 'Ukuran foto maksimal 5MB']);
        exit;
    }
    
    // Validasi bahwa file benar-benar gambar
    $image_info = @getimagesize($file['tmp_name']);
    if ($image_info === false) {
        echo json_encode(['status' => 'error', 'message' => 'File bukan gambar valid']);
        exit;
    }
    
    $mime_type = $image_info['mime'];
    $allowed_mimes = ['image/jpeg', 'image/png', 'image/webp'];
    
    if (!in_array($mime_type, $allowed_mimes)) {
        echo json_encode(['status' => 'error', 'message' => 'Format foto harus JPG, PNG, atau WebP']);
        exit;
    }
    
    try {
        $customer = ORM::for_table('tbl_customers')->find_one($customer_id);
        
        if (!$customer) {
            echo json_encode(['status' => 'error', 'message' => 'Customer tidak ditemukan']);
            exit;
        }
        
        // Buat folder jika belum ada
        $upload_dir = 'system/uploads/foto/customers/';
        if (!is_dir($upload_dir)) {
            mkdir($upload_dir, 0755, true);
        }
        
        // Hapus foto lama jika ada
        if (!empty($customer['foto_lokasi'])) {
            $old_foto_path = $upload_dir . $customer['foto_lokasi'];
            if (file_exists($old_foto_path)) {
                unlink($old_foto_path);
            }
        }
        
        // Sanitize nama file
        $safe_name = preg_replace('/[^a-zA-Z0-9_-]/', '_', $customer['username']);
        $safe_name = substr($safe_name, 0, 50);
        if (empty($safe_name)) {
            $safe_name = 'customer_' . $customer_id;
        }
        $filename = $safe_name . '_' . time() . '.jpg';
        $filepath = $upload_dir . $filename;
        
        // Resize dan simpan gambar
        $resized = network_mapping_resize_image($file['tmp_name'], $mime_type, $filepath, 1920);
        
        if ($resized) {
            // Update database
            $customer->foto_lokasi = $filename;
            $customer->save();
            
            // Generate URL
            $base_url = str_replace('?_route=', '', U);
            $base_url = rtrim($base_url, '/') . '/';
            $foto_timestamp = filemtime($filepath);
            $foto_url = $base_url . $filepath . '?v=' . $foto_timestamp;
            
            echo json_encode([
                'status' => 'success',
                'message' => 'Foto lokasi berhasil diupload',
                'data' => [
                    'foto_lokasi' => $filename,
                    'foto_lokasi_url' => $foto_url
                ]
            ]);
        } else {
            echo json_encode(['status' => 'error', 'message' => 'Gagal menyimpan foto']);
        }
    } catch (Exception $e) {
        echo json_encode([
            'status' => 'error',
            'message' => 'Gagal upload foto: ' . $e->getMessage()
        ]);
    }
    exit;
}

/**
 * Hapus foto lokasi customer
 */
function network_mapping_delete_customer_foto_lokasi()
{
    header('Content-Type: application/json');
    $lang = network_mapping_load_language();
    
    $customer_id = _req('customer_id');
    
    if (empty($customer_id)) {
        echo json_encode(['status' => 'error', 'message' => 'Customer ID tidak valid']);
        exit;
    }
    
    try {
        $customer = ORM::for_table('tbl_customers')->find_one($customer_id);
        
        if (!$customer) {
            echo json_encode(['status' => 'error', 'message' => 'Customer tidak ditemukan']);
            exit;
        }
        
        // Hapus file foto jika ada
        if (!empty($customer['foto_lokasi'])) {
            $upload_dir = 'system/uploads/foto/customers/';
            $foto_path = $upload_dir . $customer['foto_lokasi'];
            if (file_exists($foto_path)) {
                unlink($foto_path);
            }
            
            // Update database
            $customer->foto_lokasi = null;
            $customer->save();
            
            echo json_encode([
                'status' => 'success',
                'message' => 'Foto lokasi berhasil dihapus'
            ]);
        } else {
            echo json_encode([
                'status' => 'success',
                'message' => 'Tidak ada foto untuk dihapus'
            ]);
        }
    } catch (Exception $e) {
        echo json_encode([
            'status' => 'error',
            'message' => 'Gagal menghapus foto: ' . $e->getMessage()
        ]);
    }
    exit;
}

/**
 * Add Customer dengan ODP dan Foto Lokasi
 * Endpoint pengganti customers/add-post
 */
function network_mapping_add_customer_full()
{
    global $config, $admin;
    
    // Validasi CSRF
    $csrf_token = _post('csrf_token');
    if (!Csrf::check($csrf_token)) {
        r2(getUrl('customers/add'), 'e', Lang::T('Invalid or Expired CSRF Token') . ".");
    }
    
    // Ambil data dari form
    $username = alphanumeric(_post('username'), ":+_.@-");
    $fullname = _post('fullname');
    $password = trim(_post('password'));
    $pppoe_username = trim(_post('pppoe_username'));
    $pppoe_password = trim(_post('pppoe_password'));
    $pppoe_ip = trim(_post('pppoe_ip'));
    $email = _post('email');
    $address = _post('address');
    $phonenumber = _post('phonenumber');
    $service_type = _post('service_type');
    $account_type = _post('account_type');
    $coordinates = _post('coordinates');
    $city = _post('city');
    $district = _post('district');
    $state = _post('state');
    $zip = _post('zip');
    
    // Data ODP
    $odp_id = _post('odp_id');
    
    // Custom fields
    $custom_field_names = isset($_POST['custom_field_name']) ? (array) $_POST['custom_field_name'] : [];
    $custom_field_values = isset($_POST['custom_field_value']) ? (array) $_POST['custom_field_value'] : [];
    
    // Validasi
    $msg = '';
    if (Validator::Length($username, 55, 2) == false) {
        $msg .= 'Username should be between 3 to 54 characters' . '<br>';
    }
    if (Validator::Length($fullname, 36, 1) == false) {
        $msg .= 'Full Name should be between 2 to 25 characters' . '<br>';
    }
    if (!Validator::Length($password, 36, 2)) {
        $msg .= 'Password should be between 3 to 35 characters' . '<br>';
    }
    
    // Cek username sudah ada atau belum
    $existing = ORM::for_table('tbl_customers')->where('username', $username)->find_one();
    if ($existing) {
        $msg .= Lang::T('Account already axist') . '<br>';
    }
    
    if ($msg == '') {
        // Simpan customer
        $d = ORM::for_table('tbl_customers')->create();
        $d->username = $username;
        $d->password = $password;
        $d->pppoe_username = $pppoe_username;
        $d->pppoe_password = $pppoe_password;
        $d->pppoe_ip = $pppoe_ip;
        $d->email = $email;
        $d->account_type = $account_type;
        $d->fullname = $fullname;
        $d->address = $address;
        $d->created_by = $admin['id'];
        $d->phonenumber = Lang::phoneFormat($phonenumber);
        $d->service_type = $service_type;
        $d->coordinates = $coordinates;
        $d->city = $city;
        $d->district = $district;
        $d->state = $state;
        $d->zip = $zip;
        
        // Simpan ODP
        $d->odp_id = !empty($odp_id) ? $odp_id : null;
        
        // Handle upload foto lokasi
        if (!empty($_FILES['foto_lokasi']['name']) && $_FILES['foto_lokasi']['error'] === UPLOAD_ERR_OK) {
            $foto_filename = network_mapping_upload_customer_foto($username);
            if ($foto_filename) {
                $d->foto_lokasi = $foto_filename;
            }
        }
        
        $d->save();
        
        // Ambil customer ID yang baru dibuat
        $customerId = $d->id();
        
        // Simpan custom fields
        if (!empty($custom_field_names) && !empty($custom_field_values)) {
            $totalFields = min(count($custom_field_names), count($custom_field_values));
            for ($i = 0; $i < $totalFields; $i++) {
                $name = $custom_field_names[$i];
                $value = $custom_field_values[$i];
                
                if (!empty($name)) {
                    $customField = ORM::for_table('tbl_customers_fields')->create();
                    $customField->customer_id = $customerId;
                    $customField->field_name = $name;
                    $customField->field_value = $value;
                    $customField->save();
                }
            }
        }

        // Simpan special fields: Invoice dan Expired Date
        $specialFields = ['Invoice', 'Expired Date'];
        foreach ($specialFields as $sfName) {
            if (isset($_POST['custom_fields'][$sfName]) && $_POST['custom_fields'][$sfName] !== '') {
                $newField = ORM::for_table('tbl_customers_fields')->create();
                $newField->set('customer_id', $customerId);
                $newField->set('field_name', $sfName);
                $newField->set('field_value', $_POST['custom_fields'][$sfName]);
                $newField->save();
            }
        }

        // Auto-recharge paket PPPoE jika dipilih
        $plan_id = _post('plan_id');
        if (!empty($plan_id)) {
            $plan = ORM::for_table('tbl_plans')->where('enabled', 1)->find_one($plan_id);
            if ($plan) {
                $server = $plan['routers'];
                if ($plan['is_radius'] == '1') {
                    $server = 'Radius';
                }
                Package::rechargeUser($customerId, $server, $plan['id'], 'Cash', $admin['fullname']);
            }
        }
        
        // Kirim welcome message jika diaktifkan
        if (isset($_POST['send_welcome_message']) && $_POST['send_welcome_message'] == true) {
            $welcomeMessage = Lang::getNotifText('welcome_message');
            $welcomeMessage = str_replace('[[company]]', $config['CompanyName'], $welcomeMessage);
            $welcomeMessage = str_replace('[[name]]', $d['fullname'], $welcomeMessage);
            $welcomeMessage = str_replace('[[username]]', $d['username'], $welcomeMessage);
            $welcomeMessage = str_replace('[[password]]', $d['password'], $welcomeMessage);
            $welcomeMessage = str_replace('[[url]]', APP_URL . '/?_route=login', $welcomeMessage);
            
            $subject = "Welcome to " . $config['CompanyName'];
            
            if (isset($_POST['sms'])) {
                try {
                    Message::sendSMS($d['phonenumber'], $welcomeMessage);
                } catch (Exception $e) {
                    _log("Failed to send welcome message via SMS: " . $e->getMessage());
                }
            }
            if (isset($_POST['wa'])) {
                try {
                    Message::sendWhatsapp($d['phonenumber'], $welcomeMessage);
                } catch (Exception $e) {
                    _log("Failed to send welcome message via WhatsApp: " . $e->getMessage());
                }
            }
            if (isset($_POST['mail'])) {
                try {
                    Message::sendEmail($d['email'], $subject, $welcomeMessage, $d['email']);
                } catch (Exception $e) {
                    _log("Failed to send welcome message via Email: " . $e->getMessage());
                }
            }
            if (isset($_POST['inbox'])) {
                try {
                    Message::addToInbox($customerId, $subject, $welcomeMessage, 'Admin');
                } catch (Exception $e) {
                    _log("Failed to send welcome message via Inbox: " . $e->getMessage());
                }
            }
        }
        
        r2(getUrl('customers/list'), 's', Lang::T('Account Created Successfully'));
    } else {
        r2(getUrl('customers/add'), 'e', $msg);
    }
}

/**
 * Upload foto lokasi untuk customer baru
 */
function network_mapping_upload_customer_foto($username)
{
    $file = $_FILES['foto_lokasi'];
    
    // Cek ukuran file (max 5MB)
    $max_size = 5 * 1024 * 1024;
    if ($file['size'] > $max_size) {
        return null;
    }
    
    // Validasi gambar
    $image_info = @getimagesize($file['tmp_name']);
    if ($image_info === false) {
        return null;
    }
    
    $mime_type = $image_info['mime'];
    $allowed_mimes = ['image/jpeg', 'image/png', 'image/webp'];
    
    if (!in_array($mime_type, $allowed_mimes)) {
        return null;
    }
    
    // Buat folder jika belum ada
    $upload_dir = 'system/uploads/foto/customers/';
    if (!is_dir($upload_dir)) {
        mkdir($upload_dir, 0755, true);
    }
    
    // Sanitize nama file
    $safe_name = preg_replace('/[^a-zA-Z0-9_-]/', '_', $username);
    $safe_name = substr($safe_name, 0, 50);
    if (empty($safe_name)) {
        $safe_name = 'customer_' . time();
    }
    $filename = $safe_name . '_' . time() . '.jpg';
    $filepath = $upload_dir . $filename;
    
    // Resize dan simpan gambar
    $resized = network_mapping_resize_image($file['tmp_name'], $mime_type, $filepath, 1920);
    
    if ($resized) {
        return $filename;
    }
    
    return null;
}

/**
 * Hook: Hapus foto lokasi saat customer dihapus
 */
function network_mapping_hook_delete_customer($args = [])
{
    global $routes;
    
    // Ambil customer ID dari URL
    $customer_id = $routes['2'];
    
    if (empty($customer_id)) {
        return false;
    }
    
    // Ambil data customer
    $customer = ORM::for_table('tbl_customers')->find_one($customer_id);
    
    if ($customer && !empty($customer['foto_lokasi'])) {
        $upload_dir = 'system/uploads/foto/customers/';
        $foto_path = $upload_dir . $customer['foto_lokasi'];
        
        // Hapus file foto jika ada
        if (file_exists($foto_path)) {
            unlink($foto_path);
        }
    }
    
    return true;
}

// ============================================
// UNINSTALL & UPDATE FUNCTIONS
// ============================================

/**
 * Get GitHub token from database
 */
function network_mapping_get_github_token()
{
    $config = ORM::for_table('tbl_appconfig')->where('setting', 'github_token')->find_one();
    return $config ? $config->value : null;
}

/**
 * Recursively delete directory for uninstall
 */
function network_mapping_delete_directory($dir)
{
    if (!is_dir($dir)) return true;
    
    $files = array_diff(scandir($dir), ['.', '..']);
    foreach ($files as $file) {
        $path = $dir . '/' . $file;
        is_dir($path) ? network_mapping_delete_directory($path) : unlink($path);
    }
    return rmdir($dir);
}

/**
 * Run complete uninstallation
 */
function network_mapping_run_uninstall()
{
    global $admin;
    
    // Only SuperAdmin can uninstall
    $admin = Admin::_info();
    if ($admin['user_type'] !== 'SuperAdmin') {
        header('Content-Type: application/json');
    $lang = network_mapping_load_language();
        echo json_encode(['success' => false, 'message' => 'Only SuperAdmin can uninstall this plugin']);
        exit;
    }
    
    header('Content-Type: application/json');
    $lang = network_mapping_load_language();
    
    $result = [
        'success' => false,
        'message' => '',
        'details' => []
    ];
    
    try {
        $plugin_dir = __DIR__;
        $system_dir = realpath($plugin_dir . '/../');
        $root_dir = realpath($plugin_dir . '/../../');
        $ui_dir = $root_dir . '/ui';
        
        // ========== STEP 1: Drop all tables ==========
        $tables = [
            'tbl_olt_ports',
            'tbl_odc_ports', 
            'tbl_odp_ports',
            'tbl_olt',
            'tbl_odc',
            'tbl_odp',
            'tbl_tiang',
            'tbl_kabel',
            'tbl_homepass',
            'tbl_customer_online_status'
        ];
        
        foreach ($tables as $table) {
            try {
                ORM::raw_execute("DROP TABLE IF EXISTS `{$table}`");
            } catch (Exception $e) {
                // Ignore errors
            }
        }
        $result['details'][] = "✓ Dropped database tables (" . count($tables) . " tables)";
        
        // ========== STEP 2: Remove columns from tbl_customers ==========
        $columns_to_remove = ['odp_id', 'odp_port', 'foto_lokasi'];
        foreach ($columns_to_remove as $column) {
            try {
                ORM::raw_execute("ALTER TABLE `tbl_customers` DROP COLUMN `{$column}`");
            } catch (Exception $e) {
                // Column might not exist, ignore
            }
        }
        $result['details'][] = "✓ Removed custom columns from tbl_customers";
        
        // ========== STEP 3: Delete config from tbl_appconfig ==========
        try {
            ORM::for_table('tbl_appconfig')->where('setting', 'ffth_mapping_version')->delete_many();
            ORM::for_table('tbl_appconfig')->where('setting', 'ffth_mapping_installed_at')->delete_many();
            ORM::for_table('tbl_appconfig')->where('setting', 'ffth_mapping_updated_at')->delete_many();
            $result['details'][] = "✓ Removed plugin configuration";
        } catch (Exception $e) {
            // Ignore
        }
        
        // ========== STEP 4: Delete cron file ==========
        $cron_file = $system_dir . '/cron_customer_monitor.php';
        if (file_exists($cron_file)) {
            unlink($cron_file);
            $result['details'][] = "✓ Removed cron file (cron_customer_monitor.php)";
        }
        
        // ========== STEP 5: Delete UI Custom folders ==========
        $ui_custom_path = $ui_dir . '/ui_custom/admin/customers';
        if (is_dir($ui_custom_path)) {
            // Check if genieacs_manager.php exists, if so skip view.tpl
            $genieacs_exists = file_exists($system_dir . '/../system/plugin/genieacs_manager.php');
            $tpl_files = ['list.tpl', 'add.tpl', 'edit.tpl'];
            if (!$genieacs_exists) {
                $tpl_files[] = 'view.tpl';
            }
            foreach ($tpl_files as $tpl) {
                $tpl_path = $ui_custom_path . '/' . $tpl;
                if (file_exists($tpl_path)) {
                    unlink($tpl_path);
                }
            }
            $result['details'][] = "✓ Removed custom UI templates";
        }
        
        // ========== STEP 6: Delete CSS folder ==========
        $css_folder = $plugin_dir . '/ui/css';
        if (is_dir($css_folder)) {
            network_mapping_delete_directory($css_folder);
            $result['details'][] = "✓ Removed stylesheet files";
        }
        
        // ========== STEP 6.5: Delete Language folder ==========
        $lang_folder = $plugin_dir . '/lang';
        if (is_dir($lang_folder)) {
            network_mapping_delete_directory($lang_folder);
            $result['details'][] = "✓ Removed language files";
        }
        
        // Delete language preferences from tbl_appconfig
        try {
            ORM::raw_execute("DELETE FROM `tbl_appconfig` WHERE `setting` LIKE 'ffth_lang_%'");
            $result['details'][] = "✓ Removed language preferences";
        } catch (Exception $e) {
            // Ignore
        }
        
        // ========== STEP 7: Delete TPL files ==========
        $tpl_file = $plugin_dir . '/ui/network_mapping_main.tpl';
        if (file_exists($tpl_file)) {
            unlink($tpl_file);
        }
        $result['details'][] = "✓ Removed template files";
        
        // ========== STEP 8: Delete upload folders ==========
        $upload_folders = [
            'system/uploads/foto/odc',
            'system/uploads/foto/odp',
            'system/uploads/foto/tiang',
            'system/uploads/foto/homepass',
            'system/uploads/foto/sambungan'
        ];
        foreach ($upload_folders as $folder) {
            $full_path = $root_dir . '/' . $folder;
            if (is_dir($full_path)) {
                network_mapping_delete_directory($full_path);
            }
        }
        $result['details'][] = "✓ Removed uploaded files";
        
        // ========== STEP 9: Delete ui folder if empty ==========
        $ui_plugin_dir = $plugin_dir . '/ui';
        if (is_dir($ui_plugin_dir)) {
            $remaining = array_diff(scandir($ui_plugin_dir), ['.', '..']);
            if (empty($remaining)) {
                rmdir($ui_plugin_dir);
            }
        }
        
        $result['success'] = true;
        $result['message'] = 'FFTH Mapping has been uninstalled successfully!';
        $result['details'][] = "✓ Uninstallation completed";
        $result['redirect'] = true;
        
        // ========== FINAL: Delete this file (self-destruct) ==========
        $self_file = $plugin_dir . '/network_mapping.php';
        
        // Send response first
        echo json_encode($result);
        
        // Flush output
        if (ob_get_level() > 0) {
            ob_end_flush();
        }
        flush();
        
        // Now delete self
        if (file_exists($self_file)) {
            @unlink($self_file);
        }
        
        exit;
        
    } catch (Exception $e) {
        $result['success'] = false;
        $result['message'] = 'Uninstall error: ' . $e->getMessage();
        echo json_encode($result);
        exit;
    }
}

/**
 * Check for updates from GitHub (Private Repo)
 */
function network_mapping_check_update()
{
    header('Content-Type: application/json');

    // Auto-update disabled by SadiQ (cloudmikrotik.online).
    // The plugin will never contact any external update server.
    echo json_encode([
        'success'         => true,
        'has_update'      => false,
        'current_version' => 'installed',
        'latest_version'  => 'installed',
        'changelog'       => [],
        'message'         => 'You are using the latest version.'
    ]);
    exit;
}

function network_mapping_check_update_disabled_original()
{
    header('Content-Type: application/json');
    $lang = network_mapping_load_language();
    
    $result = [
        'success' => false,
        'has_update' => false,
        'current_version' => '',
        'latest_version' => '',
        'changelog' => [],
        'message' => ''
    ];
    
    try {
        // Get GitHub token
        $github_token = network_mapping_get_github_token();
        if (empty($github_token)) {
            throw new Exception('GitHub token not configured. Please setup GitHub authentication in Settings.');
        }
        
        // Get current version from database
        $config = ORM::for_table('tbl_appconfig')->where('setting', 'ffth_mapping_version')->find_one();
        $current_version = $config ? $config->value : '1.0.0';
        $result['current_version'] = $current_version;
        
        // Fetch version.json from GitHub API (Private Repo)
        $api_url = 'https://api.github.com/repos/ExodiaForb-Plugin/Mapping-FFTH/contents/version.json?ref=main';
        
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $api_url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_TIMEOUT, 30);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
        curl_setopt($ch, CURLOPT_HTTPHEADER, [
            'Authorization: token ' . $github_token,
            'Accept: application/vnd.github.v3+json',
            'User-Agent: PHPNuxBill-FFTH-Mapping'
        ]);
        
        $response = curl_exec($ch);
        $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);
        
        if ($http_code === 401) {
            throw new Exception('GitHub authentication failed. Please check your token.');
        }
        
        if ($http_code === 404) {
            throw new Exception('version.json not found in repository.');
        }
        
        if ($http_code !== 200 || empty($response)) {
            throw new Exception('Failed to fetch version info (HTTP ' . $http_code . ')');
        }
        
        $api_data = json_decode($response, true);
        if (!$api_data || !isset($api_data['content'])) {
            throw new Exception('Invalid response from GitHub API');
        }
        
        // Decode base64 content
        $version_content = base64_decode($api_data['content']);
        $version_data = json_decode($version_content, true);
        
        if (!$version_data || !isset($version_data['version'])) {
            throw new Exception('Invalid version.json format');
        }
        
        $latest_version = $version_data['version'];
        $result['latest_version'] = $latest_version;
        $result['changelog'] = $version_data['changelog'] ?? [];
        $result['release_date'] = $version_data['release_date'] ?? '';
        
        // Compare versions
        if (version_compare($latest_version, $current_version, '>')) {
            $result['has_update'] = true;
            $result['message'] = "Update available: v{$latest_version}";
        } else {
            $result['has_update'] = false;
            $result['message'] = "You are using the latest version";
        }
        
        $result['success'] = true;
        
    } catch (Exception $e) {
        $result['success'] = false;
        $result['message'] = $e->getMessage();
    }
    
    echo json_encode($result);
    exit;
}

/**
 * Run the update process (Private Repo)
 */
function network_mapping_run_update()
{
    // Auto-update permanently disabled by SadiQ (cloudmikrotik.online).
    // This prevents the plugin from downloading and overwriting files
    // from any external repository.
    header('Content-Type: application/json');
    echo json_encode([
        'success' => false,
        'message' => 'Auto-update is disabled on this installation.',
        'details' => []
    ]);
    exit;
}

function network_mapping_run_update_disabled_original()
{
    global $admin;
    
    // Only SuperAdmin can update
    $admin = Admin::_info();
    if ($admin['user_type'] !== 'SuperAdmin') {
        header('Content-Type: application/json');
    $lang = network_mapping_load_language();
        echo json_encode(['success' => false, 'message' => 'Only SuperAdmin can update this plugin']);
        exit;
    }
    
    header('Content-Type: application/json');
    $lang = network_mapping_load_language();
    
    $result = [
        'success' => false,
        'message' => '',
        'details' => []
    ];
    
    try {
        // Get GitHub token
        $github_token = network_mapping_get_github_token();
        if (empty($github_token)) {
            throw new Exception('GitHub token not configured');
        }
        
        $plugin_dir = __DIR__;
        $system_dir = realpath($plugin_dir . '/../');
        $root_dir = realpath($plugin_dir . '/../../');
        $ui_dir = $root_dir . '/ui';
        $temp_dir = $plugin_dir . '/_update_temp';
        
        // ========== STEP 1: Fetch version info ==========
        $api_url = 'https://api.github.com/repos/ExodiaForb-Plugin/Mapping-FFTH/contents/version.json?ref=main';
        
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $api_url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_TIMEOUT, 30);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
        curl_setopt($ch, CURLOPT_HTTPHEADER, [
            'Authorization: token ' . $github_token,
            'Accept: application/vnd.github.v3+json',
            'User-Agent: PHPNuxBill-FFTH-Mapping'
        ]);
        
        $response = curl_exec($ch);
        curl_close($ch);
        
        $api_data = json_decode($response, true);
        $version_content = base64_decode($api_data['content']);
        $version_data = json_decode($version_content, true);
        
        $new_version = $version_data['version'];
        $result['details'][] = "✓ Fetched update info (v{$new_version})";
        
        // ========== STEP 2: Download ZIP from Private Repo ==========
        $zip_file = $plugin_dir . '/_update.zip';
        $download_url = 'https://api.github.com/repos/ExodiaForb-Plugin/Mapping-FFTH/zipball/main';
        
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $download_url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
        curl_setopt($ch, CURLOPT_TIMEOUT, 120);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
        curl_setopt($ch, CURLOPT_HTTPHEADER, [
            'Authorization: token ' . $github_token,
            'Accept: application/vnd.github.v3+json',
            'User-Agent: PHPNuxBill-FFTH-Mapping'
        ]);
        
        $zip_content = curl_exec($ch);
        $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);
        
        if ($http_code !== 200 || empty($zip_content)) {
            throw new Exception('Failed to download update package (HTTP ' . $http_code . ')');
        }
        
        file_put_contents($zip_file, $zip_content);
        $result['details'][] = "✓ Downloaded update package";
        
        // ========== STEP 3: Extract ZIP ==========
        $zip = new ZipArchive();
        if ($zip->open($zip_file) !== true) {
            throw new Exception('Failed to open update package');
        }
        
        // Create temp directory
        if (!is_dir($temp_dir)) {
            mkdir($temp_dir, 0755, true);
        }
        
        $zip->extractTo($temp_dir);
        $zip->close();
        
        // Find extracted folder (GitHub format: OrgName-RepoName-CommitHash)
        $extracted_folders = glob($temp_dir . '/*', GLOB_ONLYDIR);
        if (empty($extracted_folders)) {
            throw new Exception('Invalid update package structure');
        }
        $extracted_dir = $extracted_folders[0];
        
        $result['details'][] = "✓ Extracted update package";
        
        // ========== STEP 4: Update PHP files ==========
        $php_source = $extracted_dir . '/_ffth_php';
        if (is_dir($php_source)) {
            $php_files = glob($php_source . '/*.php');
            foreach ($php_files as $file) {
                $filename = basename($file);
                $dest = $plugin_dir . '/' . $filename;
                copy($file, $dest);
            }
            $result['details'][] = "✓ Updated PHP modules (" . count($php_files) . " files)";
        }
        
        // ========== STEP 5: Update TPL files ==========
        $tpl_source = $extracted_dir . '/_ffth_ui';
        if (is_dir($tpl_source)) {
            $tpl_files = glob($tpl_source . '/*.tpl');
            $ui_dest = $plugin_dir . '/ui';
            foreach ($tpl_files as $file) {
                $filename = basename($file);
                $dest = $ui_dest . '/' . $filename;
                copy($file, $dest);
            }
            $result['details'][] = "✓ Updated UI templates (" . count($tpl_files) . " files)";
        }
        
        // ========== STEP 6: Update CSS files ==========
        $css_source = $extracted_dir . '/_ffth_css/css';
        if (is_dir($css_source)) {
            $css_dest = $plugin_dir . '/ui/css';
            if (!is_dir($css_dest)) {
                mkdir($css_dest, 0755, true);
            }
            $css_files = glob($css_source . '/*.css');
            foreach ($css_files as $file) {
                $filename = basename($file);
                $dest = $css_dest . '/' . $filename;
                copy($file, $dest);
            }
            $result['details'][] = "✓ Updated stylesheets (" . count($css_files) . " files)";
        }
        
        // ========== STEP 7: Update Cron file ==========
        $cron_source = $extracted_dir . '/_ffth_cron/cron_customer_monitor.php';
        if (file_exists($cron_source)) {
            $cron_dest = $system_dir . '/cron_customer_monitor.php';
            copy($cron_source, $cron_dest);
            $result['details'][] = "✓ Updated cron file";
        }
        
        // ========== STEP 8: Update UI Custom ==========
        $ui_custom_source = $extracted_dir . '/_ffth_ui_custom';
        $ui_custom_dest = $ui_dir . '/ui_custom';
        if (is_dir($ui_custom_source)) {
            network_mapping_update_copy_directory($ui_custom_source, $ui_custom_dest);
            $result['details'][] = "✓ Updated custom UI components";
        }
        
        // ========== STEP 8.5: Update Language Files ==========
        $lang_source = $extracted_dir . '/_ffth_lang';
        if (is_dir($lang_source)) {
            $lang_dest = $plugin_dir . '/lang';
            if (!is_dir($lang_dest)) {
                mkdir($lang_dest, 0755, true);
            }
            $lang_files = glob($lang_source . '/*.json');
            foreach ($lang_files as $file) {
                $filename = basename($file);
                $dest = $lang_dest . '/' . $filename;
                copy($file, $dest);
            }
            $result['details'][] = "✓ Updated language files (" . count($lang_files) . " files)";
        }
        
        // ========== STEP 9: Update version in database ==========
        $config = ORM::for_table('tbl_appconfig')->where('setting', 'ffth_mapping_version')->find_one();
        if (!$config) {
            $config = ORM::for_table('tbl_appconfig')->create();
            $config->setting = 'ffth_mapping_version';
        }
        $config->value = $new_version;
        $config->save();
        
        $config_date = ORM::for_table('tbl_appconfig')->where('setting', 'ffth_mapping_updated_at')->find_one();
        if (!$config_date) {
            $config_date = ORM::for_table('tbl_appconfig')->create();
            $config_date->setting = 'ffth_mapping_updated_at';
        }
        $config_date->value = date('Y-m-d H:i:s');
        $config_date->save();
        
        $result['details'][] = "✓ Updated version to v{$new_version}";
        
        // ========== STEP 10: Cleanup ==========
        @unlink($zip_file);
        network_mapping_delete_directory($temp_dir);
        
        $result['details'][] = "✓ Cleaned up temporary files";
        
        $result['success'] = true;
        $result['message'] = "Successfully updated to v{$new_version}";
        $result['new_version'] = $new_version;
        
    } catch (Exception $e) {
        // Cleanup on error
        if (isset($zip_file) && file_exists($zip_file)) {
            @unlink($zip_file);
        }
        if (isset($temp_dir) && is_dir($temp_dir)) {
            network_mapping_delete_directory($temp_dir);
        }
        
        $result['success'] = false;
        $result['message'] = 'Update failed: ' . $e->getMessage();
    }
    
    echo json_encode($result);
    exit;
}

/**
 * Recursively copy directory for update
 */
function network_mapping_update_copy_directory($source, $dest)
{
    if (!is_dir($dest)) {
        mkdir($dest, 0755, true);
    }
    
    $dir = opendir($source);
    while (($file = readdir($dir)) !== false) {
        if ($file == '.' || $file == '..') continue;
        
        $src_path = $source . '/' . $file;
        $dst_path = $dest . '/' . $file;
        
        if (is_dir($src_path)) {
            network_mapping_update_copy_directory($src_path, $dst_path);
        } else {
            copy($src_path, $dst_path);
        }
    }
    closedir($dir);
}