WME Workflow Engine

Xây dựng và chạy các chuỗi tác vụ tự động hóa (workflows) tùy chỉnh trong WME. Tự động điều hướng từ file Excel/CSV/GG Sheets và thực thi các hành động được điều chỉnh sẵn bằng WME SDK.

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         WME Workflow Engine
// @namespace    https://greasyfork.org/
// @version      2.1.4
// @description  Xây dựng và chạy các chuỗi tác vụ tự động hóa (workflows) tùy chỉnh trong WME. Tự động điều hướng từ file Excel/CSV/GG Sheets và thực thi các hành động được điều chỉnh sẵn bằng WME SDK.
// @author       Minh Tan
// @match        https://www.waze.com/editor*
// @match        https://www.waze.com/*/editor*
// @match        https://beta.waze.com/editor*
// @match        https://beta.waze.com/*/editor*
// @exclude      https://www.waze.com/*user/editor*
// @connect      vinfastauto.com
// @connect      script.google.com
// @connect      googleusercontent.com
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @require      https://greasyfork.org/scripts/24851-wazewrap/code/WazeWrap.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js
// @require      https://update.greasyfork.org/scripts/389765/1090053/CommonUtils.js
// @require      https://update.greasyfork.org/scripts/450160/1704233/WME-Bootstrap.js
// ==/UserScript==
/* global require */
/* global $, jQuery */
/* global I18n */
/* global Node$1, Segment, Venue, VenueAddress, WmeSDK */
/* global W, WazeWrap, XLSX */
(function () {
    'use strict';
    let permalinks = [];
    let currentIndex = -1;
    let allWorkflows = {};
    let isLooping = false; // Biến này chỉ ra rằng vòng lặp đang hoạt động hoặc được yêu cầu dừng
    let workbookData = null; // Lưu workbook để ghi đè
    let currentFileName = ''; // Tên file hiện tại
    let statusColumnIndex = -1; // Vị trí cột Status
    let hasUnsavedChanges = false;
    let previousIndex = -1;
    let currentApiData = null; // Lưu dữ liệu JSON trả về từ API
    let currentRowData = [];   // Lưu dữ liệu thô của hàng hiện tại trong Excel
    let eventCleanupRegistry = [];
    let wmeSDK = null;
    const STORAGE_KEY_SETTINGS = 'wme_wfe_gas_settings';
    const STATUS_COL_NAME = 'Status';
    let isGasMode = false;
    let gasHeaders = null;
    let selectedSubCategory = 'CHARGING_STATION';
    let apiDataCache = new Map();
    const PRELOAD_WINDOW = 5; // Số lượng mục sẽ preload
    let gasWriteQueue = [];
    let gasWriteProcessing = false;
    // Provider registry for external charge station providers (extensible)
    const PROVIDERS_FETCH_API = {
        vinfast: {
            brand: "VinFast",
            async fetchData(id) {
                return new Promise((resolve, reject) => {
                    if (!id) {
                        PROVIDERS_FETCH_API.vinfast.updatePanel(null);
                        return resolve(null);
                    }
                    if (apiDataCache.has(id)) {
                        const data = apiDataCache.get(id);
                        apiDataCache.delete(id); // Dùng xong thì xóa khỏi cache để làm sạch
                        currentApiData = data;
                        PROVIDERS_FETCH_API.vinfast.updatePanel(currentApiData);
                        return resolve(data);
                    }
                    GM_xmlhttpRequest({
                        method: "GET",
                        url: `https://vinfastauto.com/vn_vi/get-locator/${id}`,
                        onload: function (response) {
                            if (response.status === 200) {
                                try {
                                    const json = JSON.parse(response.responseText);
                                    if (json && json.data) {
                                        currentApiData = json.data;
                                        PROVIDERS_FETCH_API.vinfast.updatePanel(currentApiData);
                                        resolve(json.data);
                                    } else {
                                        log('API trả về nhưng không có dữ liệu data.', 'warn');
                                        PROVIDERS_FETCH_API.vinfast.updatePanel(null);
                                        resolve(null);
                                    }
                                } catch (e) {
                                    log('Lỗi parse JSON API.', 'error');
                                    PROVIDERS_FETCH_API.vinfast.updatePanel(null);
                                    reject(e);
                                }
                            } else {
                                log(`Lỗi gọi API: Status ${response.status}`, 'error');
                                PROVIDERS_FETCH_API.vinfast.updatePanel(null);
                                reject(new Error(response.statusText));
                            }
                        },
                        onerror: function (err) {
                            log('Lỗi kết nối mạng API.', 'error');
                            PROVIDERS_FETCH_API.vinfast.updatePanel(null);
                            reject(err);
                        }
                    });
                });
            },
            showImagesPopup() {
                if (!currentApiData || !currentApiData.data.images || currentApiData.data.images.length === 0) {
                    alert("Không có dữ liệu ảnh.");
                    return;
                }
                const images = currentApiData.data.images;
                let html = `<div style="display: flex; flex-wrap: wrap; gap: 15px; justify-content: center; padding: 10px;">`;
                images.forEach(img => {
                    html += `<a href="${img.url}" target="_blank"><img src="${img.url}" style="max-width: 300px; height: auto; object-fit: cover; border-radius: 4px; border: 1px solid #ccc; cursor: zoom-in;"></a>`;
                });
                html += `</div>`;
                let imgModal = document.getElementById('img-modal-overlay');
                if (!imgModal) {
                    imgModal = document.createElement('div');
                    imgModal.id = 'img-modal-overlay';
                    imgModal.style.cssText = `
                position: fixed; top:0; left:0; width:100%; height:100%;
                background:rgba(0,0,0,0.7); z-index:2000;
                display:flex; justify-content:center; align-items:center;
            `;
                    imgModal.addEventListener('click', (e) => {
                        if (e.target === imgModal) imgModal.remove();
                    });
                    document.body.appendChild(imgModal);
                }
                imgModal.innerHTML = `
            <div style="background:white; padding:20px; border-radius:8px; max-width:80%; max-height:90%; overflow:auto; position:relative;">
                <button id="close-img-modal" style="position:absolute; right:10px; top:10px; border:none; background:none; font-size:20px; cursor:pointer;">✖</button>
                <h3>Ảnh Trạm Sạc: ${currentApiData.name}</h3>
                ${html}
            </div>
        `;
                document.getElementById('close-img-modal').addEventListener('click', () => {
                    imgModal.remove();
                });
            },
            renderConnectors(evses) {
                const connectorInfo = document.getElementById('vf-connectors-info');
                if (!connectorInfo) return;
                if (!evses || evses.length === 0) {
                    connectorInfo.innerHTML = '<i style="color: #777;">Không có cổng sạc nào được liệt kê.</i>';
                    return;
                }
                const acData = {};
                const dcData = {};
                evses.forEach(evse => {
                    if (evse.connectors) {
                        evse.connectors.forEach(conn => {
                            const powerKw = Math.round(conn.max_electric_power / 1000);
                            const standard = conn.standard || "";
                            const type = conn.power_type || "";
                            let typeKey = standard;
                            if (type === 'AC' && !typeKey) typeKey = 'IEC_62196_T2';
                            if (type === 'DC' && !typeKey) typeKey = 'CCS1';
                            if (type === 'AC' || (standard.includes('IEC_62196_T2') && !standard.includes('COMBO'))) {
                                if (!acData[powerKw]) acData[powerKw] = { count: 0, typeKey };
                                acData[powerKw].count++;
                            } else {
                                if (!dcData[powerKw]) dcData[powerKw] = { count: 0, typeKey };
                                dcData[powerKw].count++;
                            }
                        });
                    }
                });
                const createBtns = (dataObj, bgColor, borderColor) => {
                    const keys = Object.keys(dataObj).sort((a, b) => a - b);
                    if (keys.length === 0) return '';
                    return keys.map(kw => {
                        const item = dataObj[kw];
                        return `<button class="vf-connector-btn"
                                    data-type="${item.typeKey}"
                                    data-power="${kw}"
                                    data-count="${item.count}"
                                    style="cursor:pointer; border:1px solid ${borderColor}; background:${bgColor}; padding:4px 8px; margin:2px; border-radius:4px; font-weight:bold; font-size: 11px; color: #333;">
                                    ➕ ${kw} kW (x${item.count})
                                    </button>`;
                    }).join(" ");
                };
                let html = '';
                const acHtml = createBtns(acData, '#e8f5e9', '#c8e6c9');
                if (acHtml) {
                    html += `<div style="margin-bottom: 8px;"><strong style="color: #2e7d32; font-size: 12px;">⚡ AC (Type 2)</strong><br>${acHtml}</div>`;
                }
                const dcHtml = createBtns(dcData, '#ffebee', '#ffcdd2');
                if (dcHtml) {
                    html += `<div><strong style="color: #c62828; font-size: 12px;">🚀 DC (CCS2)</strong><br>${dcHtml}</div>`;
                }
                if (html === '') {
                    connectorInfo.innerHTML = '<i style="color: #777;">Không tìm thấy cổng chuẩn.</i>';
                } else {
                    connectorInfo.innerHTML = html;
                    connectorInfo.querySelectorAll('.vf-connector-btn').forEach(btn => {
                        btn.onclick = (e) => {
                            e.stopPropagation();
                            const type = btn.getAttribute('data-type');
                            const power = parseInt(btn.getAttribute('data-power'));
                            const count = parseInt(btn.getAttribute('data-count'));
                            const originalText = btn.innerHTML;
                            btn.innerHTML = "⏳...";
                            btn.disabled = true;
                            setTimeout(() => {
                                PROVIDERS_FETCH_API.vinfast.addChargeToWmeDirectly(type, power, count);
                                btn.innerHTML = originalText;
                                btn.disabled = false;
                            }, 50);
                        };
                    });
                }
            },
            addChargeToWmeDirectly(typeKey, power, count, venueModel) {
                try {
                    const venue = venueModel && venueModel.attributes ? venueModel : (WazeWrap.getSelectedFeatures()[0] && WazeWrap.getSelectedFeatures()[0].WW ? WazeWrap.getSelectedFeatures()[0].WW.getObjectModel() : null);
                    if (!venue) throw new Error('No venue model available for addChargeToWmeDirectly');
                    const venueId = venue.attributes.id;
                    let currentCategoryAttrs = venue.attributes.categoryAttributes;
                    let newAttrs = currentCategoryAttrs ? JSON.parse(JSON.stringify(currentCategoryAttrs)) : {};
                    const wmeTypeID = CHARGER_TYPE_MAP[typeKey] || 'UNKNOWN';
                    const portId = `${wmeTypeID}.${power}`;
                    if (!Array.isArray(newAttrs.CHARGING_STATION?.chargingPorts)) {
                        if (!newAttrs.CHARGING_STATION) newAttrs.CHARGING_STATION = {};
                        newAttrs.CHARGING_STATION.chargingPorts = [];
                    }
                    let ports = newAttrs.CHARGING_STATION.chargingPorts;
                    const existingIndex = ports.findIndex(p =>
                                                          p.maxChargeSpeedKw === power &&
                                                          p.connectorTypes.includes(wmeTypeID)
                                                         );
                    if (existingIndex !== -1) {
                        ports[existingIndex].count = count;
                    } else {
                        ports.push({
                            portId,
                            connectorTypes: [wmeTypeID],
                            maxChargeSpeedKw: power,
                            count
                        });
                    }
                    newAttrs.CHARGING_STATION.accessType = "PUBLIC";
                    newAttrs.CHARGING_STATION.paymentMethods = ["CREDIT", "APP", "DEBIT", "ONLINE_PAYMENT"];
                    newAttrs.CHARGING_STATION.costType = "FEE";
                    newAttrs.CHARGING_STATION.network = "Vinfast (V-Green)";
                    newAttrs.CHARGING_STATION.locationInVenue = `${currentApiData.address}`;
                    try {
                        let UpdateObject = require("Waze/Action/UpdateObject");
                        W.model.actionManager.add(new UpdateObject(venue, { 'categoryAttributes': newAttrs }));
                        return true;
                    } catch (actionError) {
                        log("⚠️ Fallback: setAttribute", 'warn');
                        venue.setAttribute('categoryAttributes', newAttrs);
                        return true;
                    }
                } catch (e) {
                    log(`❌ Lỗi: ${e.message}`, 'error');
                    console.error(e);
                    return false;
                }
            },
            updatePanel(data) {
                // If panel wasn't created (user requested it disabled), skip UI updates
                const panelRoot = document.getElementById('vf-panel');
                if (!panelRoot) return;
                const displayDiv = document.getElementById('vf-data-display');
                const nameText = document.getElementById('vf-name-text');
                const imagesBtn = document.getElementById('vf-images-btn');
                const fetchBtn = document.getElementById('vf-fetch-btn');
                if (data) {
                    displayDiv.style.display = 'block';
                    nameText.textContent = data.name || 'N/A';
                    const imagesCount = data.data.images ? data.data.images.length : 0;
                    imagesBtn.disabled = imagesCount === 0;
                    imagesBtn.textContent = `📷 Xem ${imagesCount} Ảnh`;
                    PROVIDERS_FETCH_API.vinfast.renderConnectors(data.data.evses || []);
                    fetchBtn.textContent = 'Fetch';
                    fetchBtn.disabled = false;
                } else {
                    displayDiv.style.display = 'none';
                    imagesBtn.disabled = true;
                    imagesBtn.textContent = '📷 Xem Ảnh';
                    const connectorInfo = document.getElementById('vf-connectors-info');
                    if (connectorInfo) connectorInfo.innerHTML = '<i style="color: #777;">Chưa có dữ liệu cổng sạc.</i>';
                    currentApiData = null;
                    fetchBtn.textContent = 'Fetch';
                    fetchBtn.disabled = false;
                }
            },
            async transferNameToWME(venueModel) {
                if (!currentApiData || !currentApiData.name) {
                    log("Không có tên trạm VinFast để chuyển.", 'warn');
                    return;
                }
                const newName = `Trạm sạc VinFast - ${currentApiData.name}`.replace(/Cửa hàng xăng dầu/gi, 'CHXD');
                const nameUpdated = await updateField(
                    '#venue-edit-general > div:nth-child(2) > wz-text-input',
                    'input',
                    newName,
                );
                if (!nameUpdated) return;
                const targetVenueId = venueModel && venueModel.attributes ? venueModel.attributes.id : (WazeWrap.getSelectedFeatures()[0] && WazeWrap.getSelectedFeatures()[0].WW ? WazeWrap.getSelectedFeatures()[0].WW.getObjectModel().attributes.id : null);
                if (targetVenueId) {
                    wmeSDK.DataModel.Venues.updateVenue({
                        venueId: targetVenueId,
                        phone: '1900 2323 89',
                        url: 'vinfastauto.com',
                        openingHours: [{ days: [0, 1, 2, 3, 4, 5, 6], fromHour: "00:00", toHour: "00:00" }]
                    });
                }
                if (!currentApiData.data || !currentApiData.data.evses) {
                    log("Không có dữ liệu cổng sạc để tự động thêm.", 'warn');
                    return;
                }
                await delay(50);
                try {
                    const evses = currentApiData.data.evses;
                    const portGroups = {};
                    evses.forEach(evse => {
                        if (evse.connectors) {
                            evse.connectors.forEach(conn => {
                                const powerKw = Math.round(conn.max_electric_power / 1000);
                                let standard = conn.standard || "";
                                let type = conn.power_type || "";
                                let typeKey = standard;
                                if (type === 'AC' && !typeKey) typeKey = 'IEC_62196_T2';
                                if (type === 'DC' && !typeKey) typeKey = 'CCS1';
                                const key = `${typeKey}::${powerKw}`;
                                if (!portGroups[key]) portGroups[key] = { typeKey, power: powerKw, count: 0 };
                                portGroups[key].count++;
                            });
                        }
                    });
                    const keys = Object.keys(portGroups);
                    if (keys.length === 0) {
                        log("Không tìm thấy cổng sạc chuẩn nào để thêm.", 'warn');
                        return;
                    }
                    for (const key of keys) {
                        const item = portGroups[key];
                        PROVIDERS_FETCH_API.vinfast.addChargeToWmeDirectly(item.typeKey, item.power, item.count);
                        await delay(50);
                    }
                } catch (err) {
                    log(`Lỗi khi tự động thêm sạc: ${err.message}`, 'error');
                }
            }
        },
    }
    const PROVIDERS_NON_API = {
        //['', 'MIPECORP', 'PV Oil', 'Petrolimex', 'SaigonPetro', 'Satra', 'Thalexim']
        petrolimex: {
            brand: 'Petrolimex',
            url: 'petrolimex.com.vn',
            phone: '1900 2828'
        },
        saigonpetro: {
            brand: 'SaigonPetro',
            url: 'saigonpetro.com.vn',
        },
        pvoil: {
            brand: 'PV Oil',
            url: 'pvoil.com.vn'
        },
        mipecorp: {
            brand: 'MIPECORP',
            url: 'mipecorp.com.vn'
        },
        evone: {
            brand: 'ev-one.vn'
        }

    }
    const CHARGER_TYPE_MAP = {
        'IEC_62196_T2': 'TYPE2',           // Type 2
        'IEC_62196_T2_COMBO': 'CCS_TYPE2', // CCS2
        'CCS1': 'CCS_TYPE1',
        'CHADEMO': 'CHADEMO',
        'TESLA': 'TESLA',
        'J1772': 'TYPE1',
        'WALL': 'WALL_OUTLET',
        'GB_T': 'GB_T'
    };
    const SDK_REGISTRY = {
        "update_charge_station_api": {
            name: "Cập nhật Thông tin trạm sạc có fetch API",
            description: "Dựa vào {{tên cột}} là lấy cột ID trong bảng tính, {{value}} là tên nhà cung cấp dịch vụ",
            params: [
                { key: "id", label: "Tên cột chứa ID để fetch iteration data vd: https://<domain>.com/{id}", type: "text", placeholder: "{{A}}" },
                { key: "provider", label: "Tên nhà cung cấp (vinfast,...)", type: "text", placeholder: "{{value}}" }
            ]
        },
        "update_gas_station": {
            name: "Cập nhật Thông tin trạm xăng",
            description: "Dựa vào {{tên cột}} để lấy tên cửa hàng trong bảng tính",
            params: [
                {
                    key: 'name',
                    label: 'Tên cột chứa tên cửa hàng',
                    type: 'text',
                    placeholder: "{{A}}"
                },
                {
                    key: "provider",
                    label: "Tên nhà cung cấp viết liền, không viết hoa (petrolimex,saigonpetro,...)"
                },
                {
                    key: "openHours",
                    label: "Cột giờ mở cửa (vd: 07:00 SA - 05:00 CH)",
                    type: "text",
                    placeholder: "{{F}} (Optional)"
                },
                {
                    key: "phone",
                    label: "Cột số điện thoại (vd: 09000..00)",
                    type: "text",
                    placeholder: "{{C}} (Optional)"
                }
            ]
        },
        "update_lock_rank": {
            name: "Khóa đối tượng (Lock Level)",
            description: "Đặt cấp độ khóa cho đối tượng.",
            params: [
                { key: "rank", label: "Cấp độ (1-5)", type: "number", min: 1, max: 5, placeholder: "3" }
            ]
        },
        "update_segment_city": {
            name: "Cập nhật tên tỉnh,tp/xã phường mới cho đường",
            description: "Đổi tên tỉnh,tp/xã phường mới cho các Segment đang chọn. Dùng {{value}} để lấy từ ô nhập liệu.",
            params: [
                { key: "cityName", label: "Tên TP mới", type: "text", placeholder: "{{value}}" }
            ]
        },
    };
    const defaultWorkflows = {
        "update_vinfast_charge_station": {
            name: "Cập nhật dữ liệu trạm sạc VF",
            tasks: [
                {
                    taskId: "update_charge_station_api",
                    enabled: true,
                    params: {
                        id: "{{entity_id}}",
                        provider: "Vinfast"
                    }
                },
                {
                    taskId: "update_lock_rank",
                    enabled: true,
                    params: { rank: "3" }
                }
            ]
        },
        "wf_update_gas_saition": {
            name: "Cập nhật dữ liệu trạm xăng",
            tasks: [
                {
                    taskId: "update_gas_station",
                    enabled: true,
                    params: {
                        name: "{{A}}",
                        provider: "Petrolimex",
                        openHours: "{{F}}",
                        phone: "{{C}}"
                    }
                }
            ]
        },
        "wf_update_segment_city": {
            name: "Đổi tên tỉnh,tp/xã phường mới cho Segments",
            tasks: [
                {
                    taskId: "update_segment_city",
                    enabled: true,
                    params: { cityName: "{{value}}" }
                }
            ]
        }
    };
    let CATEGORIES = [
        { key: 'CAR_SERVICES', subs: ['CAR_WASH', 'CHARGING_STATION', 'GARAGE_AUTOMOTIVE_SHOP', 'GAS_STATION'] },
        { key: 'CRISIS_LOCATIONS', subs: ['DONATION_CENTERS', 'SHELTER_LOCATIONS'] },
        {
            key: 'CULTURE_AND_ENTERTAINEMENT',
            subs: ['ART_GALLERY', 'CASINO', 'CLUB', 'TOURIST_ATTRACTION_HISTORIC_SITE', 'MOVIE_THEATER', 'MUSEUM', 'MUSIC_VENUE', 'PERFORMING_ARTS_VENUE', 'GAME_CLUB', 'STADIUM_ARENA', 'THEME_PARK', 'ZOO_AQUARIUM', 'RACING_TRACK', 'THEATER'],
        },
        { key: 'FOOD_AND_DRINK', subs: ['RESTAURANT', 'BAKERY', 'DESSERT', 'CAFE', 'FAST_FOOD', 'FOOD_COURT', 'BAR', 'ICE_CREAM'] },
        { key: 'LODGING', subs: ['HOTEL', 'HOSTEL', 'CAMPING_TRAILER_PARK', 'COTTAGE_CABIN', 'BED_AND_BREAKFAST'] },
        { key: 'NATURAL_FEATURES', subs: ['ISLAND', 'SEA_LAKE_POOL', 'RIVER_STREAM', 'FOREST_GROVE', 'FARM', 'CANAL', 'SWAMP_MARSH', 'DAM'] },
        { key: 'OTHER', subs: ['CONSTRUCTION_SITE'] },
        { key: 'OUTDOORS', subs: ['PARK', 'PLAYGROUND', 'BEACH', 'SPORTS_COURT', 'GOLF_COURSE', 'PLAZA', 'PROMENADE', 'POOL', 'SCENIC_LOOKOUT_VIEWPOINT', 'SKI_AREA'] },
        { key: 'PARKING_LOT', subs: ['PARKING_LOT'] },
        {
            key: 'PROFESSIONAL_AND_PUBLIC',
            subs: [
                'COLLEGE_UNIVERSITY',
                'SCHOOL',
                'CONVENTIONS_EVENT_CENTER',
                'GOVERNMENT',
                'LIBRARY',
                'CITY_HALL',
                'ORGANIZATION_OR_ASSOCIATION',
                'PRISON_CORRECTIONAL_FACILITY',
                'COURTHOUSE',
                'CEMETERY',
                'FIRE_DEPARTMENT',
                'POLICE_STATION',
                'MILITARY',
                'HOSPITAL_URGENT_CARE',
                'DOCTOR_CLINIC',
                'OFFICES',
                'POST_OFFICE',
                'RELIGIOUS_CENTER',
                'KINDERGARDEN',
                'FACTORY_INDUSTRIAL',
                'EMBASSY_CONSULATE',
                'INFORMATION_POINT',
                'EMERGENCY_SHELTER',
                'TRASH_AND_RECYCLING_FACILITIES',
            ],
        },
        {
            key: 'SHOPPING_AND_SERVICES',
            subs: [
                'ARTS_AND_CRAFTS',
                'BANK_FINANCIAL',
                'SPORTING_GOODS',
                'BOOKSTORE',
                'PHOTOGRAPHY',
                'CAR_DEALERSHIP',
                'FASHION_AND_CLOTHING',
                'CONVENIENCE_STORE',
                'PERSONAL_CARE',
                'DEPARTMENT_STORE',
                'PHARMACY',
                'ELECTRONICS',
                'FLOWERS',
                'FURNITURE_HOME_STORE',
                'GIFTS',
                'GYM_FITNESS',
                'SWIMMING_POOL',
                'HARDWARE_STORE',
                'MARKET',
                'SUPERMARKET_GROCERY',
                'JEWELRY',
                'LAUNDRY_DRY_CLEAN',
                'SHOPPING_CENTER',
                'MUSIC_STORE',
                'PET_STORE_VETERINARIAN_SERVICES',
                'TOY_STORE',
                'TRAVEL_AGENCY',
                'ATM',
                'CURRENCY_EXCHANGE',
                'CAR_RENTAL',
                'TELECOM',
            ],
        },
        {
            key: 'TRANSPORTATION',
            subs: ['AIRPORT', 'BUS_STATION', 'FERRY_PIER', 'SEAPORT_MARINA_HARBOR', 'SUBWAY_STATION', 'TRAIN_STATION', 'BRIDGE', 'TUNNEL', 'TAXI_STATION', 'JUNCTION_INTERCHANGE', 'REST_AREAS', 'CARPOOL_SPOT'],
        },
    ];
    const STORAGE_KEY = 'wme_custom_workflows';
    function bootstrap() {
        if (typeof WazeWrap !== 'undefined' && WazeWrap.Init) {
            WazeWrap.Init(() => {
                const sdk = typeof unsafeWindow !== 'undefined' && unsafeWindow.getWmeSdk ? unsafeWindow.getWmeSdk({ scriptId: 'wme-wfe', scriptName: 'WME Workflow Engine' }) : getWmeSdk({ scriptId: 'wme-wfe', scriptName: 'WME Workflow Engine' });
                init(sdk);
            });
        } else {
            // Fallback initialization if WazeWrap isn't fully ready (shouldn't happen with @require)
            if (typeof unsafeWindow !== 'undefined' && unsafeWindow.SDK_INITIALIZED) {
                unsafeWindow.SDK_INITIALIZED.then(() => {
                    const sdk = unsafeWindow.getWmeSdk({ scriptId: 'wme-wfe', scriptName: 'WME Workflow Engine' });
                    init(sdk);
                });
            } else if (typeof window.SDK_INITIALIZED !== 'undefined') {
                window.SDK_INITIALIZED.then(() => {
                    const sdk = window.getWmeSdk({ scriptId: 'wme-wfe', scriptName: 'WME Workflow Engine' });
                    init(sdk);
                });
            } else {
                log('WME SDK is not available. Script will not run.', 'error');
            }
        }
    }
    function init(sdk) {
        console.log("WME Workflow Engine: Initialized");
        wmeSDK = sdk
        loadWorkflows();
        createUI();
        createWorkflowEditorModal();
        populateWorkflowSelector();
        updateUIState();
        registerHotkeys();
        window.addEventListener('beforeunload', (e) => {
            cleanupAllEvents();
            placeholderCache.clear();
            if (hasUnsavedChanges) {
                const message = '⚠️ Bạn có thay đổi status chưa được lưu! Nhấn "Cập nhật Status" trước khi thoát.';
                e.preventDefault();
                e.returnValue = message;
                return message;
            }
        });
    }
    function registerEventCleanup(element, event, handler) {
        element.addEventListener(event, handler);
        eventCleanupRegistry.push({ element, event, handler });
    }

    function cleanupAllEvents() {
        eventCleanupRegistry.forEach(({ element, event, handler }) => {
            element.removeEventListener(event, handler);
        });
        eventCleanupRegistry = [];
    }
    function resetData() {
        // Clear intervals nếu có
        if (window._wmeWorkflowInterval) {
            clearInterval(window._wmeWorkflowInterval);
            window._wmeWorkflowInterval = null;
        }
        if (window._wmeWorkflowTimeout) {
            clearTimeout(window._wmeWorkflowTimeout);
            window._wmeWorkflowTimeout = null;
        }
        cleanupAllEvents();
        if (workbookData) {
            workbookData = null;
        }
        permalinks.length = 0;
        currentRowData = null;
        currentApiData = null;
        const logBox = document.getElementById('log_info');
        if (logBox) {
            logBox.innerHTML = ''; // Clear log entries
        }
    }
    /**
    * Tạo POI mới trên bản đồ Waze
    * @param {number} lat - Vĩ độ
    * @param {number} lon - Kinh độ
    * @param {string} type - 'point' hoặc 'area'
    */
    async function createWazePOI(lat, lon, type, method = 'auto') {
        try {
            let geometry;

            if (method === 'auto') {
                if (!lat || !lon) return;
                if (type === 'point') {
                    geometry = { type: "Point", coordinates: [lon, lat] };
                } else {
                    const offset = 0.00015; // Kích thước vùng (~15m)
                    geometry = {
                        type: "Polygon",
                        coordinates: [[[lon-offset, lat-offset], [lon+offset, lat-offset], [lon+offset, lat+offset], [lon-offset, lat+offset], [lon-offset, lat-offset]]]
                    };
                }
            } else {
                geometry = await (type === 'point' ? wmeSDK.Map.drawPoint() : wmeSDK.Map.drawPolygon());
            }

            const newId = wmeSDK.DataModel.Venues.addVenue({
                category: selectedSubCategory,
                geometry: geometry
            });
            setTimeout(()=>{
                wmeSDK.Editing.setSelection({ selection: { ids: [newId.toString()], objectType: 'venue' } });
            }, 100)
        } catch (err) {
            log(`Lỗi tạo POI: ${err.message}`, 'error');
        }
    }
    function getApiInfoForNextItem(index) {
        if (index < 0 || index >= permalinks.length) return null;

        const item = permalinks[index];
        const selectedWorkflowId = document.getElementById('workflow_select')?.value;
        const selectedWorkflow = selectedWorkflowId ? allWorkflows[selectedWorkflowId] : null;

        if (!selectedWorkflow || !Array.isArray(selectedWorkflow.tasks)) return null;

        const chargeApiTask = selectedWorkflow.tasks.find(t => t.enabled && t.taskId === 'update_charge_station_api');
        if (!chargeApiTask) return null;

        const providerKey = (chargeApiTask.params.provider || 'vinfast').toLowerCase();
        const handler = PROVIDERS_FETCH_API[providerKey];
        if (!handler) return null;

        // Simulate placeholder replacement using the target item's rowData
        const idPlaceholder = chargeApiTask.params.id || '';
        let resolvedId = idPlaceholder.replace(/{{([^}]+)}}/g, (match, key) => {
            return item.rowData[key.trim()] || match;
        });

        resolvedId = resolvedId.toString().trim();
        return resolvedId ? { id: resolvedId, handler } : null;
    }

    function performApiPreload(id, handler) {
        if (apiDataCache.has(id)) return;

        GM_xmlhttpRequest({
            method: "GET",
            url: `https://vinfastauto.com/vn_vi/get-locator/${id}`,
            onload: function (response) {
                if (response.status === 200) {
                    try {
                        const json = JSON.parse(response.responseText);
                        if (json && json.data) {
                            apiDataCache.set(id, json.data);
                        }
                    } catch (e) {
                        // Lỗi parse JSON khi preload, chỉ log console, không làm gián đoạn
                        console.error(`Preload: Lỗi parse JSON cho ID ${id}.`, e);
                    }
                } else {
                    console.warn(`Preload: Lỗi gọi API Status ${response.status} cho ID ${id}.`);
                }
            },
            onerror: function (err) {
                console.error(`Preload: Lỗi kết nối mạng cho ID ${id}.`, err);
            }
        });
    }

    function preloadApiData() {
        apiDataCache.clear();
        if (permalinks.length === 0) return;

        const start = currentIndex;
        const end = Math.min(start + PRELOAD_WINDOW, permalinks.length);

        for (let i = start; i < end; i++) {
            const apiInfo = getApiInfoForNextItem(i);
            if (apiInfo) {
                performApiPreload(apiInfo.id, apiInfo.handler);
            }
        }
    }
    let placeholderCache = new Map();

    function replacePlaceholders(text) {
        if (!text || typeof text !== 'string') return text;

        // Check cache
        const cacheKey = `${text}_${currentIndex}`;
        if (placeholderCache.has(cacheKey)) {
            return placeholderCache.get(cacheKey);
        }

        if (!currentRowData || typeof currentRowData !== 'object') {
            return text.replace(/{{[A-Z]+}}/g, match => match)
                .replace(/{{[^}]+}}/g, match => match);
        }

        let result = text.replace(/{{([^}]+)}}/g, (match, key) => {
            const trimmedKey = key.trim();
            if (currentRowData[trimmedKey] !== undefined) {
                return currentRowData[trimmedKey];
            }
            return match;
        });

        // Replace {{value}}
        const manualValue = document.getElementById('workflow_variable_input').value;
        result = result.replace('{{value}}', manualValue);

        // Cache result
        placeholderCache.set(cacheKey, result);

        // Limit cache size
        if (placeholderCache.size > 100) {
            const firstKey = placeholderCache.keys().next().value;
            placeholderCache.delete(firstKey);
        }

        return result;
    }

    function getColumnLetter(colIndex) {
        let temp, letter = '';
        while (colIndex >= 0) {
            temp = colIndex % 26;
            letter = String.fromCharCode(temp + 65) + letter;
            colIndex = Math.floor(colIndex / 26) - 1;
        }
        return letter;
    }
    function getColumnIndexFromLetter(colLetter) {
        let colIndex = 0;
        for (let i = 0; i < colLetter.length; i++) {
            colIndex = colIndex * 26 + (colLetter.charCodeAt(i) - 64);
        }
        return colIndex - 1; // 0-based index
    }
    /**
    * Tìm một phần tử, hỗ trợ tìm kiếm bên trong Shadow DOM.
    * @param {string} selector - CSS selector cho phần tử chính.
    * @param {string} [shadowSelector] - CSS selector cho phần tử bên trong shadow DOM.
    * @returns {Promise<Element|null>}
    */
    async function findElement(selector, shadowSelector = '') {
        try {
            const baseElement = await waitForElement(selector);
            if (!shadowSelector) {
                return baseElement;
            }
            if (baseElement && baseElement.shadowRoot) {
                await delay(50); // Small delay for shadow DOM content to fully render
                const shadowElement = baseElement.shadowRoot.querySelector(shadowSelector);
                if (!shadowElement) {
                    log(`Lỗi: Không tìm thấy phần tử con với selector "${shadowSelector}" trong shadow DOM của "${selector}".`, 'error');
                }
                return shadowElement;
            }
            log(`Lỗi: Không tìm thấy shadow root trên phần tử "${selector}".`, 'error');
            return null;
        } catch (error) {
            log(`Lỗi khi tìm phần tử "${selector}": ${error.message}`, 'error');
            throw error; // Re-throw to propagate the error
        }
    }
    async function updateField(baseSelector, shadowSelector, newValue) {
        try {
            const hostElement = document.querySelector(baseSelector);
            if (!hostElement) {
                return false;
            }
            // Xử lý shadowRoot động
            let inputElement;
            if (shadowSelector.startsWith('#wz-textarea')) {
                // Tìm textarea đầu tiên trong shadowRoot (bỏ qua ID động)
                inputElement = hostElement.shadowRoot.querySelector('textarea');
            } else {
                inputElement = hostElement.shadowRoot.querySelector(shadowSelector);
            }
            if (!inputElement) {
                return false;
            }
            inputElement.focus();
            inputElement.value = newValue;
            inputElement.dispatchEvent(new Event("input", { bubbles: true, cancelable: true }));
            inputElement.dispatchEvent(new Event("change", { bubbles: true, cancelable: true }));
            inputElement.blur();
            return true;
        } catch (e) {
            log(`❌ Lỗi: ${e.message}`, 'error');
            return false;
        }
    }
    function findCityIdByName(cityName) {
        if (!cityName) return null;
        const targetName = cityName.toString().trim();
        const cities = W.model.cities.objects;
        for (const id in cities) {
            if (cities.hasOwnProperty(id)) {
                const city = cities[id];
                // Kiểm tra attributes tồn tại và so sánh tên chính xác
                if (city.attributes && city.attributes.name === targetName) {
                    return city.attributes.id;
                }
            }
        }
        return null;
    }
    function getOrCreateStreet(streetName, cityId) {
        return wmeSDK.DataModel.Streets.getStreet({ streetName, cityId })
            ?? wmeSDK.DataModel.Streets.addStreet({ streetName, cityId });
    }
    function convertTo24Hour(timeStr) {
        // timeStr ví dụ: "05:00 SA" hoặc "05:00 CH"
        const parts = timeStr.trim().split(' ');
        if (parts.length !== 2) return null;
        let [hourMin, meridiem] = parts;
        let [hourStr, minuteStr] = hourMin.split(':');
        const hour = parseInt(hourStr);
        const minute = parseInt(minuteStr);
        if (isNaN(hour) || isNaN(minute)) return null;
        meridiem = meridiem.toUpperCase();
        let hour24 = hour;
        if (meridiem === 'SA') {
            if (hour === 12) hour24 = 0; // 12:xx SA -> 00:xx
        } else if (meridiem === 'CH') {
            if (hour !== 12) hour24 += 12; // 01:xx CH -> 13:xx
        } else {
            return null; // Invalid meridiem
        }
        // Format back to HH:MM (zero padding)
        return `${String(hour24).padStart(2, '0')}:${String(minute).padStart(2, '0')}`;
    }
    /**
     * Parse giờ mở cửa từ định dạng Việt Nam sang cấu trúc WME SDK.
     * @param {string} openHoursString Ví dụ: "07:00 SA - 05:00 CH" hoặc "24/24"
     * @returns {Array<Object>|null} WME openingHours array hoặc null nếu lỗi.
     */
    function parseVietnameseOpenHours(openHoursString) {
        if (!openHoursString) return null;
        openHoursString = openHoursString.toString().trim().toUpperCase();
        if (openHoursString === '24/24') {
            return [{ days: [0, 1, 2, 3, 4, 5, 6], fromHour: "00:00", toHour: "00:00" }];
        }
        // Regex để bắt: HH:MM SA/CH - HH:MM SA/CH
        const match = openHoursString.match(/(\d{1,2}:\d{2}\s+(?:SA|CH))\s*-\s*(\d{1,2}:\d{2}\s+(?:SA|CH))/);
        if (!match) {
            log(`Không thể parse giờ mở cửa: "${openHoursString}".`, 'warn');
            return null;
        }
        const from24h = convertTo24Hour(match[1]);
        const to24h = convertTo24Hour(match[2]);
        if (from24h && to24h) {
            // Áp dụng cho cả 7 ngày trong tuần
            return [{ days: [0, 1, 2, 3, 4, 5, 6], fromHour: from24h, toHour: to24h }];
        } else {
            log(`Lỗi chuyển đổi giờ từ "${openHoursString}" sang 24h.`, 'error');
            return null;
        }
    }
    function capitalizeWords(string) {
        const words = string.split(' ');
        const capitalizedWords = words.map(word => {
            if (word.length === 0) return '';
            return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
        });
        return capitalizedWords.join(' ');
    }
    /**
     * Thực thi một Task SDK cụ thể
     */
    async function executeSdkTask(task, selectedFeature) {
        // Parse params with {{variables}}
        const parsedParams = {};
        for (const [key, val] of Object.entries(task.params)) {
            parsedParams[key] = replacePlaceholders(val);
        }
        const WazeActionUpdateObject = require("Waze/Action/UpdateObject");
        const WazeActionUpdateFeatureAddress = require("Waze/Action/UpdateFeatureAddress");
        const featureModel = selectedFeature.WW.getObjectModel(); // Lấy Waze Model Object
        switch (task.taskId) {
            case "update_lock_rank": {
                const rank = parseInt(parsedParams.rank) || 1;
                const modelRank = Math.max(0, Math.min(5, rank - 1));
                if (featureModel.attributes.lockRank !== modelRank) {
                    W.model.actionManager.add(new WazeActionUpdateObject(featureModel, { lockRank: modelRank }));
                }
                break;
            }
            case "update_charge_station_api": {
                // Provider-based handler for external charge-station APIs (extensible)
                const providerKey = (parsedParams.provider || 'vinfast').toLowerCase();
                const handler = PROVIDERS_FETCH_API[providerKey];
                if (!handler) {
                    log(`SDK: Provider "${parsedParams.provider}" chưa được hỗ trợ.`, 'warn');
                    break;
                }
                // Luôn tạo panel của provider nếu nó được gọi, để hiển thị dữ liệu API
                createProviderPanel(handler); // Gọi hàm tạo panel chung
                if (!parsedParams.id) {
                    log('SDK: Không có ID API cung cấp cho cập nhật trạm sạc.', 'error');
                    // Vẫn gọi update panel để xóa dữ liệu cũ
                    handler.updatePanel(null);
                    break;
                }
                try {
                    // Lấy dữ liệu
                    const apiData = await handler.fetchData(parsedParams.id);
                    if (!apiData) {
                        log(`SDK: Không lấy được dữ liệu từ API ${handler.brand}.`, 'warn');
                        break;
                    }
                    currentApiData = apiData;
                    const featureModel = selectedFeature?.WW?.getObjectModel ? selectedFeature.WW.getObjectModel() : null;
                    if (!featureModel) {
                        log('SDK: Không có feature model hợp lệ để cập nhật trạm sạc.', 'error');
                        break;
                    }
                    // Let provider apply name/venue-level changes
                    if (typeof handler.transferNameToWME === 'function') {
                        await handler.transferNameToWME(featureModel);
                    }
                } catch (err) {
                    log(`SDK: Lỗi khi xử lý API trạm sạc: ${err.message}`, 'error');
                    console.error(err);
                }
                break;
            }
            case "update_segment_city": {
                let cityID = findCityIdByName(parsedParams.cityName);
                let city = W.model.cities.objects[cityID].attributes;
                let segmentsSelected = wmeSDK.Editing.getSelection()
                segmentsSelected?.ids.forEach(segmentId => {
                    // Process the city
                    const newCityProperties = {
                        cityName: city.name,
                        countryId: cityID,
                    };
                    let newCityId = wmeSDK.DataModel.Cities.getById({ cityId: cityID })?.id;
                    if (newCityId == null) {
                        newCityId = wmeSDK.DataModel.Cities.addCity(newCityProperties).id;
                    }
                    // Process the street
                    const newPrimaryStreetId = getOrCreateStreet(wmeSDK.DataModel.Segments.getAddress({ segmentId: segmentId }).street.name, newCityId).id;
                    // Update the segment with the new street
                    wmeSDK.DataModel.Segments.updateAddress({ segmentId, primaryStreetId: newPrimaryStreetId });
                });
                break;
            }
            case "update_gas_station": {
                // https://www.waze.com/editor/sdk/classes/index.SDK.Venues.html#updatevenue
                let venueSelected = wmeSDK.Editing.getSelection()
                const venueId = venueSelected.ids[0]
                let provider = parsedParams.provider;
                let name = parsedParams.name;
                const providerKey = (provider || '').trim().toLowerCase();
                const providerConfig = PROVIDERS_NON_API[providerKey];
                if (!providerConfig) {
                    log(`Provider "${parsedParams.provider}" chưa được hỗ trợ trong danh sách cấu hình.`, 'warn');
                    // Sử dụng tên provider thô nếu không tìm thấy config
                }
                let providerName = providerConfig?.brand || parsedParams.provider.toString();
                let providerPhone = providerConfig?.phone || '';
                let providerUrl = providerConfig?.url || '';
                name = name.replace(/Cửa hàng xăng dầu/gi, "CHXD");
                if (name.includes(provider)) {
                    name = name.replace(new RegExp(provider, "gi"), "").trim();
                }
                let finalName = `${PROVIDERS_NON_API[providerKey].brand} - ${name}`;
                const updatePayload = {
                    brand: providerName,
                    lockRank: 2,
                    name: finalName,
                    phone: providerPhone,
                    categories: [selectedSubCategory],
                    url: providerUrl,
                    venueId: venueId,
                };
                if (parsedParams.phone) {
                    const phoneValue = parsedParams.phone.toString().trim();
                    if (phoneValue) {
                        updatePayload.phone = phoneValue;
                    }
                }
                if (parsedParams.openHours) {
                    const hours = parseVietnameseOpenHours(parsedParams.openHours);
                    if (hours) {
                        updatePayload.openingHours = hours;
                    } else {
                        log(`SDK: Bỏ qua giờ mở cửa do parse lỗi hoặc định dạng không khớp.`, 'warn');
                    }
                }
                wmeSDK.DataModel.Venues.updateVenue(updatePayload)
            }
        }
        await delay(100); // Nhỏ delay để UI không bị đơ nếu chạy loop
    }
    async function runSelectedWorkflow(isCalledByLoop = false) {
        const workflowId = document.getElementById('workflow_select').value;
        if (!workflowId || !allWorkflows[workflowId]) {
            if (isCalledByLoop) throw new Error("Không workflow hợp lệ.");
            return alert("Chọn workflow hợp lệ!");
        }
        const workflow = allWorkflows[workflowId];
        const selection = WazeWrap.getSelectedFeatures();
        if (selection.length === 0) {
            log("❌ Chưa chọn đối tượng nào trên bản đồ!", "error");
            if (isCalledByLoop) throw new Error("Không có selection.");
            return;
        }
        // Với SDK, ta có thể xử lý nhiều object cùng lúc, nhưng ở đây ta loop qua object đầu tiên (hoặc tất cả nếu muốn)
        // Hiện tại script hỗ trợ workflow chạy trên 1 đối tượng focus từ Excel
        const target = selection[0];
        try {
            const tasksToRun = (workflow.tasks || []).filter(t => t.enabled);
            if (tasksToRun.length === 0) {
                log("Workflow không có hành động nào được bật.", "warn");
                return;
            }
            const workflowHasChargeApi = tasksToRun.some(t => t.taskId === 'update_charge_station_api');
            if (!workflowHasChargeApi) {
                resetApiPanelState();
            }
            for (const task of tasksToRun) {
                if (isCalledByLoop && !isLooping) throw new Error("Stopped by user");
                await executeSdkTask(task, target);
            }
        } catch (error) {
            log(`❌ Lỗi Workflow: ${error.message}`, 'error');
            console.error(error);
            throw error;
        }
    }
    async function toggleWorkflowLoop() {
        if (isLooping) {
            // Đang chạy, yêu cầu dừng
            isLooping = false;
            log("Đã yêu cầu dừng vòng lặp. Sẽ dừng sau khi hoàn thành hoặc giữa bước hiện tại.", 'warn');
            // updateUIState() sẽ được gọi bởi executeLoop khi nó thực sự dừng
        } else {
            // Không chạy, bắt đầu vòng lặp
            if (permalinks.length === 0) {
                log("Vui lòng tải một file Excel/CSV trước khi bắt đầu vòng lặp.", 'warn');
                return;
            }
            isLooping = true; // Thiết lập trạng thái đang lặp
            updateUIState(); // Cập nhật UI để hiển thị nút "Dừng Lặp"
            log("--- Bắt đầu vòng lặp tự động ---", 'special');
            await executeLoop();
        }
    }
    async function executeLoop() {
        if (currentIndex < 0 && permalinks.length > 0) {
            currentIndex = 0;
        }
        while (isLooping && currentIndex < permalinks.length) {
            updateUIState();
            updateStatus('Đang tạo'); // Đánh dấu đang làm việc
            try {
                await processCurrentLink();
                if (!isLooping) { break; }
                await delay(100);
                if (!isLooping) { break; }
                await runSelectedWorkflow(true);
                const shouldSavePermalink = document.getElementById('save_permalink_after_create')?.checked;

                // Kiểm tra có đối tượng đang được chọn (để đảm bảo có link hợp lệ)
                if (shouldSavePermalink) {
                    const newPermalink = wmeSDK.Map.getPermalink();
                    if (newPermalink) {
                        updatePermalinkInWorkbook(currentIndex, newPermalink);
                    } else {
                        log(`⚠️ (Loop) Không lấy được Permalink (Đối tượng chưa được lưu hoặc chưa có ID).`, 'warn');
                    }
                }
                // Đánh dấu Đã tạo khi hoàn thành workflow
                updateStatus('Đã tạo');
            } catch (error) {
                if (isLooping) {
                    log(`Lỗi ở mục ${currentIndex + 1}, bỏ qua và tiếp tục. Lỗi: ${error.message}`, 'error');
                } else {
                    log(`Vòng lặp đã dừng tại mục ${currentIndex + 1} do yêu cầu dừng.`, 'warn');
                }
                break;
            }
            if (!isLooping) break;
            if (currentIndex < permalinks.length - 1) {
                previousIndex = currentIndex; // Lưu index hiện tại
                currentIndex++;
                if (!isLooping) { break; }
                await delay(100);
            } else {
                log("Đã đến mục cuối cùng của danh sách.", 'info');
                isLooping = false;
            }
        }
        isLooping = false;
        if (currentIndex >= permalinks.length && permalinks.length > 0) {
            log("--- ✅ Hoàn thành vòng lặp tự động! ---", 'special');
        } else if (permalinks.length === 0) {
            log("Không có permalink nào để lặp.", 'warn');
        } else {
            log("--- Vòng lặp tự động đã dừng. ---", 'warn');
        }
        updateUIState();
    }

    function handleFile(e) {
        isGasMode = false;
        gasHeaders = null;

        resetData()
        permalinks = [];
        currentIndex = -1;
        previousIndex = -1;
        hasUnsavedChanges = false;
        const file = e.target.files[0];
        if (!file) {
            updateUIState();
            return;
        }
        currentFileName = file.name;
        const urlColumnInput = document.getElementById('url_column').value.toUpperCase();
        const urlColumnIndex = getColumnIndexFromLetter(urlColumnInput); // Hàm helper đã có
        if (urlColumnIndex < 0 || urlColumnIndex > 255) {
            log(`Lỗi: Cột "${urlColumnInput}" không hợp lệ. Vui lòng nhập A-IV.`, 'error');
            updateUIState();
            return;
        }
        const reader = new FileReader();
        reader.onload = (e) => {
            try {
                const data = new Uint8Array(e.target.result);
                const workbook = XLSX.read(data, { type: 'array',cellDates: false, cellStyles: false});
                workbookData = workbook;
                const firstSheetName = workbook.SheetNames[0];
                const worksheet = workbook.Sheets[firstSheetName];
                // Lấy JSON header: 1 (array of arrays) để dễ dàng kiểm soát index
                const json = XLSX.utils.sheet_to_json(worksheet, { header: 1, defval: '', raw: false });

                if (json.length === 0) {
                    log('File không có dữ liệu.', 'error');
                    updateUIState();
                    return;
                }

                const headerRow = json[0];
                statusColumnIndex = headerRow.findIndex(h => h && h.toString().trim().toLowerCase() === STATUS_COL_NAME.toLowerCase());
                let hasExistingStatus = statusColumnIndex !== -1;
                if (!hasExistingStatus) {
                    statusColumnIndex = headerRow.length;
                    headerRow.push(STATUS_COL_NAME);
                }
                const headersMap = headerRow.map(h => h.toString().trim() || null);
                const permalinkData = [];
                let foundWorkingIndex = -1;

                for (let i = 1; i < json.length; i++) {
                    const rawRow = json[i];

                    // Ensure row has enough columns
                    while (rawRow.length < statusColumnIndex + 1) {
                        rawRow.push('');
                    }

                    const cellValue = rawRow[urlColumnIndex];

                    if (cellValue && typeof cellValue === 'string') {
                        const trimmedValue = cellValue.trim();
                        const isURL = trimmedValue.includes('waze.com/editor') ||
                              trimmedValue.includes('waze.com/ul');
                        const isCoordinate = /^\s*\(?\s*-?\d+\.?\d*\s*,\s*-?\d+\.?\d*\s*\)?\s*$/.test(trimmedValue);

                        if (isURL || isCoordinate) {
                            // Build row object efficiently
                            const rowObject = {};

                            for (let idx = 0; idx < rawRow.length; idx++) {
                                const headerName = headersMap[idx];
                                const val = rawRow[idx];

                                if (headerName) {
                                    rowObject[headerName] = val;
                                }
                                // Column letter mapping
                                rowObject[getColumnLetter(idx)] = val;
                            }

                            const status = rawRow[statusColumnIndex].toString().trim();

                            permalinkData.push({
                                url: trimmedValue,
                                rowIndex: i,
                                status: status,
                                rowData: rowObject,
                                localFileIndexes: {
                                    urlCol: urlColumnIndex,
                                    statusCol: statusColumnIndex,
                                    sheetName: firstSheetName
                                }
                            });

                            const statusLower = status.toLowerCase();
                            if (foundWorkingIndex === -1 && statusLower === 'đang tạo') {
                                foundWorkingIndex = permalinkData.length - 1;
                            }
                            if (foundWorkingIndex === -1 && statusLower !== 'đã tạo') {
                                foundWorkingIndex = permalinkData.length - 1;
                            }
                        }
                    }
                }

                permalinks = permalinkData;

                // Ghi lại worksheet với cột Status (nếu mới tạo)
                const newWorksheet = XLSX.utils.aoa_to_sheet(json);
                workbookData.Sheets[firstSheetName] = newWorksheet;

                if (permalinks.length > 0) {
                    currentIndex = foundWorkingIndex !== -1 ? foundWorkingIndex : 0;
                    updateStatus('Đang tạo');
                    permalinks[currentIndex].status = 'Đang tạo';
                    processCurrentLink();
                } else {
                    log(`Không tìm thấy URL hoặc tọa độ hợp lệ trong cột ${urlColumnInput}.`, 'warn');
                }
                preloadApiData();
                updateUIState();
            } catch (err) {
                log(`Lỗi khi đọc file: ${err.message}`, 'error');
                console.error(err);
                updateUIState();
            }
        };
        reader.readAsArrayBuffer(file);
    }
    function saveWorkflows() {
        try {
            localStorage.setItem(STORAGE_KEY, JSON.stringify(allWorkflows));
            log("Đã lưu các workflows.", 'success');
        } catch (e) {
            log("Lỗi khi lưu workflows vào localStorage.", 'error');
        }
    }
    function loadWorkflows() {
        try {
            const saved = localStorage.getItem(STORAGE_KEY);
            if (saved) {
                allWorkflows = JSON.parse(saved);
            } else {
                allWorkflows = { ...defaultWorkflows };
                log("Đã tải các workflows mặc định. Các thay đổi sẽ được lưu lại.");
            }
        } catch (e) {
            log("Lỗi khi tải workflows từ localStorage, sử dụng các preset mặc định.", 'error');
            allWorkflows = { ...defaultWorkflows };
        }
    }
    function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); }
    function resetApiPanelState() {
        try {
            currentApiData = null; // Reset API data
            if (PROVIDERS_FETCH_API.vinfast) {
                // Dùng VinFast làm đại diện để reset UI panel
                PROVIDERS_FETCH_API.vinfast.updatePanel(null);
            }
            if (apiProviderPanel) {
                apiProviderPanel.style.display = 'none';
            }
        } catch (e) {
            console.error("Error resetting API panel state:", e);
        }
    }

    /**
* Cập nhật status cho URL hiện tại
*/
    function updateStatus(status) {
        const shouldSave = isStatusSavingEnabled();
        if (isGasMode) {
            if (permalinks[currentIndex]) {
                const item = permalinks[currentIndex];
                if (shouldSave) {
                    // Sử dụng Queue mới
                    updateGasStatusByRowIndex(item.rowIndex, status);
                }
                item.status = status;
                updateSaveButtonState();
            }
        } else {
            if (currentIndex >= 0 && permalinks[currentIndex]) {
                const item = permalinks[currentIndex];
                if (shouldSave) {
                    _updateLocalStatusCell(item.rowIndex, item.localFileIndexes.statusCol, status, item.localFileIndexes.sheetName);
                }
                item.status = status;
                hasUnsavedChanges = true;
                updateSaveButtonState();
            }
        }
    }
    /**
     * Tách hàm cập nhật trạng thái ô cụ thể trong workbookData (Local File only)
     * @param {number} rowIndex - Row index (0-based)
     * @param {number} colIndex - Column index (0-based)
     * @param {string} value - New status value
     * @param {string} sheetName - Target sheet name
     */
    function _updateLocalStatusCell(rowIndex, colIndex, value, sheetName) {
        if (!workbookData) return;
        try {
            const worksheet = workbookData.Sheets[sheetName];
            const cellAddress = XLSX.utils.encode_cell({ r: rowIndex, c: colIndex });
            // Cập nhật giá trị ô
            worksheet[cellAddress] = { t: 's', v: value };

            // Cập nhật lại trạng thái bộ nhớ để kích hoạt nút Save
            // hasUnsavedChanges = true;
            updateSaveButtonState();
        } catch (err) {
            log(`Lỗi khi cập nhật cell [${rowIndex}, ${colIndex}] trong file local.`, 'error');
            console.error(err);
        }
    }

    /**
* Lưu workbook ra file và trigger download
*/
    function saveWorkbookToFile() {
        if (isGasMode) {
            log("Chế độ Google Sheets: Status được lưu tự động lên sheet.", 'info');
            hasUnsavedChanges = false;
            updateSaveButtonState();
            return;
        }
        if (!workbookData) {
            log('Không có dữ liệu để lưu.', 'warn');
            return;
        }
        try {
            const wbout = XLSX.write(workbookData, { bookType: 'xlsx', type: 'array' });
            const blob = new Blob([wbout], { type: 'application/octet-stream' });
            const url = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = url;
            a.download = currentFileName;
            a.click();
            URL.revokeObjectURL(url);
            hasUnsavedChanges = false; // Reset cờ sau khi lưu
            updateSaveButtonState();
        } catch (err) {
            log(`Lỗi khi lưu file: ${err.message}`, 'error');
        }
    }
    /**
* Cập nhật trạng thái nút Save Status
*/
    function updateSaveButtonState() {
        const saveBtn = document.getElementById('save_status_btn');
        if (saveBtn) {
            saveBtn.disabled = !hasUnsavedChanges;
            if (hasUnsavedChanges) {
                saveBtn.classList.add('primary');
                saveBtn.style.animation = 'pulse 1.5s infinite';
            } else {
                saveBtn.classList.remove('primary');
                saveBtn.style.animation = '';
            }
        }
    }
    function waitForElement(selector, timeout = 7000) {
        return new Promise((resolve, reject) => {
            const intervalTime = 100;
            let elapsedTime = 0;
            const interval = setInterval(() => {
                const element = document.querySelector(selector);
                // Check if element exists and is visible (offsetParent is not null)
                if (element && element.offsetParent !== null) {
                    clearInterval(interval);
                    resolve(element);
                }
                elapsedTime += intervalTime;
                if (elapsedTime >= timeout) {
                    clearInterval(interval);
                    reject(new Error(`Element "${selector}" not found or not visible after ${timeout}ms`));
                }
            }, intervalTime);
        });
    }

    let logQueue = [];
    let logTimer = null;

    function log(message, type = 'normal') {
        const colorMap = {
            error: '#c0392b', success: '#27ae60', warn: '#e67e22',
            info: '#2980b9', special: '#8e44ad', normal: 'inherit'
        };

        logQueue.push({
            message,
            color: colorMap[type],
            time: new Date().toLocaleTimeString()
        });

        if (logTimer) clearTimeout(logTimer);

        logTimer = setTimeout(() => {
            const logBox = document.getElementById('log_info');
            if (!logBox) return;

            // Batch insert với DocumentFragment (nhanh hơn nhiều)
            const fragment = document.createDocumentFragment();
            const div = document.createElement('div');

            logQueue.forEach(({ message, color, time }) => {
                div.innerHTML = `<div style="color:${color}; border-bottom: 1px solid #f0f0f0;">[${time}] ${message}</div>`;
                fragment.insertBefore(div.firstChild, fragment.firstChild);
            });

            logBox.insertBefore(fragment, logBox.firstChild);

            // Giới hạn 20 dòng
            while (logBox.children.length > 20) {
                logBox.removeChild(logBox.lastChild);
            }

            logQueue.length = 0;
        }, 50); // Debounce 50ms
    }
    function createUI() {
        const panel = document.createElement('div');
        panel.id = 'workflow-engine-panel';
        panel.style.cssText = `
        position: fixed; top: 80px; left: 15px; background: rgba(255, 255, 255, 0.95); border: 1px solid #ccc;
        padding: 0; z-index: 1001; border-radius: 8px;
        font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; font-size: 11px; width: 300px;
        box-shadow: 0 4px 12px rgba(0,0,0,0.15);
        `;
        panel.innerHTML = `
        <h5 id="navigator-header" style="display: flex; justify-content: space-between; align-items: center; margin:0; padding: 3px 3px; cursor: grab; border-bottom: 1px solid #eee; background: #f7f7f7; border-top-left-radius: 8px; border-top-right-radius: 8px;">
            <span>WME Workflow Engine</span>
        <button id="toggle_panel_btn" title="Thu gọn Panel">▲</button>
        </h5>
        <div id="wwe-panel-content" style="padding: 5px;">
            <!-- Section 1: Điều khiển chính -->
            <h6 style="margin-top: 0; margin-bottom: 5px; color: #333;">Điều khiển chính</h6>
        <div style="display: flex; justify-content: space-between; align-items: center; gap: 5px;">
            <button id="prev_btn" class="nav-btn" title="Đối tượng trước (Mũi tên trái)" disabled>◀</button>
        <div style="display: flex; align-items: center; flex-grow: 1;">
            <input type="number" id="nav_index_input" min="1" style="width: 100%; text-align: center;" disabled>
                <span id="nav_total_count" style="margin-left: 5px; font-weight: bold;">/ N/A</span>
        </div>
        <button id="next_btn" class="nav-btn" title="Đối tượng tiếp theo (Mũi tên phải)" disabled>▶</button>
        </div>
        <div class="wwe-form-group" style="margin-top: 3px;">
            <label for="workflow_select">Chọn tác vụ:</label>
        <select id="workflow_select"></select>
        </div>
        <div class="wwe-form-group">
            <label for="workflow_variable_input">Giá trị nhập (cho <code>{{value}}</code>):</label>
                                                               <input type="text" id="workflow_variable_input" placeholder="Tên tỉnh/xã phường hoặc giá trị khác..." />
                                                               </div>
                                                               <button id="run_workflow_btn" class="action-btn primary" style="width: 100%;" title="Chạy workflow (Mũi tên xuống)" disabled>▶️ Chạy Workflow</button>
                                                               <button id="loop_workflow_btn" class="action-btn secondary" style="width: 100%; margin-top: 8px;" title="Lặp tự động chạy các tác vụ" disabled>🔁 Bắt đầu Lặp</button>
                                                               <hr style="border: 0; border-top: 1px solid #eee; margin: 5px 0;">
                                                               <!-- Nút Save Status -->
                                                               <button id="save_status_btn" class="action-btn" style="width: 100%; background-color: #28a745; color: white; border-color: #28a745;" title="Lưu trạng thái vào file" disabled>💾 Cập nhật Status</button>
                                                               <!-- Section 2: Accordion Items -->
                                                               <div class="accordion-container" style="margin-top: 5px;">
                                                               <!-- Accordion: Tải dữ liệu -->
                                                               <div class="accordion-item">
                                                               <button class="accordion-header">Tải & Cấu hình Dữ liệu</button>
                                                               <div class="accordion-content">
 
                                                               <div class="wwe-form-group">
                                                                <label>Nguồn dữ liệu:</label>
                                                                <div style="display: flex; gap: 10px;">
                                                                    <label style="font-weight: normal; cursor: pointer;"><input type="radio" name="data_source_mode" value="local" checked> File Local</label>
                                                                    <label style="font-weight: normal; cursor: pointer;"><input type="radio" name="data_source_mode" value="gas"> Google Sheets (GAS)</label>
                                                                </div>
                                                               </div>
 
                                                               <div id="local_file_config">
                                                                <div class="wwe-form-group">
                                                                    <label for="excel_file">Chọn File Excel/CSV:</label>
                                                                    <input type="file" id="excel_file" accept=".xlsx, .xls, .csv"/>
                                                                </div>
                                                                <div class="wwe-form-group row-group">
                                                                    <label for="url_column">Cột URL/Tọa độ (A-Z):</label>
                                                                    <input type="text" id="url_column" value="F" style="text-transform: uppercase; text-align: center; width: 50px;">
                                                                </div>
                                                               </div>
 
                                                               <div id="gas_config" style="display: none;">
                                                                <div class="wwe-form-group">
                                                                    <label for="gas_url">Google Web App URL (GAS):</label>
                                                                    <input type="text" id="gas_url" placeholder="https://script.google.com/macros/s/..." />
                                                                </div>
                                                                <div class="wwe-form-group">
        <label for="sheet_name_input">Tên Sheet (Tab) cần xử lý:</label>
        <input type="text" id="sheet_name_input" value="Sheet1" placeholder="Ví dụ: Data_To_Process">
        </div>
                                                                <div class="wwe-form-group">
                                                                    <label for="url_col_name">Tên Cột URL/Tọa độ (Header Name):</label>
                                                                    <input type="text" id="url_col_name" value="Link WME" placeholder="Ví dụ: Link WME">
                                                                </div>
                                                                <div class="wwe-form-group" style="margin-top: -5px; margin-bottom: 12px;">
                                                                    <label for="skip_done_check" style="font-weight: normal; font-size: 13px;">
                                                                        <input type="checkbox" id="skip_done_check" checked style="width: auto; margin-right: 5px;">
                                                                        Bỏ qua các dòng có Status là "Đã tạo" khi tải.
                                                                    </label>
                                                                </div>
                                                                <button id="load_sheet_btn" class="action-btn primary" style="width: 100%;">Tải Dữ liệu từ Sheet</button>
                                                               </div>
                                                               <button id="reselect_btn" class="action-btn secondary" style="width: 100%; margin-top: 3px;" title="Tải lại đối tượng hiện tại (Mũi tên lên)" disabled>🔄 Tải lại & Chọn</button>
                                                               </div>
                                                               </div>
                                                               <div class="accordion-item">
                                                               <button class="accordion-header">Zoom & Lưu Permalink</button>
                                                               <div class="accordion-content">
                                                               <div class="wwe-form-group">
 
                                                               <label for="coordinate_zoom">Zoom level cho tọa độ:</label>
                                                               <input type="number" id="coordinate_zoom" value="20" min="1" max="25" style="width: 50px;">
                                                               </div>
                                                               <div class="wwe-form-group" style="margin-top: 3px;">
                                                               <label style="font-weight: normal; display: flex; align-items: center; margin-bottom: 5px;">
        <input type="checkbox" id="save_status_enabled" checked style="width: auto; margin-right: 3px;">
        Tự động lưu Status
    </label>
                <label style="font-weight: normal; display: flex; align-items: center;">
                    <input type="checkbox" id="save_permalink_after_create" checked style="width: auto; margin-right: 3px;">
                    Tự động lưu Permalink
                </label>
            </div>
                                                               </div>
                                                               </div>
                                                               <!-- Accordion: Chức năng POI -->
                                                               <div class="accordion-item">
    <button class="accordion-header">Công cụ tạo POI</button>
    <div class="accordion-content">
        <label style="font-weight: bold; margin-bottom: 5px; display: block;">1. Loại POI:</label>
        <div style="display: flex; gap: 10px; margin-bottom: 8px;">
            <label><input type="radio" name="poi_creation_mode" value="none" checked> Không</label>
            <label><input type="radio" name="poi_creation_mode" value="point"> Điểm</label>
            <label><input type="radio" name="poi_creation_mode" value="area"> Vùng</label>
        </div>
 
        <label style="font-weight: bold; margin-bottom: 5px; display: block;">2. Phương thức:</label>
        <div style="display: flex; gap: 10px; margin-bottom: 8px;">
            <label><input type="radio" name="poi_method" value="auto" checked> Tự động</label>
            <label><input type="radio" name="poi_method" value="manual"> Click tay</label>
        </div>
 
        <div class="wwe-form-group">
            <label>Category:</label>
            <select id="poi_category_select"></select>
        </div>
    </div>
</div>
                                                               <!-- Accordion: Quản lý Workflows -->
<div class="accordion-item">
    <button class="accordion-header">Quản lý Workflows</button>
<div class="accordion-content">
    <div style="display: flex;margin: 5px 0 5px 0;">
        <button id="edit_workflow_btn" class="action-btn" style="flex-grow: 1;">✏️ Sửa</button>
<button id="new_workflow_btn" class="action-btn" style="flex-grow: 1;">➕ Tạo mới</button>
<button id="delete_workflow_btn" class="action-btn danger" style="flex-grow: 1;">🗑️ Xóa</button>
</div>
</div>
</div>
<!-- Accordion: Nhật ký -->
<div class="accordion-item">
    <button class="accordion-header">Nhật ký Hoạt động</button>
<div class="accordion-content">
    <div id="log_info" style="font-size: 11px; height: 120px; overflow-y: auto; border: 1px solid #eee; padding: 5px; background: #f8f9fa; border-radius: 4px; margin-top: 5px;"></div>
</div>
</div>
</div>
</div>
`;
        document.body.appendChild(panel);
        if (!document.getElementById('wme-wfe-styles')) {
            const style = document.createElement('style');
            style.id = 'wme-wfe-styles';
            style.innerHTML = `
        /* CSS Styles (Copied and optimized from original thought process) */
        #workflow-engine-panel button, #workflow-editor-modal button {
            border: 1px solid #ccc;
            background-color: #f0f0f0;
            border-radius: 4px;
            padding: 5px 10px;
            cursor: pointer;
            transition: background-color 0.2s, border-color 0.2s;
            font-family: inherit;
        }
#workflow-engine-panel button:hover:not(:disabled), #workflow-editor-modal button:hover:not(:disabled) {
    background-color: #e0e0e0;
}
#workflow-engine-panel button:disabled, #workflow-editor-modal button:disabled {
    cursor: not-allowed;
    opacity: 0.5;
}
#workflow-engine-panel input[type=text], #workflow-engine-panel input[type=number], #workflow-engine-panel input[type=file], #workflow-engine-panel select,
    #workflow-editor-modal input[type=text], #workflow-editor-modal select {
        border-radius: 4px;
        border: 1px solid #ccc;
        width: 100%;
        box-sizing: border-box;
        padding: 5px;
        font-family: inherit;
    }
#toggle_panel_btn, #toggle_editor_panel_btn {
    background: none;
    border: none;
    cursor: pointer;
    font-size: 15px;
    line-height: 1;
    padding: 0 5px;
    color: #888;
    font-weight: bold;
}
#workflow-engine-panel.is-collapsed #wwe-panel-content { display: none; }
#workflow-engine-panel.is-collapsed #navigator-header { border-bottom: none; }
#workflow-engine-panel .nav-btn { font-size: 12x; padding: 5px 12px; font-weight: bold; }
#workflow-engine-panel .action-btn { font-weight: 500; }
#workflow-engine-panel .action-btn.primary { background-color: #007bff; color: white; border-color: #007bff; }
#workflow-engine-panel .action-btn.primary:hover:not(:disabled) { background-color: #0056b3; }
#workflow-engine-panel .action-btn.secondary { background-color: #6c757d; color: white; border-color: #6c757d; }
#workflow-engine-panel .action-btn.secondary:hover:not(:disabled) { background-color: #5a6268; }
#workflow-engine-panel .action-btn.danger { background-color: #dc3545; color: white; border-color: #dc3545; }
#workflow-engine-panel .action-btn.danger:hover:not(:disabled) { background-color: #c82333; }
#loop_workflow_btn.looping { background-color: #ffc107; border-color: #ffc107; color: black; font-weight: bold; }
#loop_workflow_btn.looping:hover:not(:disabled) { background-color: #e0a800; border-color: #d39e00;}
.workflow-btns { display: flex; justify-content: flex-end; margin-top: 5px; }
.workflow-btns button:first-child { margin-right: auto; }
.wwe-form-group { display: flex; flex-direction: column; margin-bottom: 3px; }
.wwe-form-group label { font-weight: bold; font-size: 12px; margin-bottom: 3px; }
.wwe-form-group.row-group label { width: 100%; }
/* Accordion Styles */
.accordion-item { border-top: 1px solid #eee; }
.accordion-header {
    background-color: #f7f7f7; color: #444; cursor: pointer; padding: 10px;
    width: 100%; border: none; text-align: left; outline: none;
    font-size: 12px; transition: background-color 0.2s; font-weight: bold;
}
.accordion-header:hover { background-color: #e9e9e9; }
.accordion-header::after { content: ' ▼'; font-size: 10px; float: right; margin-top: 3px; }
.accordion-header.active::after { content: ' ▲'; }
.accordion-content {
    padding: 0 15px; background-color: white;
    max-height: 0; overflow: hidden;
    transition: max-height 0.3s ease-out;
}
/* Workflow Editor Modal Styles */
#workflow-editor-modal {
    display: none; position: fixed; z-index: 1002; left: 0; top: 0;
    width: 100%; height: 100%; overflow: auto; background-color: rgba(0,0,0,0.6);
}
#workflow-editor-content {
    background-color: #fefefe; padding: 0; border: 1px solid #888;
    width: 80%; max-width: 700px; border-radius: 8px; position: absolute;
    top: 50%; left: 50%; transform: translate(-50%, -50%);
    box-shadow: 0 5px 15px rgba(0,0,0,0.3);
}
#editor-header {
    display: flex; justify-content: space-between; align-items: center;
    padding: 5px 10px !important; cursor: grab; border-bottom: 1px solid #eee;
    background: #f7f7f7; border-top-left-radius: 8px; border-top-right-radius: 8px;
}
#workflow-editor-content.is-collapsed #editor-panel-content { display: none; }
#workflow-steps_list { list-style: none; padding: 0; min-height: 100px; border: 1px dashed #ccc; padding: 5px; border-radius: 4px; background: #fff; }
#workflow_steps_list li {
    display: flex; align-items: center; justify-content: space-between;
    padding: 6px 10px; border: 1px solid #ddd; margin-bottom: 3px;
    border-radius: 4px; background: #fafafa; cursor: grab;
}
#workflow_steps_list li .step-number { margin-right: 3px; font-weight: bold; color: #888; }
#workflow_steps_list li.editing { background-color: #e0eafc !important; border-color: #007bff !important; }
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.7; } }
#save_status_btn:not(:disabled):hover { background-color: #218838 !important; }
`;
            document.head.appendChild(style);
        }
        // const style = document.createElement('style');
        // style.innerHTML = ;
        // document.head.appendChild(style);
        // Accordion functionality for main panel
        panel.addEventListener('click', (e) => {
            const header = e.target.closest('.accordion-header');
            if (!header) return;

            // Đóng các accordion khác
            panel.querySelectorAll('.accordion-header.active').forEach(activeButton => {
                if (activeButton !== header) {
                    activeButton.classList.remove('active');
                    activeButton.nextElementSibling.style.maxHeight = null;
                }
            });

            header.classList.toggle('active');
            const content = header.nextElementSibling;
            content.style.maxHeight = content.style.maxHeight ? null : (content.scrollHeight + 10 + "px");
        });
        // Toggle panel button for main panel
        const toggleBtn = document.getElementById('toggle_panel_btn');
        registerEventCleanup(toggleBtn, 'click', (e) => {
            e.stopPropagation();
            const isCollapsed = panel.classList.toggle('is-collapsed');
            e.currentTarget.innerHTML = isCollapsed ? '▼' : '▲';
            e.currentTarget.title = isCollapsed ? 'Mở rộng Panel' : 'Thu gọn Panel';
        });

        // Radio buttons với delegation
        const localConfig = document.getElementById('local_file_config');
        const gasConfig = document.getElementById('gas_config');
        const saveStatusBtn = document.getElementById('save_status_btn');

        panel.addEventListener('change', (e) => {
            if (e.target.name === 'data_source_mode') {
                const mode = e.target.value;
                localConfig.style.display = mode === 'local' ? 'block' : 'none';
                gasConfig.style.display = mode === 'gas' ? 'block' : 'none';
                saveStatusBtn.textContent = mode === 'local' ?
                    '💾 Cập nhật Status vào File' :
                '☁️ Cập nhật Status (Tự động)';
            }
        });

        // File input - phải dùng direct listener
        document.getElementById('excel_file').addEventListener('change', handleFile, false);

        // Các button event - dùng registry
        const btnEvents = [
            ['load_sheet_btn', loadFromGoogleSheet],
            ['prev_btn', () => navigate(-1)],
            ['next_btn', () => navigate(1)],
            ['reselect_btn', processCurrentLink],
            ['run_workflow_btn', () => runSelectedWorkflow(false)],
            ['loop_workflow_btn', toggleWorkflowLoop],
            ['save_status_btn', saveWorkbookToFile],
            ['new_workflow_btn', () => openWorkflowEditor()],
            ['edit_workflow_btn', () => {
                const id = document.getElementById('workflow_select').value;
                if (id) openWorkflowEditor(id);
            }],
            ['delete_workflow_btn', deleteSelectedWorkflow]
        ];

        btnEvents.forEach(([id, handler]) => {
            const el = document.getElementById(id);
            if (el) registerEventCleanup(el, 'click', handler);
        });

        // Select events
        registerEventCleanup(
            document.getElementById('poi_category_select'),
            'change',
            (e) => { selectedSubCategory = e.target.value; }
        );

        registerEventCleanup(
            document.getElementById('nav_index_input'),
            'change',
            (e) => {
                const targetIndex = parseInt(e.target.value, 10);
                if (!isNaN(targetIndex)) navigate(0, targetIndex - 1);
            }
        );

        registerEventCleanup(
            document.getElementById('workflow_select'),
            'change',
            () => {
                resetApiPanelState();
                updateUIState();
                if (currentIndex >= 0 && permalinks.length > 0) {
                    processCurrentLink();
                }
            }
        );

        populateCategorySelector();
        loadGasSettings();
        makeDraggable(panel, document.getElementById('navigator-header'));

    };
    let apiProviderPanel = null;
    function populateCategorySelector() {
        const select = document.getElementById('poi_category_select');
        select.innerHTML = ''; // Clear existing options
        for (const cat of CATEGORIES) {
            // Tạo optgroup cho mỗi group key
            const optgroup = document.createElement('optgroup');
            optgroup.label = cat.key.replace(/_/g, ' ');
            // Thêm các sub categories
            for (const sub of cat.subs) {
                const option = document.createElement('option');
                option.value = sub;
                option.textContent = sub.replace(/_/g, ' ');
                if (sub === selectedSubCategory) {
                    option.selected = true;
                }
                optgroup.appendChild(option);
            }
            select.appendChild(optgroup);
        }
    }
    let vinfastPanel = null;
    function createProviderPanel(providerHandler) {
        if (apiProviderPanel) { // Đã đổi tên
            // Nếu đã có panel, chỉ cần reset nó
            providerHandler.updatePanel(null);
            document.getElementById('vf-panel-title').textContent = `${providerHandler.brand} Charging`;
            document.getElementById('vf-id-input').placeholder = `Nhập ID ${providerHandler.brand}`;
            document.getElementById('vf-transfer-name-btn').title = `Chuyển tên ${providerHandler.brand} vào trường Place Name`;
            return;
        }
        const panel = document.createElement('div');
        panel.id = 'vf-panel';
        panel.style.cssText = `
                position: fixed; top: 80px; left: 450px; background: rgba(255, 255, 255, 0.95); border: 1px solid #ccc;
padding: 10px; z-index: 1000; border-radius: 8px; width: 300px; max-height: 500px;
font-family: inherit; font-size: 13px; box-shadow: 0 4px 12px rgba(0,0,0,0.15);
transition: opacity 0.3s;
`;
        panel.innerHTML = `
                <h4 id="vf-panel-title" style="margin: 0 0 10px 0; color: #007bff; border-bottom: 1px solid #eee; padding-bottom: 5px;">${providerHandler.brand} Charging</h4>
<div style="display: flex; gap: 5px; margin-bottom: 10px;">
    <input type="text" id="vf-id-input" placeholder="Nhập ID ${providerHandler.brand}" style="flex-grow: 1; padding: 4px;">
        <button id="vf-fetch-btn" class="action-btn primary" style="padding: 4px 8px; font-size: 12px; height: auto;">Fetch</button>
</div>
<div id="vf-data-display" style="display: none;">
    <div id="vf-name-line" style="margin-bottom: 8px; font-weight: bold; display: flex; align-items: center; border-bottom: 1px dashed #ddd; padding-bottom: 5px;">
        <span style="flex-shrink: 0; margin-right: 5px;">Tên:</span>
<span id="vf-name-text" style="flex-grow: 1; margin-right: 5px;"></span>
<!-- Nút chuyển tên -->
<button id="vf-transfer-name-btn" title="Chuyển tên ${providerHandler.brand} vào trường Place Name" style="padding: 2px 5px; font-size: 12px; background-color: #f39c12; color: white; border-color: #f39c12; line-height: 1;"><-> Swap</button>
</div>
<div style="display: flex; gap: 5px; margin-bottom: 10px;">
    <button id="vf-images-btn" class="action-btn" style="flex: 1; background-color: #17a2b8; color: white; border-color: #17a2b8; font-size: 12px;">📷 Xem Ảnh</button>
</div>
<h5 style="margin: 5px 0;">Cổng sạc & Công suất:</h5>
<div id="vf-connectors-info" style="border: 1px solid #ddd; padding: 8px; border-radius: 4px; background: #f9f9f9; max-height: 200px; overflow-y: auto;">
    <i style="color: #777;">Chưa có dữ liệu cổng sạc.</i>
</div>
</div>
`;
        document.body.appendChild(panel);
        apiProviderPanel = panel; // Đã đổi tên
        // Bật draggable cho panel
        makeDraggable(panel, panel.querySelector('#vf-panel-title'));
        // Hàm xử lý fetch chung cho panel
        async function handleProviderFetchClick() {
            const id = document.getElementById('vf-id-input').value.trim();
            if (id) {
                const fetchBtn = document.getElementById('vf-fetch-btn');
                if (fetchBtn) {
                    fetchBtn.textContent = 'Fetching...';
                    fetchBtn.disabled = true;
                }
                try {
                    // Gọi fetchData từ providerHandler
                    await providerHandler.fetchData(id);
                } catch (e) {
                    log(`Lỗi khi fetch data cho ${providerHandler.brand}: ${e.message}`, 'error');
                } finally {
                    // updatePanel sẽ được gọi trong fetchData, fetchBtn sẽ được reset ở đó
                }
            } else {
                log(`Vui lòng nhập ID ${providerHandler.brand}.`, 'warn');
            }
        }
        // Attach listeners
        document.getElementById('vf-fetch-btn').addEventListener('click', handleProviderFetchClick);
        // Chuyển nút Transfer Name sang gọi trực tiếp từ provider
        document.getElementById('vf-transfer-name-btn').addEventListener('click', () => {
            if (providerHandler.transferNameToWME) {
                providerHandler.transferNameToWME();
            }
        });
        // Chuyển nút Images sang gọi trực tiếp từ provider
        document.getElementById('vf-images-btn').addEventListener('click', () => {
            if (providerHandler.showImagesPopup) {
                providerHandler.showImagesPopup();
            }
        });
        providerHandler.updatePanel(null); // Reset UI sau khi tạo
    }
    /**
    * Makes an element draggable using its handle.
    * @param {HTMLElement} elementToMove The element that will be moved.
    * @param {HTMLElement} dragHandle The element that acts as the drag handle.
    */
    function makeDraggable(elementToMove, dragHandle) {
        let offsetX, offsetY;
        let isDragging = false;
        dragHandle.onmousedown = (e) => {
            e.preventDefault();
            isDragging = true;
            dragHandle.style.cursor = 'grabbing'; // Change cursor while dragging
            // Get the element's current computed style to check its position
            const computedStyle = getComputedStyle(elementToMove);
            if (computedStyle.position === 'static') {
                elementToMove.style.position = 'absolute'; // Change to absolute if not already positioned
            }
            // If the element has a transform property (like translate for centering),
            // apply that transform to its top/left before dragging starts.
            if (computedStyle.transform && computedStyle.transform !== 'none') {
                const matrix = new DOMMatrixReadOnly(computedStyle.transform);
                // Adjust element's current top/left by its transform translate values
                elementToMove.style.left = (elementToMove.offsetLeft + matrix.m41) + 'px';
                elementToMove.style.top = (elementToMove.offsetTop + matrix.m42) + 'px';
                elementToMove.style.transform = 'none'; // Clear the transform
            }
            // Calculate the initial offset from the element's current position to the mouse click
            const rect = elementToMove.getBoundingClientRect();
            offsetX = e.clientX - rect.left;
            offsetY = e.clientY - rect.top;
            document.onmousemove = (ev) => {
                if (!isDragging) return;
                // Calculate new position based on mouse position and initial offset
                elementToMove.style.left = (ev.clientX - offsetX) + 'px';
                elementToMove.style.top = (ev.clientY - offsetY) + 'px';
            };
            document.onmouseup = () => {
                isDragging = false;
                document.onmouseup = null;
                document.onmousemove = null;
                dragHandle.style.cursor = 'grab'; // Reset cursor
            };
        };
    }
    function isStatusSavingEnabled() {
        return document.getElementById('save_status_enabled')?.checked === true;
    }
    function createWorkflowEditorModal() {
        const modal = document.createElement('div');
        modal.id = 'workflow-editor-modal';
        // CSS giữ nguyên hoặc tùy chỉnh nhẹ
        modal.innerHTML = `
            <div id="workflow-editor-content" style="width: 600px;">
                <h3 id="editor-header">
                    <span id="editor-title">Chỉnh sửa Workflow (SDK Mode)</span>
<span id="close-modal" style="float:right; cursor:pointer; font-size:20px;">×</span>
</h3>
<div id="editor-panel-content" style="padding: 15px; max-height: 70vh; overflow-y: auto;">
    <input type="hidden" id="editing_workflow_id">
        <div class="wwe-form-group">
            <label>Tên tác vụ:</label>
<input type="text" id="workflow_name_input" placeholder="Nhập tên tác vụ...">
    </div>
<hr>
    <h4>Chọn các hành động thực thi:</h4>
<div id="sdk-tasks-container">
    <!-- Tasks will be injected here via JS -->
    </div>
<div class="workflow-btns" style="margin-top: 15px; border-top: 1px solid #eee; padding-top: 10px;">
    <button id="save_workflow_btn" class="primary">Lưu các tác vụ</button>
<button id="cancel_workflow_btn" class="secondary">Hủy</button>
</div>
</div>
</div>`;
        document.body.appendChild(modal);
        // Event listeners
        document.getElementById('close-modal').onclick = closeWorkflowEditor;
        document.getElementById('cancel_workflow_btn').onclick = closeWorkflowEditor;
        document.getElementById('save_workflow_btn').onclick = saveWorkflowFromEditor;
        makeDraggable(document.getElementById('workflow-editor-content'), document.getElementById('editor-header'));
    }
    function renderSdkTasksInEditor(existingTasks = []) {
        const container = document.getElementById('sdk-tasks-container');
        container.innerHTML = '';
        Object.keys(SDK_REGISTRY).forEach(taskId => {
            const def = SDK_REGISTRY[taskId];
            // Kiểm tra xem task này đã có trong workflow cũ chưa
            const existing = existingTasks.find(t => t.taskId === taskId) || { enabled: false, params: {} };
            const wrapper = document.createElement('div');
            wrapper.style.cssText = "border: 1px solid #ddd; margin-bottom: 8px; padding: 10px; border-radius: 4px; background: #fafafa;";
            // Header: Checkbox + Name
            const header = document.createElement('div');
            header.innerHTML = `
                <label style="font-weight: bold; color: #333; display: flex; align-items: center; cursor: pointer;">
                    <input type="checkbox" class="task-enable-cb" data-task-id="${taskId}" ${existing.enabled ? 'checked' : ''} style="width: auto; margin-right: 8px;">
                    ${def.name}
                </label>
                <div style="font-size: 0.85em; color: #666; margin-left: 24px; margin-bottom: 5px;">${def.description}</div>
            `;
            wrapper.appendChild(header);
            // Params Inputs
            if (def.params.length > 0) {
                const paramsDiv = document.createElement('div');
                paramsDiv.className = 'task-params';
                paramsDiv.style.cssText = `margin-left: 24px; display: ${existing.enabled ? 'block' : 'none'};`;
                def.params.forEach(p => {
                    const row = document.createElement('div');
                    row.style.marginBottom = '5px';
                    const val = existing.params[p.key] || '';
                    row.innerHTML = `
                        <label style="display:block; font-size: 11px; margin-bottom: 2px;">${p.label}:</label>
                        <input type="text" class="task-param-input" data-task-id="${taskId}" data-param-key="${p.key}" value="${val}" placeholder="${p.placeholder}" style="width: 100%;">
                    `;
                    paramsDiv.appendChild(row);
                });
                wrapper.appendChild(paramsDiv);
            }
            container.appendChild(wrapper);
        });
        // Toggle hiển thị params khi check/uncheck
        container.querySelectorAll('.task-enable-cb').forEach(cb => {
            cb.addEventListener('change', (e) => {
                const paramsDiv = e.target.closest('div').parentElement.querySelector('.task-params');
                if (paramsDiv) paramsDiv.style.display = e.target.checked ? 'block' : 'none';
            });
        });
    }
    function registerHotkeys() {
        document.addEventListener('keydown', (e) => {
            // Do not trigger hotkeys if focus is in an input field or a text area,
            // or if the event originated from within our panels/modals
            if (e.target.closest('#workflow-engine-panel, #workflow-editor-modal') || e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') {
                return;
            }
            if (e.key === 'ArrowRight' && !document.getElementById('next_btn').disabled) {
                e.preventDefault();
                document.getElementById('next_btn').click();
            }
            if (e.key === 'ArrowLeft' && !document.getElementById('prev_btn').disabled) {
                e.preventDefault();
                document.getElementById('prev_btn').click();
            }
            if (e.key === 'ArrowUp' && !document.getElementById('reselect_btn').disabled) {
                e.preventDefault();
                document.getElementById('reselect_btn').click();
            }
            if (e.key === 'ArrowDown' && !document.getElementById('run_workflow_btn').disabled) {
                e.preventDefault();
                document.getElementById('run_workflow_btn').click();
            }
            if (e.key.toLowerCase() === 'u') {
                e.preventDefault();
                updateStatus('Không tồn tại');
                permalinks[currentIndex].status = 'Không tồn tại';
                window.alert('Workflow Engine: Đã đánh dấu "Không tồn tại" cho vị trí này.');
            }
        });
    }
    function navigate(direction, targetIndex = null) {
        placeholderCache.clear();
        if (isLooping) return;
        if (permalinks.length === 0) return;

        let newIndex = (targetIndex !== null) ? targetIndex : (currentIndex + direction);

        if (newIndex >= 0 && newIndex < permalinks.length) {

            const previousIndex = currentIndex;
            const navigationDirection = newIndex - currentIndex;

            if (previousIndex >= 0 && previousIndex !== newIndex) {
                const currentItemStatus = permalinks[previousIndex].status?.toLowerCase();
                const shouldSavePermalink = document.getElementById('save_permalink_after_create')?.checked;
                if (shouldSavePermalink && navigationDirection > 0) {
                    const newPermalink = wmeSDK.Map.getPermalink();
                    if (newPermalink) {
                        updatePermalinkInWorkbook(previousIndex, newPermalink);
                    } else {
                        log(`⚠️ Không lấy được Permalink cho mục ${previousIndex + 1}.`, 'warn');
                    }
                }
                if (currentItemStatus === 'đang tạo' || currentItemStatus === '') {
                    updateStatusByIndex(previousIndex, 'Đã tạo');
                }
            }

            currentIndex = newIndex;
            // Nếu mục mới chưa có status hoặc là "Đã tạo", đặt lại là Đang tạo
            if (!permalinks[currentIndex].status || permalinks[currentIndex].status.toLowerCase() !== 'đang tạo') {
                updateStatus('Đang tạo');
                // Cập nhật trạng thái local ngay lập tức
                permalinks[currentIndex].status = 'Đang tạo';
            }

            updateUIState();
            processCurrentLink();
            preloadApiData();
        }
    }
    /**
    * Cập nhật status cho một URL cụ thể theo index
    */
    function updateStatusByIndex(index, status) {
        const shouldSave = isStatusSavingEnabled();
        if (isGasMode) {
            if (index >= 0 && permalinks[index]) {
                const item = permalinks[index];
                if (shouldSave) {
                    // Sử dụng Queue mới
                    updateGasStatusByRowIndex(item.rowIndex, status);
                }
                item.status = status;
            }
        } else {
            if (index >= 0 && permalinks[index] && permalinks[index].localFileIndexes) {
                const item = permalinks[index];
                if (shouldSave) {
                    _updateLocalStatusCell(item.rowIndex, item.localFileIndexes.statusCol, status, item.localFileIndexes.sheetName);
                }
                item.status = status;
            }
        }
    }
    function extractCoords(item) {
        // 1. Thử lấy từ API data nếu có
        if (currentApiData?.lat || currentApiData?.coordinates?.latitude) {
            return {
                lat: parseFloat(currentApiData.lat || currentApiData.coordinates.latitude),
                lon: parseFloat(currentApiData.lng || currentApiData.coordinates.longitude)
            };
        }
        // 2. Thử lấy từ định dạng tọa độ thô "10.123, 106.123"
        const coordMatch = item.url.match(/(-?\d+\.?\d*)\s*,\s*(-?\d+\.?\d*)/);
        if (coordMatch) return { lat: parseFloat(coordMatch[1]), lon: parseFloat(coordMatch[2]) };

        // 3. Thử lấy bằng Regex từ URL Waze (phòng trường hợp URL lỗi nhưng vẫn có số)
        const urlMatch = item.url.match(/lat=(-?\d+\.\d+)&lon=(-?\d+\.\d+)/);
        if (urlMatch) return { lat: parseFloat(urlMatch[1]), lon: parseFloat(urlMatch[2]) };

        return null;
    }
    async function processCurrentLink() {
        if (currentIndex < 0 || currentIndex >= permalinks.length) return;
        const item = permalinks[currentIndex]; // permalinks giờ sẽ chứa object {url, rowIndex, rowData...}
        currentRowData = item.rowData; // Cập nhật dữ liệu hàng hiện tại để hàm replacePlaceholders dùng
        currentApiData = null; // Reset API data cũ
        // 1. Xác định Workflow, Provider và API ID cần fetch
        const selectedWorkflowId = document.getElementById('workflow_select')?.value;
        const selectedWorkflow = selectedWorkflowId ? allWorkflows[selectedWorkflowId] : null;
        let targetApiId = '';
        let providerHandler = null;
        if (selectedWorkflow && Array.isArray(selectedWorkflow.tasks)) {
            const chargeApiTask = selectedWorkflow.tasks.find(t => t.enabled && t.taskId === 'update_charge_station_api');
            if (chargeApiTask) {
                const providerKey = (chargeApiTask.params.provider || 'vinfast').toLowerCase();
                providerHandler = PROVIDERS_FETCH_API[providerKey];
                // --- LẤY ID TỪ PLACEHOLDER TRONG CẤU HÌNH WORKFLOW ---
                const idPlaceholder = chargeApiTask.params.id || ''; // Ví dụ: "{{A}}"
                // Sử dụng replacePlaceholders để resolve cột A, B, C... thành giá trị thực
                let resolvedId = replacePlaceholders(idPlaceholder);
                targetApiId = resolvedId ? resolvedId.toString().trim() : '';
            }
        }
        // 2. Cập nhật UI và Fetch tọa độ nếu cần
        const vfIdInput = document.getElementById('vf-id-input');
        if (vfIdInput) {
            vfIdInput.value = targetApiId; // Cập nhật input ID trên panel
        }
        currentApiData = null;
        let targetLat = null;
        let targetLng = null;
        let isCoordinateFromAPI = false;
        // Nếu workflow có API sạc và có provider, tạo/reset panel và fetch data nếu có ID
        if (providerHandler) {
            createProviderPanel(providerHandler);
            if (targetApiId) {
                try {
                    // Gọi fetchData từ providerHandler
                    const apiResult = await providerHandler.fetchData(targetApiId);
                    if (apiResult && (apiResult.lat || (apiResult.coordinates && apiResult.coordinates.latitude))) {
                        coords = {
                            lat: parseFloat(apiResult.lat || apiResult.coordinates.latitude),
                            lon: parseFloat(apiResult.lng || apiResult.coordinates.longitude)
                        };
                        isCoordinateFromAPI = true;
                    }
                } catch (e) { log(`Lỗi API ${providerHandler.brand}: ${e.message}`, 'warn'); }
            }
        } else if (apiProviderPanel) {
            // Nếu workflow không yêu cầu API sạc, ẩn panel nếu nó tồn tại
            apiProviderPanel.style.display = 'none';
        }
        const coords = extractCoords(item);
        const createMode = document.querySelector('input[name="poi_creation_mode"]:checked')?.value || 'none';
        const method = document.querySelector('input[name="poi_method"]:checked').value;
        if (coords) {
            const zoom = parseInt(document.getElementById('coordinate_zoom')?.value || 20);
            W.map.setCenter(WazeWrap.Geometry.ConvertTo900913(coords.lon, coords.lat), zoom);
            if (createMode !== 'none' && !item.url.includes('venues=')) {
                await delay(50);
                createWazePOI(coords.lat, coords.lon, createMode, method);
            }
        }

        // Cuối cùng mới thực hiện điều hướng (nếu URL cũ có ID sẵn)
        parseWazeUrlAndNavigate(item.url);
    }
    function updatePermalinkInWorkbook(index, newPermalink) {
        if (!document.getElementById('save_permalink_after_create')?.checked) return;

        const item = permalinks[index];
        if (!item) return;

        if (isGasMode) {
            updateGasStatusByRowIndex(item.rowIndex, item.status, newPermalink);
            item.url = newPermalink;
            const urlColName = document.getElementById('url_col_name')?.value?.trim() || 'Link WME';
            item.rowData[urlColName] = newPermalink;
        } else {
            if (!workbookData || !item.localFileIndexes) return;
            try {
                const sheet = workbookData.Sheets[item.localFileIndexes.sheetName];
                const statusColIndex = item.localFileIndexes.statusCol;
                const newColIndex = statusColIndex + 1;
                const NEW_COL_NAME = "New Permalink";
                const rowIndex = item.rowIndex; // Excel row index (0-based)
                const headerAddress = XLSX.utils.encode_cell({ r: 0, c: newColIndex });
                if (!sheet[headerAddress] || sheet[headerAddress].v !== NEW_COL_NAME) {
                    sheet[headerAddress] = { t: 's', v: NEW_COL_NAME };
                }
                const cellAddress = XLSX.utils.encode_cell({ r: rowIndex, c: newColIndex });
                sheet[cellAddress] = { t: 's', v: newPermalink };
                const range = XLSX.utils.decode_range(sheet['!ref']);
                if (newColIndex > range.e.c) {
                    range.e.c = newColIndex;
                    sheet['!ref'] = XLSX.utils.encode_range(range);
                }
                item.url = newPermalink;
                const urlColLetter = getColumnLetter(item.localFileIndexes.urlCol);
                item.rowData[urlColLetter] = newPermalink; // Cập nhật bằng chữ cái
                hasUnsavedChanges = true;
                updateSaveButtonState();

            } catch (err) {
                log(`❌ Lỗi khi cập nhật file local: ${err.message}`, "error");
                console.error(err);
            }
        }
    }
    async function parseWazeUrlAndNavigate(value) {
        try {
            const trimmedValue = value.trim();
            const coordMatch = trimmedValue.match(/^\s*\(?\s*(-?\d+\.?\d*)\s*,\s*(-?\d+\.?\d*)\s*\)?\s*$/);
            if (coordMatch) {
                // Xử lý tọa độ
                const lat = parseFloat(coordMatch[1]);
                const lon = parseFloat(coordMatch[2]);
                if (isNaN(lat) || isNaN(lon)) {
                    throw new Error('Tọa độ không hợp lệ.');
                }
                // Lấy zoom level từ input hoặc dùng mặc định
                const zoomInput = document.getElementById('coordinate_zoom');
                const defaultZoom = zoomInput ? parseInt(zoomInput.value, 10) : 20;
                W.map.setCenter(WazeWrap.Geometry.ConvertTo900913(lon, lat), defaultZoom);
                W.selectionManager.setSelectedModels([]);
                const createMode = document.querySelector('input[name="poi_creation_mode"]:checked').value;
                if (createMode !== 'none') {
                    // Đợi map load xong
                    await delay(50);
                    createWazePOI(lat, lon, createMode);
                    // Đợi POI được tạo và WME chọn nó
                    await delay(100);
                }
                return;
            }
            // Nếu không phải tọa độ, xử lý như URL bình thường
            const parsedUrl = new URL(trimmedValue);
            const params = parsedUrl.searchParams;
            const lon = parseFloat(params.get('lon'));
            const lat = parseFloat(params.get('lat'));
            const zoom = parseInt(params.get('zoomLevel') || params.get('zoom'), 10) + 2;
            const segmentIDs = (params.get('segments') || '').split(',').filter(id => id);
            const venueIDs = (params.get('venues') || '').split(',').filter(id => id);
            // Set map center and zoom
            W.map.setCenter(WazeWrap.Geometry.ConvertTo900913(lon, lat), zoom);
            // Wait for model to be ready, then select objects
            WazeWrap.Model.onModelReady(() => {
                (async () => {
                    await delay(1000);
                    let objectsToSelect = [];
                    if (segmentIDs.length > 0) {
                        const segments = segmentIDs.map(id => W.model.segments.getObjectById(id)).filter(Boolean);
                        if (segments.length === 0) {
                            log(`Cảnh báo: Không tìm thấy segment nào từ ID ${segmentIDs.join(',')} sau khi tải.`, 'warn');
                        } else {
                            objectsToSelect.push(...segments);
                        }
                    }
                    if (venueIDs.length > 0) {
                        const venues = venueIDs.map(id => W.model.venues.getObjectById(id)).filter(Boolean);
                        if (venues.length === 0) {
                            log(`Cảnh báo: Không tìm thấy venue nào từ ID ${venueIDs.join(',')} sau khi tải.`, 'warn');
                        } else {
                            objectsToSelect.push(...venues);
                        }
                    }
                    if (objectsToSelect.length > 0) {
                        W.selectionManager.setSelectedModels(objectsToSelect);
                    }
                })();
            }, true);
        } catch (error) {
            log(`Lỗi khi xử lý "${value}": ${error.message}`, 'error');
            console.error(error);
        }
    }
    function updateUIState() {
        const hasLinks = permalinks.length > 0;
        const navIndexInput = document.getElementById('nav_index_input');
        const navTotalCount = document.getElementById('nav_total_count');
        const workflowSelect = document.getElementById('workflow_select');
        const loopBtn = document.getElementById('loop_workflow_btn');
        const runWorkflowBtn = document.getElementById('run_workflow_btn');
        // Navigation buttons
        document.getElementById('prev_btn').disabled = !hasLinks || currentIndex <= 0 || isLooping;
        document.getElementById('next_btn').disabled = !hasLinks || currentIndex >= permalinks.length - 1 || isLooping;
        document.getElementById('reselect_btn').disabled = !hasLinks || isLooping;
        // Index input
        navIndexInput.disabled = !hasLinks || isLooping;
        if (hasLinks) {
            navIndexInput.value = currentIndex + 1;
            navIndexInput.max = permalinks.length;
            navTotalCount.textContent = ` / ${permalinks.length}`;
        } else {
            navIndexInput.value = '';
            navTotalCount.textContent = '/ N/A';
        }
        // Workflow action buttons
        runWorkflowBtn.disabled = !workflowSelect.value || isLooping;
        loopBtn.disabled = !hasLinks;
        // Workflow editor related buttons
        document.getElementById('excel_file').disabled = isLooping;
        document.getElementById('workflow_select').disabled = isLooping;
        document.getElementById('workflow_variable_input').disabled = isLooping;
        document.getElementById('edit_workflow_btn').disabled = !workflowSelect.value || isLooping;
        document.getElementById('delete_workflow_btn').disabled = !workflowSelect.value || isLooping;
        document.getElementById('new_workflow_btn').disabled = isLooping;
        // Update Save Status button
        updateSaveButtonState();
        // Loop button visual state
        if (isLooping) {
            loopBtn.textContent = '⏹️ Dừng Lặp';
            loopBtn.classList.add('looping');
            loopBtn.classList.remove('secondary');
        } else {
            loopBtn.textContent = '🔁 Bắt đầu Lặp';
            loopBtn.classList.remove('looping');
            if (hasLinks) loopBtn.classList.add('secondary');
        }
    }
    function populateWorkflowSelector() {
        const select = document.getElementById('workflow_select');
        const currentId = select.value; // Store current selection
        select.innerHTML = ''; // Clear existing options
        const emptyOption = document.createElement('option');
        emptyOption.value = '';
        emptyOption.textContent = Object.keys(allWorkflows).length === 0 ? '--- Không có workflow ---' : '--- Chọn workflow ---';
        select.appendChild(emptyOption);
        for (const id in allWorkflows) {
            const option = document.createElement('option');
            option.value = id;
            option.textContent = allWorkflows[id].name;
            select.appendChild(option);
        }
        // Restore previous selection or select the first valid one
        if (currentId && allWorkflows[currentId]) {
            select.value = currentId;
        } else if (Object.keys(allWorkflows).length > 0) {
            // Select the first workflow if none were selected, but prefer the empty option if it exists
            const firstWorkflowId = Object.keys(allWorkflows)[0];
            if (firstWorkflowId) {
                select.value = firstWorkflowId;
            }
        } else {
            select.value = ''; // No workflows, ensure no value is set
        }
        updateUIState(); // Update other UI elements based on selection
    }
    function deleteSelectedWorkflow() {
        const select = document.getElementById('workflow_select');
        const idToDelete = select.value;
        const workflowName = allWorkflows[idToDelete]?.name;
        if (!idToDelete) {
            alert("Vui lòng chọn một workflow để xóa.");
            return;
        }
        if (confirm(`Bạn có chắc chắn muốn xóa workflow "${workflowName}" không?`)) {
            delete allWorkflows[idToDelete];
            saveWorkflows();
            populateWorkflowSelector();
            log(`Đã xóa workflow: "${workflowName}"`, 'info');
        }
    }
    function openWorkflowEditor(workflowId = null) {
        const modal = document.getElementById('workflow-editor-modal');
        const nameInput = document.getElementById('workflow_name_input');
        const idInput = document.getElementById('editing_workflow_id');
        const title = document.getElementById('editor-title');
        if (workflowId && allWorkflows[workflowId]) {
            const wf = allWorkflows[workflowId];
            title.textContent = "Chỉnh sửa Workflow (SDK)";
            nameInput.value = wf.name;
            idInput.value = workflowId;
            renderSdkTasksInEditor(wf.tasks || []);
        } else {
            title.textContent = "Tạo Workflow Mới (SDK)";
            nameInput.value = '';
            idInput.value = '';
            renderSdkTasksInEditor([]);
        }
        modal.style.display = 'block';
    }
    function closeWorkflowEditor() {
        document.getElementById('workflow-editor-modal').style.display = 'none';
    }
    function saveWorkflowFromEditor() {
        const name = document.getElementById('workflow_name_input').value.trim();
        if (!name) return alert("Vui lòng nhập tên tác vụ.");
        const tasks = [];
        document.querySelectorAll('.task-enable-cb').forEach(cb => {
            if (cb.checked) {
                const taskId = cb.dataset.taskId;
                const params = {};
                // Thu thập params
                document.querySelectorAll(`.task-param-input[data-task-id="${taskId}"]`).forEach(inp => {
                    params[inp.dataset.paramKey] = inp.value;
                });
                tasks.push({ taskId, enabled: true, params });
            }
        });
        if (tasks.length === 0) return alert("Vui lòng chọn ít nhất một hành động.");
        let id = document.getElementById('editing_workflow_id').value;
        if (!id) id = `sdk_wf_${Date.now()}`;
        allWorkflows[id] = { name, tasks };
        saveWorkflows();
        populateWorkflowSelector();
        document.getElementById('workflow_select').value = id;
        closeWorkflowEditor();
        log(`Đã lưu workflow SDK "${name}"`, 'success');
    }
    function loadGasSettings() {
        try {
            const saved = localStorage.getItem(STORAGE_KEY_SETTINGS);
            if (saved) {
                const settings = JSON.parse(saved);
                document.getElementById('gas_url').value = settings.gasUrl || '';
                // Sửa: Chỉ truy cập các ID có sẵn
                const sheetNameInput = document.getElementById('sheet_name_input');
                if (sheetNameInput) sheetNameInput.value = settings.sheetName || 'Sheet1';

                const urlColNameInput = document.getElementById('url_col_name');
                if (urlColNameInput) urlColNameInput.value = settings.urlCol || 'Link WME';

                const skipDoneCheck = document.getElementById('skip_done_check');
                if (skipDoneCheck) skipDoneCheck.checked = settings.skipDone || false;
            }
        } catch (e) {
            log('Lỗi khi tải cài đặt GAS.', 'error');
        }
    }
    resetData()
    /**
     * Tải dữ liệu từ Google Sheets thông qua GAS Web App.
     */
    function loadFromGoogleSheet() {
        const scriptUrl = document.getElementById('gas_url')?.value?.trim() || '';
        const sheetName = document.getElementById('sheet_name_input')?.value?.trim() || '';
        const urlColName = document.getElementById('url_col_name')?.value?.trim() || '';
        const skipDone = document.getElementById('skip_done_check')?.checked || false;

        if (!scriptUrl) { alert("Vui lòng nhập Web App URL!"); return; }

        // Lưu cài đặt GAS
        localStorage.setItem(STORAGE_KEY_SETTINGS, JSON.stringify({
            gasUrl: scriptUrl,
            sheetName: sheetName,
            urlCol: urlColName,
            skipDone: skipDone
        }));

        // Reset trạng thái
        permalinks = [];
        currentIndex = -1;
        previousIndex = -1;
        isGasMode = true;
        gasHeaders = null;
        hasUnsavedChanges = false;
        gasWriteQueue.length = 0;
        gasWriteProcessing = false;

        log("⏳ Đang tải dữ liệu từ Google Sheets...");
        const loadBtn = document.getElementById('load_sheet_btn');
        if (loadBtn) loadBtn.disabled = true;
        const readUrl = `${scriptUrl}?action=get&sheetName=${encodeURIComponent(sheetName)}`;

        GM_xmlhttpRequest({
            method: "GET",
            url: readUrl,
            onload: function (response) {
                if (loadBtn) loadBtn.disabled = false;
                if (response.status !== 200) {
                    log(`❌ Lỗi kết nối GAS: Status ${response.status}. Kiểm tra URL và quyền truy cập.`, 'error');
                    isGasMode = false;
                    updateUIState();
                    return;
                }
                try {
                    const json = JSON.parse(response.responseText);
                    if (json.result === "success" && json.data) {
                        // Thiết lập gasHeaders và xử lý dữ liệu
                        if (json.headers && Array.isArray(json.headers)) {
                            gasHeaders = json.headers;
                            log("Đã load dữ liệu từ GG Sheets thành công", 'success')
                        } else {
                            log("Cảnh báo: Không nhận được headers từ GAS, mapping {{tên cột}} có thể không chính xác.", 'warn');
                        }

                        let foundIndex = -1;
                        let tempPermalinks = [];

                        json.data.forEach(row => {
                            const url = row[urlColName] || "";
                            const stt = row[STATUS_COL_NAME] || "";
                            const rowIndex = row["_rowIndex"] || null; // _rowIndex là cột đặc biệt dùng để ghi lại

                            if (url && rowIndex !== null) {
                                const statusTrimmed = stt.toString().trim().toLowerCase();
                                if (skipDone && statusTrimmed === 'đã tạo') {
                                    return; // Bỏ qua nếu skipDone được bật
                                }

                                tempPermalinks.push({
                                    url: url.toString().trim(),
                                    rowIndex: rowIndex,
                                    status: statusTrimmed,
                                    rowData: row // Lưu Object data
                                });

                                // Tìm index để bắt đầu
                                if (foundIndex === -1 && statusTrimmed === 'đang tạo') {
                                    foundIndex = tempPermalinks.length - 1;
                                }
                            }
                        });

                        permalinks = tempPermalinks;

                        if (foundIndex === -1) {
                            foundIndex = permalinks.findIndex(p => p.status !== 'đã tạo');
                            if (foundIndex === -1 && permalinks.length > 0) foundIndex = 0;
                        }

                        currentIndex = foundIndex === -1 ? 0 : foundIndex;
                        if (permalinks.length > 0) {
                            updateStatus('Đang tạo'); // Dùng hàm local để cập nhật GAS status
                        }
                        updateUIState();
                        processCurrentLink();
                        preloadApiData();
                    } else {
                        log("❌ Lỗi Sheet: " + (json.message || "Lỗi dữ liệu trả về."));
                        isGasMode = false;
                    }
                } catch (e) {
                    log("❌ Lỗi parse JSON hoặc lỗi xử lý dữ liệu: " + e.message, 'error');
                    console.error(e);
                    isGasMode = false;
                }
            },
            onerror: function (err) {
                if (loadBtn) loadBtn.disabled = false;
                log("❌ Lỗi kết nối mạng GAS.", 'error');
                console.error(err);
                isGasMode = false;
            }
        });
    }

    function updateGasStatusByRowIndex(rowIndex, newStatus, newPermalink = null) {
        if (!isGasMode || !rowIndex) return;

        gasWriteQueue.push({ rowIndex, newStatus, newPermalink });
        processGasWriteQueue();
    }

    async function performGasUpdate(item) {
        const scriptUrl = document.getElementById('gas_url')?.value?.trim();
        const sheetName = document.getElementById('sheet_name_input')?.value?.trim();

        if (!scriptUrl || !sheetName) {
            console.error('Lỗi: Thiếu Web App URL hoặc Tên Sheet để cập nhật GAS.');
            return;
        }

        let url = `${scriptUrl}?action=post&rowIndex=${item.rowIndex}&status=${encodeURIComponent(item.newStatus)}&sheetName=${encodeURIComponent(sheetName)}`;

        if (item.newPermalink) {
            const urlColName = document.getElementById('url_col_name')?.value?.trim();
            if (urlColName) {
                url += `&urlCol=${encodeURIComponent(urlColName)}`;
                url += `&permalink=${encodeURIComponent(item.newPermalink)}`;
            }
        }

        return new Promise(resolve => {
            GM_xmlhttpRequest({
                method: "GET",
                url: url,
                onload: (response) => {
                    try {
                        const res = JSON.parse(response.responseText);
                        if (res.result !== "success") {
                            log(`⚠️ Lỗi GAS ghi status (Row ${item.rowIndex}): ${res.message}`, 'warn');
                        }
                    } catch (e) {
                        log(`⚠️ Lỗi phản hồi JSON từ GAS khi ghi status.`, 'warn');
                    }
                    resolve();
                },
                onerror: (err) => {
                    log(`❌ Lỗi kết nối khi cập nhật GAS status (Row ${item.rowIndex}).`, 'error');
                    resolve(); // Giải quyết Promise để queue có thể tiếp tục
                }
            });
        });
    }

    async function processGasWriteQueue() {
        if (gasWriteProcessing) return;

        gasWriteProcessing = true;
        while (gasWriteQueue.length > 0) {
            const item = gasWriteQueue.shift();
            await performGasUpdate(item);
            // Đợi một chút giữa các lần ghi để tránh overload GAS nếu loop quá nhanh
            await delay(100);
        }
        gasWriteProcessing = false;
    }

    // Initialize the script
    bootstrap();
})();