/** @odoo-module **/
// Fusion Claims - Google Maps Task View with Sidebar
// Copyright 2024-2026 Nexa Systems Inc.
// License OPL-1

import { registry } from "@web/core/registry";
import { standardViewProps } from "@web/views/standard_view_props";
import { useService } from "@web/core/utils/hooks";
import { useModelWithSampleData } from "@web/model/model";
import { useSetupAction } from "@web/search/action_hook";
import { usePager } from "@web/search/pager_hook";
import { useSearchBarToggler } from "@web/search/search_bar/search_bar_toggler";
import { RelationalModel } from "@web/model/relational_model/relational_model";
import { Layout } from "@web/search/layout";
import { SearchBar } from "@web/search/search_bar/search_bar";
import { CogMenu } from "@web/search/cog_menu/cog_menu";
import { _t } from "@web/core/l10n/translation";
import {
    Component,
    onMounted,
    onPatched,
    onWillUnmount,
    useRef,
    useState,
} from "@odoo/owl";

// ── Constants ───────────────────────────────────────────────────────
const STATUS_COLORS = {
    pending: "#f59e0b",
    scheduled: "#3b82f6",
    en_route: "#f59e0b",
    in_progress: "#8b5cf6",
    completed: "#10b981",
    cancelled: "#ef4444",
    rescheduled: "#f97316",
};
const STATUS_LABELS = {
    pending: "Pending",
    scheduled: "Scheduled",
    en_route: "En Route",
    in_progress: "In Progress",
    completed: "Completed",
    cancelled: "Cancelled",
    rescheduled: "Rescheduled",
};
const STATUS_ICONS = {
    pending: "fa-hourglass-half",
    scheduled: "fa-clock-o",
    en_route: "fa-truck",
    in_progress: "fa-wrench",
    completed: "fa-check-circle",
    cancelled: "fa-times-circle",
    rescheduled: "fa-calendar",
};

// Date group keys
const GROUP_PENDING = "pending";
const GROUP_YESTERDAY = "yesterday";
const GROUP_TODAY = "today";
const GROUP_TOMORROW = "tomorrow";
const GROUP_THIS_WEEK = "this_week";
const GROUP_LATER = "later";
const GROUP_LABELS = {
    [GROUP_PENDING]: "Pending",
    [GROUP_YESTERDAY]: "Yesterday",
    [GROUP_TODAY]: "Today",
    [GROUP_TOMORROW]: "Tomorrow",
    [GROUP_THIS_WEEK]: "This Week",
    [GROUP_LATER]: "Upcoming",
};

// Pin colours by day group
const DAY_COLORS = {
    [GROUP_PENDING]: "#f59e0b",    // Amber
    [GROUP_YESTERDAY]: "#9ca3af",  // Gray
    [GROUP_TODAY]: "#ef4444",      // Red
    [GROUP_TOMORROW]: "#3b82f6",   // Blue
    [GROUP_THIS_WEEK]: "#10b981",  // Green
    [GROUP_LATER]: "#a855f7",      // Purple
};
const DAY_ICONS = {
    [GROUP_PENDING]: "fa-hourglass-half",
    [GROUP_YESTERDAY]: "fa-history",
    [GROUP_TODAY]: "fa-exclamation-circle",
    [GROUP_TOMORROW]: "fa-arrow-right",
    [GROUP_THIS_WEEK]: "fa-calendar",
    [GROUP_LATER]: "fa-calendar-o",
};

// ── SVG numbered pin ────────────────────────────────────────────────
function numberedPinSvg(fill, number) {
    const txt = String(number);
    const fontSize = txt.length > 2 ? 13 : 16;
    return (
        `<svg xmlns="http://www.w3.org/2000/svg" width="38" height="50" viewBox="0 0 38 50">` +
        `<ellipse cx="19" cy="48" rx="8" ry="2.5" fill="rgba(0,0,0,.25)"/>` +
        `<path d="M19 0C8.51 0 0 8.51 0 19c0 14 19 31 19 31s19-17 19-31C38 8.51 29.49 0 19 0z" fill="${fill}" stroke="#fff" stroke-width="2"/>` +
        `<text x="19" y="${fontSize > 13 ? 24 : 23}" text-anchor="middle" fill="#fff" font-size="${fontSize}" font-family="Arial,Helvetica,sans-serif" font-weight="bold">#${txt}</text>` +
        `</svg>`
    );
}
function numberedPinUri(fill, number) {
    return "data:image/svg+xml;charset=UTF-8," + encodeURIComponent(numberedPinSvg(fill, number));
}

// ── Helpers ─────────────────────────────────────────────────────────
let _gmapsPromise = null;
function loadGoogleMaps(apiKey) {
    if (window.google && window.google.maps) return Promise.resolve();
    if (_gmapsPromise) return _gmapsPromise;
    _gmapsPromise = new Promise((resolve, reject) => {
        const cb = "_fc_gmap_" + Date.now();
        window[cb] = () => { delete window[cb]; resolve(); };
        const s = document.createElement("script");
        s.src = `https://maps.googleapis.com/maps/api/js?key=${apiKey}&callback=${cb}`;
        s.async = true; s.defer = true;
        s.onerror = () => { _gmapsPromise = null; reject(new Error("Google Maps script failed")); };
        document.head.appendChild(s);
    });
    return _gmapsPromise;
}

function initialsOf(name) {
    if (!name) return "?";
    const p = name.trim().split(/\s+/);
    return p.length >= 2
        ? (p[0][0] + p[p.length - 1][0]).toUpperCase()
        : p[0].substring(0, 2).toUpperCase();
}

/** Return "YYYY-MM-DD" for a JS Date in local tz */
function localDateStr(d) {
    return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
}

/** Convert float hours (e.g. 13.5) to "1:30 PM" */
function floatToTime12(flt) {
    if (!flt && flt !== 0) return "";
    let h = Math.floor(flt);
    let m = Math.round((flt - h) * 60);
    if (m === 60) { h++; m = 0; }
    const ampm = h >= 12 ? "PM" : "AM";
    const h12 = h === 0 ? 12 : h > 12 ? h - 12 : h;
    return `${h12}:${String(m).padStart(2, "0")} ${ampm}`;
}

/** Classify a task into one of our group keys based on status and date */
function classifyTask(task) {
    if (task.status === "pending") return GROUP_PENDING;
    return classifyDate(task.scheduled_date);
}

function classifyDate(dateStr) {
    if (!dateStr) return GROUP_PENDING;
    const now = new Date();
    const todayStr = localDateStr(now);

    const yest = new Date(now);
    yest.setDate(yest.getDate() - 1);
    const yesterdayStr = localDateStr(yest);

    const tmr = new Date(now);
    tmr.setDate(tmr.getDate() + 1);
    const tomorrowStr = localDateStr(tmr);

    const endOfWeek = new Date(now);
    endOfWeek.setDate(endOfWeek.getDate() + (7 - endOfWeek.getDay()));
    const endOfWeekStr = localDateStr(endOfWeek);

    if (dateStr === yesterdayStr) return GROUP_YESTERDAY;
    if (dateStr === todayStr) return GROUP_TODAY;
    if (dateStr === tomorrowStr) return GROUP_TOMORROW;
    if (dateStr <= endOfWeekStr && dateStr > tomorrowStr) return GROUP_THIS_WEEK;
    if (dateStr < yesterdayStr) return GROUP_YESTERDAY;
    return GROUP_LATER;
}

const SOURCE_COLORS = {
    westin: "#0d6efd",
    mobility: "#198754",
};

/** Extract unique technicians from task data, sorted by name */
function extractTechnicians(tasksData) {
    const map = {};
    for (const t of tasksData) {
        if (t.technician_id) {
            const [id, name] = t.technician_id;
            if (!map[id]) {
                map[id] = { id, name, initials: initialsOf(name) };
            }
        }
    }
    return Object.values(map).sort((a, b) => a.name.localeCompare(b.name));
}

/** Group + sort tasks, returning { groupKey: { label, tasks[], count } } */
function groupTasks(tasksData, localInstanceId, visibleTechIds) {
    const sorted = [...tasksData].sort((a, b) => {
        const da = a.scheduled_date || "";
        const db = b.scheduled_date || "";
        if (da !== db) return da < db ? -1 : 1;
        return (a.time_start || 0) - (b.time_start || 0);
    });

    const hasTechFilter = visibleTechIds && Object.keys(visibleTechIds).length > 0;

    const groups = {};
    const order = [GROUP_PENDING, GROUP_YESTERDAY, GROUP_TODAY, GROUP_TOMORROW, GROUP_THIS_WEEK, GROUP_LATER];
    for (const key of order) {
        groups[key] = {
            key,
            label: GROUP_LABELS[key],
            dayColor: DAY_COLORS[key] || "#6b7280",
            dayIcon: DAY_ICONS[key] || "fa-circle",
            tasks: [],
            count: 0,
        };
    }

    const dayCounters = {};
    for (const task of sorted) {
        const techId = task.technician_id ? task.technician_id[0] : 0;
        if (hasTechFilter && !visibleTechIds[techId]) continue;

        const g = classifyTask(task);
        const dayKey = task.scheduled_date || "none";
        dayCounters[dayKey] = (dayCounters[dayKey] || 0) + 1;
        task._scheduleNum = dayCounters[dayKey];
        task._group = g;
        task._dayColor = DAY_COLORS[g] || "#6b7280";
        task._statusColor = STATUS_COLORS[task.status] || "#6b7280";
        task._statusLabel = STATUS_LABELS[task.status] || task.status || "";
        task._statusIcon = STATUS_ICONS[task.status] || "fa-circle";
        task._clientName = task.x_fc_sync_client_name || (task.partner_id ? task.partner_id[1] : "N/A");
        task._techName = task.technician_id ? task.technician_id[1] : "Unassigned";
        task._typeLbl = task.task_type
            ? task.task_type.charAt(0).toUpperCase() + task.task_type.slice(1).replace("_", " ")
            : "Task";
        task._timeRange = `${task.time_start_display || floatToTime12(task.time_start)} - ${task.time_end_display || ""}`;
        const src = task.x_fc_sync_source || localInstanceId || "";
        task._sourceLabel = src ? src.charAt(0).toUpperCase() + src.slice(1) : "";
        task._sourceColor = SOURCE_COLORS[src] || "#6c757d";
        task._hasCoords = task.address_lat && task.address_lng && task.address_lat !== 0 && task.address_lng !== 0;
        groups[g].tasks.push(task);
        groups[g].count++;
    }

    return order.map((k) => groups[k]).filter((g) => g.count > 0);
}


// ── Controller ──────────────────────────────────────────────────────
export class FusionTaskMapController extends Component {
    static template = "fusion_claims.FusionTaskMapView";
    static components = { Layout, SearchBar, CogMenu };
    static props = {
        ...standardViewProps,
        Model: Function,
        modelParams: Object,
        Renderer: { type: Function, optional: true },
        buttonTemplate: String,
    };

    setup() {
        this.orm = useService("orm");
        this.actionService = useService("action");
        this.mapRef = useRef("mapContainer");

        this.state = useState({
            loading: true,
            error: null,
            showTasks: true,
            showTechnicians: true,
            showTraffic: true,
            showRoute: true,
            taskCount: 0,
            techCount: 0,
            sidebarOpen: true,
            groups: [],
            collapsedGroups: {},
            activeTaskId: null,
            visibleGroups: {
                [GROUP_YESTERDAY]: false,
                [GROUP_TODAY]: true,
                [GROUP_TOMORROW]: false,
                [GROUP_THIS_WEEK]: false,
                [GROUP_LATER]: false,
            },
            allTechnicians: [],
            visibleTechIds: {},
        });

        // Yesterday collapsed by default in sidebar list
        this.state.collapsedGroups[GROUP_YESTERDAY] = true;
        this.state.collapsedGroups[GROUP_LATER] = true;

        this.map = null;
        this.taskMarkers = [];
        this.taskMarkerMap = {};    // id → marker
        this.techMarkers = [];
        this.routeLines = [];       // route polylines
        this.routeLabels = [];      // travel time overlay labels
        this.routeAnimFrameId = null;
        this.infoWindow = null;
        this.techStartLocations = {};
        this.apiKey = "";
        this.tasksData = [];
        this.locationsData = [];

        const Model = this.props.Model;
        this.model = useModelWithSampleData(Model, this.props.modelParams);
        useSetupAction({ getLocalState: () => this._meta() });
        usePager(() => ({
            offset: this._meta().offset || 0,
            limit: this._meta().limit || 80,
            total: this.model.data?.count || this._meta().resCount || 0,
            onUpdate: ({ offset, limit }) => this.model.load({ offset, limit }),
        }));
        this.searchBarToggler = useSearchBarToggler();
        this.display = { controlPanel: {} };
        this._lastDomainStr = "";

        onMounted(async () => {
            window.__fusionMapOpenTask = (id) => this.openTask(id);
            await this._loadAndRender();
            this._lastDomainStr = JSON.stringify(this._getDomain());
        });
        onPatched(() => {
            const cur = JSON.stringify(this._getDomain());
            if (cur !== this._lastDomainStr && this.map) {
                this._lastDomainStr = cur;
                this._onModelUpdate();
            }
        });
        onWillUnmount(() => {
            this._clearMarkers();
            this._clearRoute();
            window.__fusionMapOpenTask = () => {};
        });
    }

    // ── Model helpers (safe access across different Model types) ────
    _meta() {
        // RelationalModel uses .config, MapModel uses .metaData
        return this.model.metaData || this.model.config || {};
    }
    _getDomain() {
        const m = this._meta();
        return m.domain || [];
    }

    // ── Data ─────────────────────────────────────────────────────────
    _storeResult(result) {
        this.localInstanceId = result.local_instance_id || this.localInstanceId || "";
        this.tasksData = result.tasks || [];
        this.locationsData = result.locations || [];
        this.techStartLocations = result.tech_start_locations || {};
        this.state.allTechnicians = extractTechnicians(this.tasksData);
        this._rebuildGroups();
    }

    _rebuildGroups() {
        this.state.groups = groupTasks(
            this.tasksData, this.localInstanceId, this.state.visibleTechIds,
        );
        const filteredCount = this.state.groups.reduce((s, g) => s + g.count, 0);
        this.state.taskCount = filteredCount;
        this.state.techCount = this.locationsData.length;
    }

    async _loadAndRender() {
        try {
            const domain = this._getDomain();
            const result = await this.orm.call("fusion.technician.task", "get_map_data", [domain]);
            this.apiKey = result.api_key;
            this._storeResult(result);

            if (!this.apiKey) {
                this.state.error = _t("Google Maps API key not configured. Go to Settings > Fusion Claims.");
                this.state.loading = false;
                return;
            }
            await loadGoogleMaps(this.apiKey);
            if (this.map) {
                this._renderMarkers();
            } else if (this.mapRef.el) {
                this._initMap();
            }
            this.state.loading = false;
        } catch (e) {
            console.error("FusionTaskMap load error:", e);
            this.state.error = String(e);
            this.state.loading = false;
        }
    }

    async _softRefresh() {
        if (!this.map) return;
        try {
            const center = this.map.getCenter();
            const zoom = this.map.getZoom();

            const domain = this._getDomain();
            const result = await this.orm.call("fusion.technician.task", "get_map_data", [domain]);
            this._storeResult(result);

            this._placeMarkers();

            if (center && zoom != null) {
                this.map.setCenter(center);
                this.map.setZoom(zoom);
            }
        } catch (e) {
            console.error("FusionTaskMap soft refresh error:", e);
        }
    }

    async _onModelUpdate() {
        if (!this.map) return;
        try {
            const domain = this._getDomain();
            const result = await this.orm.call("fusion.technician.task", "get_map_data", [domain]);
            this._storeResult(result);
            this._renderMarkers();
        } catch (e) {
            console.error("FusionTaskMap update error:", e);
        }
    }

    // ── Map ──────────────────────────────────────────────────────────
    _initMap() {
        if (!this.mapRef.el) return;
        this.map = new google.maps.Map(this.mapRef.el, {
            zoom: 10,
            center: { lat: 43.7, lng: -79.4 },
            mapTypeControl: true,
            streetViewControl: false,
            fullscreenControl: true,
            zoomControl: true,
            styles: [{ featureType: "poi", stylers: [{ visibility: "off" }] }],
        });
        // Traffic layer (on by default, toggleable)
        this.trafficLayer = new google.maps.TrafficLayer();
        this.trafficLayer.setMap(this.map);

        this.infoWindow = new google.maps.InfoWindow();
        // Close popup when clicking anywhere on the map
        this.map.addListener("click", () => {
            this.infoWindow.close();
        });
        // Clear sidebar highlight when popup closes (by any means)
        this.infoWindow.addListener("closeclick", () => {
            this.state.activeTaskId = null;
        });
        this._renderMarkers();
    }

    _clearMarkers() {
        for (const m of this.taskMarkers) m.setMap(null);
        for (const m of this.techMarkers) m.setMap(null);
        this.taskMarkers = [];
        this.taskMarkerMap = {};
        this.techMarkers = [];
    }

    _clearRoute() {
        if (this.routeAnimFrameId) {
            cancelAnimationFrame(this.routeAnimFrameId);
            this.routeAnimFrameId = null;
        }
        for (const l of this.routeLines) l.setMap(null);
        this.routeLines = [];
        for (const lb of this.routeLabels) lb.setMap(null);
        this.routeLabels = [];
    }

    _placeMarkers() {
        for (const m of this.taskMarkers) m.setMap(null);
        for (const m of this.techMarkers) m.setMap(null);
        this.taskMarkers = [];
        this.taskMarkerMap = {};
        this.techMarkers = [];

        const bounds = new google.maps.LatLngBounds();
        let hasBounds = false;

        if (this.state.showTasks) {
            for (const group of this.state.groups) {
                const groupVisible = this.state.visibleGroups[group.key] !== false;
                for (const task of group.tasks) {
                    if (!task.address_lat || !task.address_lng) continue;
                    if (!groupVisible) continue;
                    const pos = { lat: task.address_lat, lng: task.address_lng };
                    const num = task._scheduleNum;
                    const color = task._dayColor;

                    const marker = new google.maps.Marker({
                        position: pos,
                        map: this.map,
                        title: `#${num} ${task.name} - ${task._clientName}`,
                        icon: {
                            url: numberedPinUri(color, num),
                            scaledSize: new google.maps.Size(38, 50),
                            anchor: new google.maps.Point(19, 50),
                        },
                        zIndex: 10 + num,
                    });

                    marker.addListener("click", () => this._openTaskPopup(task, marker));
                    this.taskMarkers.push(marker);
                    this.taskMarkerMap[task.id] = marker;
                    bounds.extend(pos);
                    hasBounds = true;
                }
            }
        }

        if (this.state.showTechnicians) {
            for (const loc of this.locationsData) {
                if (!loc.latitude || !loc.longitude) continue;
                const pos = { lat: loc.latitude, lng: loc.longitude };
                const initials = initialsOf(loc.name);
                const src = loc.sync_instance || this.localInstanceId || "";
                const isRemote = src && src !== this.localInstanceId;
                const pinColor = isRemote
                    ? (SOURCE_COLORS[src] || "#6c757d")
                    : "#1d4ed8";
                const srcLabel = src ? src.charAt(0).toUpperCase() + src.slice(1) : "";
                const svg =
                    `<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48">` +
                    `<rect x="2" y="2" width="44" height="44" rx="12" ry="12" fill="${pinColor}" stroke="#fff" stroke-width="3"/>` +
                    `<text x="24" y="30" text-anchor="middle" fill="#fff" font-size="17" font-family="Arial,Helvetica,sans-serif" font-weight="bold">${initials}</text>` +
                    `</svg>`;
                const marker = new google.maps.Marker({
                    position: pos,
                    map: this.map,
                    title: loc.name + (isRemote ? ` [${srcLabel}]` : ""),
                    icon: {
                        url: "data:image/svg+xml;charset=UTF-8," + encodeURIComponent(svg),
                        scaledSize: new google.maps.Size(44, 44),
                        anchor: new google.maps.Point(22, 22),
                    },
                    zIndex: 100,
                });
                marker.addListener("click", () => {
                    this.infoWindow.setContent(`
                        <div style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;min-width:200px;color:#1f2937;">
                            <div style="background:${pinColor};color:#fff;padding:10px 14px;">
                                <strong><i class="fa fa-user" style="margin-right:6px;"></i>${loc.name}</strong>
                                ${srcLabel ? `<span style="float:right;font-size:10px;font-weight:600;background:rgba(255,255,255,.2);padding:2px 8px;border-radius:8px;">${srcLabel}</span>` : ""}
                            </div>
                            <div style="padding:12px 14px;font-size:13px;line-height:1.8;color:#1f2937;">
                                <div><strong style="color:#374151;">Last seen:</strong> <span style="color:#111827;">${loc.logged_at || "Unknown"}</span></div>
                                <div><strong style="color:#374151;">Accuracy:</strong> <span style="color:#111827;">${loc.accuracy ? Math.round(loc.accuracy) + "m" : "N/A"}</span></div>
                            </div>
                        </div>`);
                    this.infoWindow.open(this.map, marker);
                });
                this.techMarkers.push(marker);
                bounds.extend(pos);
                hasBounds = true;
            }
        }

        const starts = this.techStartLocations || {};
        for (const uid of Object.keys(starts)) {
            const sl = starts[uid];
            if (sl && sl.lat && sl.lng) {
                bounds.extend({ lat: sl.lat, lng: sl.lng });
                hasBounds = true;
            }
        }

        return { bounds, hasBounds };
    }

    _renderMarkers() {
        this._clearRoute();
        const { bounds, hasBounds } = this._placeMarkers();

        if (this.state.showRoute && this.state.showTasks) {
            this._renderRoute();
        }

        if (hasBounds) {
            try {
                this.map.fitBounds(bounds);
                if (this.taskMarkers.length + this.techMarkers.length === 1) {
                    this.map.setZoom(14);
                }
            } catch (_e) {
                // bounds not ready yet
            }
        }
    }

    _renderRoute() {
        this._clearRoute();

        const routeSegments = {};
        for (const group of this.state.groups) {
            if (this.state.visibleGroups[group.key] === false) continue;
            for (const task of group.tasks) {
                if (!task._hasCoords) continue;
                const techId = task.technician_id ? task.technician_id[0] : 0;
                if (!techId) continue;
                const dayKey = task.scheduled_date || "none";
                const segKey = `${techId}_${dayKey}`;
                if (!routeSegments[segKey]) {
                    routeSegments[segKey] = {
                        name: task._techName, day: dayKey,
                        techId, tasks: [],
                    };
                }
                routeSegments[segKey].tasks.push(task);
            }
        }

        const LEG_COLORS = [
            "#3b82f6", "#f59e0b", "#8b5cf6", "#ec4899",
            "#f97316", "#0ea5e9", "#d946ef", "#06b6d4",
            "#a855f7", "#6366f1", "#eab308", "#0284c7",
            "#c026d3", "#7c3aed", "#2563eb", "#db2777",
            "#9333ea", "#0891b2", "#4f46e5", "#be185d",
        ];
        let globalLegIdx = 0;

        if (!this._directionsService) {
            this._directionsService = new google.maps.DirectionsService();
        }

        const allAnimLines = [];
        const starts = this.techStartLocations || {};

        for (const segKey of Object.keys(routeSegments)) {
            const seg = routeSegments[segKey];
            const tasks = seg.tasks;
            tasks.sort((a, b) => (a.time_start || 0) - (b.time_start || 0));

            const startLoc = starts[seg.techId];
            const hasStart = startLoc && startLoc.lat && startLoc.lng;

            if (tasks.length < 2 && !hasStart) continue;
            if (tasks.length < 1) continue;

            const segBaseColor = LEG_COLORS[globalLegIdx % LEG_COLORS.length];

            let origin, destination, waypoints, hasStartLeg;

            if (hasStart) {
                origin = { lat: startLoc.lat, lng: startLoc.lng };
                destination = {
                    lat: tasks[tasks.length - 1].address_lat,
                    lng: tasks[tasks.length - 1].address_lng,
                };
                waypoints = tasks.slice(0, -1).map(t => ({
                    location: { lat: t.address_lat, lng: t.address_lng },
                    stopover: true,
                }));
                hasStartLeg = true;
            } else {
                origin = { lat: tasks[0].address_lat, lng: tasks[0].address_lng };
                destination = {
                    lat: tasks[tasks.length - 1].address_lat,
                    lng: tasks[tasks.length - 1].address_lng,
                };
                waypoints = tasks.slice(1, -1).map(t => ({
                    location: { lat: t.address_lat, lng: t.address_lng },
                    stopover: true,
                }));
                hasStartLeg = false;
            }

            if (hasStart) {
                const startSvg =
                    `<svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 36 36">` +
                    `<circle cx="18" cy="18" r="16" fill="${segBaseColor}" stroke="#fff" stroke-width="3"/>` +
                    `<text x="18" y="23" text-anchor="middle" fill="#fff" font-size="16" font-family="Arial,sans-serif">&#x2302;</text>` +
                    `</svg>`;
                const startMarker = new google.maps.Marker({
                    position: origin,
                    map: this.map,
                    title: `${seg.name} - Start`,
                    icon: {
                        url: "data:image/svg+xml;charset=UTF-8," + encodeURIComponent(startSvg),
                        scaledSize: new google.maps.Size(32, 32),
                        anchor: new google.maps.Point(16, 16),
                    },
                    zIndex: 5,
                });
                startMarker.addListener("click", () => {
                    this.infoWindow.setContent(`
                        <div style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;min-width:180px;">
                            <div style="background:${segBaseColor};color:#fff;padding:8px 12px;border-radius:6px 6px 0 0;">
                                <strong>${seg.name} - Start</strong>
                            </div>
                            <div style="padding:8px 12px;font-size:13px;">
                                ${startLoc.address || 'Start location'}
                                <div style="color:#6b7280;margin-top:4px;font-size:11px;">${startLoc.source === 'clock_in' ? 'Clock-in location' : startLoc.source === 'start_address' ? 'Home address' : 'Company HQ'}</div>
                            </div>
                        </div>`);
                    this.infoWindow.open(this.map, startMarker);
                });
                this.routeLines.push(startMarker);
            }

            this._directionsService.route({
                origin,
                destination,
                waypoints,
                optimizeWaypoints: false,
                travelMode: google.maps.TravelMode.DRIVING,
                avoidTolls: true,
                drivingOptions: {
                    departureTime: new Date(),
                    trafficModel: "bestguess",
                },
            }, (result, status) => {
                if (status !== "OK" || !result.routes || !result.routes[0]) return;

                const route = result.routes[0];

                for (let li = 0; li < route.legs.length; li++) {
                    const leg = route.legs[li];
                    const legColor = LEG_COLORS[globalLegIdx % LEG_COLORS.length];
                    globalLegIdx++;

                    const legPath = [];
                    for (const step of leg.steps) {
                        for (const pt of step.path) legPath.push(pt);
                    }
                    if (legPath.length < 2) continue;

                    const baseLine = new google.maps.Polyline({
                        path: legPath, map: this.map,
                        strokeColor: legColor, strokeOpacity: 0.25, strokeWeight: 6,
                        zIndex: 1,
                    });
                    this.routeLines.push(baseLine);

                    const animLine = new google.maps.Polyline({
                        path: legPath, map: this.map,
                        strokeOpacity: 0, strokeWeight: 0, zIndex: 2,
                        icons: [{
                            icon: {
                                path: "M 0,-0.5 0,0.5",
                                strokeOpacity: 0.8, strokeColor: legColor,
                                strokeWeight: 3, scale: 4,
                            },
                            offset: "0%", repeat: "16px",
                        }],
                    });
                    this.routeLines.push(animLine);
                    allAnimLines.push(animLine);

                    const arrowLine = new google.maps.Polyline({
                        path: legPath, map: this.map,
                        strokeOpacity: 0, strokeWeight: 0, zIndex: 3,
                        icons: [{
                            icon: {
                                path: google.maps.SymbolPath.FORWARD_OPEN_ARROW,
                                scale: 3, strokeColor: legColor,
                                strokeOpacity: 0.9, strokeWeight: 2.5,
                            },
                            offset: "0%", repeat: "80px",
                        }],
                    });
                    this.routeLines.push(arrowLine);
                    allAnimLines.push(arrowLine);

                    const dur = leg.duration_in_traffic || leg.duration;
                    const dist = leg.distance;
                    if (dur) {
                        const totalMins = Math.round(dur.value / 60);
                        const totalKm = dist ? (dist.value / 1000).toFixed(1) : null;

                        const destIdx = hasStartLeg ? li : li + 1;
                        const destTask = destIdx < tasks.length ? tasks[destIdx] : tasks[tasks.length - 1];
                        const etaFloat = destTask.time_start || 0;
                        const etaStr = etaFloat ? floatToTime12(etaFloat) : "";

                        const techName = seg.name;
                        this.routeLabels.push(this._createTravelLabel(
                            legPath, totalMins, totalKm, legColor, techName, etaStr,
                        ));
                    }
                }

                if (!this.routeAnimFrameId) {
                    this._startRouteAnimation(allAnimLines);
                }
            });
        }
    }

    _pointAlongLeg(leg, fraction) {
        const points = [];
        for (const step of leg.steps) {
            for (const pt of step.path) {
                points.push(pt);
            }
        }
        if (points.length < 2) return leg.start_location;

        const segDists = [];
        let totalDist = 0;
        for (let i = 1; i < points.length; i++) {
            const d = google.maps.geometry
                ? google.maps.geometry.spherical.computeDistanceBetween(points[i - 1], points[i])
                : this._haversine(points[i - 1], points[i]);
            segDists.push(d);
            totalDist += d;
        }

        const target = totalDist * fraction;
        let acc = 0;
        for (let i = 0; i < segDists.length; i++) {
            if (acc + segDists[i] >= target) {
                const remain = target - acc;
                const ratio = segDists[i] > 0 ? remain / segDists[i] : 0;
                return new google.maps.LatLng(
                    points[i].lat() + (points[i + 1].lat() - points[i].lat()) * ratio,
                    points[i].lng() + (points[i + 1].lng() - points[i].lng()) * ratio,
                );
            }
            acc += segDists[i];
        }
        return points[points.length - 1];
    }

    _haversine(a, b) {
        const R = 6371000;
        const dLat = (b.lat() - a.lat()) * Math.PI / 180;
        const dLng = (b.lng() - a.lng()) * Math.PI / 180;
        const s = Math.sin(dLat / 2) ** 2 +
            Math.cos(a.lat() * Math.PI / 180) * Math.cos(b.lat() * Math.PI / 180) *
            Math.sin(dLng / 2) ** 2;
        return R * 2 * Math.atan2(Math.sqrt(s), Math.sqrt(1 - s));
    }

    _createTravelLabel(legPath, mins, km, color, techName, eta) {
        if (!this._TravelLabel) {
            this._TravelLabel = class extends google.maps.OverlayView {
                constructor(path, html) {
                    super();
                    this._path = path;
                    this._html = html;
                    this._div = null;
                }
                onAdd() {
                    this._div = document.createElement("div");
                    this._div.style.position = "absolute";
                    this._div.style.whiteSpace = "nowrap";
                    this._div.style.pointerEvents = "none";
                    this._div.style.zIndex = "50";
                    this._div.style.transition = "left .3s ease, top .3s ease";
                    this._div.innerHTML = this._html;
                    this.getPanes().floatPane.appendChild(this._div);
                }
                draw() {
                    const proj = this.getProjection();
                    if (!proj || !this._div) return;
                    const map = this.getMap();
                    if (!map) return;
                    const bounds = map.getBounds();
                    if (!bounds) return;

                    const visible = this._path.filter(p => bounds.contains(p));
                    if (visible.length === 0) {
                        this._div.style.display = "none";
                        return;
                    }
                    this._div.style.display = "";

                    const anchor = visible[Math.floor(visible.length / 2)];

                    const px = proj.fromLatLngToDivPixel(anchor);
                    if (px) {
                        this._div.style.left = (px.x - this._div.offsetWidth / 2) + "px";
                        this._div.style.top = (px.y - this._div.offsetHeight - 8) + "px";
                    }
                }
                onRemove() {
                    if (this._div && this._div.parentNode) {
                        this._div.parentNode.removeChild(this._div);
                    }
                    this._div = null;
                }
            };
        }

        const timeStr = mins < 60
            ? `${mins} min`
            : `${Math.floor(mins / 60)}h ${mins % 60}m`;
        const distStr = km ? `${km} km` : "";

        const firstName = techName ? techName.split(" ")[0] : "";
        const html = `<div style="
            display:inline-flex;align-items:center;gap:5px;
            background:#fff;border:2px solid ${color};
            border-radius:16px;padding:3px 10px;
            font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;
            font-size:11px;font-weight:700;color:#1f2937;
            box-shadow:0 2px 8px rgba(0,0,0,.18);
        ">${firstName ? `<span style="color:${color};font-weight:600;">${firstName}</span><span style="color:#d1d5db;">|</span>` : ""}<span style="color:${color};">&#x1F697;</span><span>${timeStr}</span>${distStr ? `<span style="color:#9ca3af;font-weight:500;">&#183; ${distStr}</span>` : ""}${eta ? `<span style="color:#d1d5db;">|</span><span style="color:#059669;font-weight:700;">ETA ${eta}</span>` : ""}</div>`;

        const label = new this._TravelLabel(legPath, html);
        label.setMap(this.map);
        return label;
    }

    _startRouteAnimation(animLines) {
        let off = 0;
        let last = 0;
        const animate = (ts) => {
            this.routeAnimFrameId = requestAnimationFrame(animate);
            if (ts - last < 50) return;
            last = ts;
            off = (off + 0.08) % 100;
            const pct = off + "%";
            for (const line of animLines) {
                const icons = line.get("icons");
                if (icons && icons.length > 0) {
                    icons[0].offset = pct;
                    line.set("icons", icons);
                }
            }
        };
        this.routeAnimFrameId = requestAnimationFrame(animate);
    }

    _openTaskPopup(task, marker) {
        const c = task._dayColor;
        const sc = task._statusColor;
        const navDest = task.address_lat && task.address_lng
            ? `${task.address_lat},${task.address_lng}`
            : encodeURIComponent(task.address_display || "");
        const html = `
            <div style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;min-width:290px;max-width:360px;color:#1f2937;">
                <div style="background:${c};padding:14px 16px 12px;border-radius:0;">
                    <div style="display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:6px;">
                        <span style="color:rgba(255,255,255,.75);font-size:11px;font-weight:600;letter-spacing:.3px;">#${task._scheduleNum} ${task.name}</span>
                        <span style="font-size:10px;font-weight:600;background:${sc};color:#fff;padding:2px 10px;border-radius:10px;">${task._statusLabel}</span>
                    </div>
                    <div style="color:#fff;font-size:16px;font-weight:700;line-height:1.25;">${task._clientName}</div>
                </div>
                <div style="padding:10px 16px 6px;display:flex;gap:6px;flex-wrap:wrap;">
                    <span style="display:inline-flex;align-items:center;gap:4px;font-size:11px;font-weight:600;background:#f1f5f9;color:#334155;padding:3px 10px;border-radius:4px;">
                        <span style="opacity:.5;">&#xf02b;</span>${task._typeLbl}
                    </span>
                    <span style="display:inline-flex;align-items:center;gap:4px;font-size:11px;font-weight:600;background:#f1f5f9;color:#334155;padding:3px 10px;border-radius:4px;">
                        <span style="opacity:.5;">&#xf017;</span>${task._timeRange}
                    </span>
                    ${task.travel_time_minutes ? `<span style="display:inline-flex;align-items:center;gap:4px;font-size:11px;font-weight:600;background:#f1f5f9;color:#334155;padding:3px 10px;border-radius:4px;"><span style="opacity:.5;">&#xf1b9;</span>${task.travel_time_minutes} min</span>` : ""}
                </div>
                <div style="padding:8px 16px 12px;font-size:12px;line-height:1.7;color:#374151;">
                    <div style="display:flex;align-items:center;gap:6px;"><span style="color:#9ca3af;width:14px;text-align:center;">&#x1F464;</span><span>${task._techName}</span></div>
                    <div style="display:flex;align-items:center;gap:6px;"><span style="color:#9ca3af;width:14px;text-align:center;">&#x1F4C5;</span><span>${task.scheduled_date || "No date"}</span></div>
                    ${task.address_display ? `<div style="display:flex;align-items:flex-start;gap:6px;"><span style="color:#9ca3af;width:14px;text-align:center;flex-shrink:0;">&#x1F4CD;</span><span>${task.address_display}</span></div>` : ""}
                </div>
                <div style="padding:6px 16px 14px;display:flex;gap:8px;align-items:center;">
                    <button onclick="window.__fusionMapOpenTask(${task.id})"
                        style="background:${c};color:#fff;border:none;padding:7px 20px;border-radius:6px;cursor:pointer;font-size:12px;font-weight:600;transition:opacity .15s;">
                        Open Task
                    </button>
                    <a href="https://www.google.com/maps/dir/?api=1&destination=${navDest}"
                       target="_blank" style="color:${c};text-decoration:none;font-size:12px;font-weight:600;padding:7px 4px;">
                        Navigate &#x2192;
                    </a>
                </div>
            </div>`;
        this.infoWindow.setContent(html);
        this.infoWindow.open(this.map, marker);
    }

    // ── Sidebar actions ─────────────────────────────────────────────
    toggleSidebar() {
        this.state.sidebarOpen = !this.state.sidebarOpen;
        // Trigger map resize after CSS transition
        if (this.map) {
            setTimeout(() => google.maps.event.trigger(this.map, "resize"), 320);
        }
    }

    toggleGroup(groupKey) {
        this.state.collapsedGroups[groupKey] = !this.state.collapsedGroups[groupKey];
    }

    isGroupCollapsed(groupKey) {
        return !!this.state.collapsedGroups[groupKey];
    }

    focusTask(taskId) {
        this.state.activeTaskId = taskId;
        const marker = this.taskMarkerMap[taskId];
        if (marker && this.map) {
            this.map.panTo(marker.getPosition());
            this.map.setZoom(15);
            // Find the task data
            for (const g of this.state.groups) {
                for (const t of g.tasks) {
                    if (t.id === taskId) {
                        this._openTaskPopup(t, marker);
                        return;
                    }
                }
            }
        }
    }

    // ── Day filter toggle ────────────────────────────────────────────
    toggleDayFilter(groupKey) {
        this.state.visibleGroups[groupKey] = !this.state.visibleGroups[groupKey];
        this._renderMarkers();
    }

    isGroupVisible(groupKey) {
        return this.state.visibleGroups[groupKey] !== false;
    }

    showAllDays() {
        for (const k of Object.keys(this.state.visibleGroups)) {
            this.state.visibleGroups[k] = true;
        }
        this._renderMarkers();
    }

    showTodayOnly() {
        for (const k of Object.keys(this.state.visibleGroups)) {
            this.state.visibleGroups[k] = k === GROUP_TODAY;
        }
        this._renderMarkers();
    }

    // ── Technician filter ─────────────────────────────────────────────
    toggleTechFilter(techId) {
        if (this.state.visibleTechIds[techId]) {
            delete this.state.visibleTechIds[techId];
        } else {
            this.state.visibleTechIds[techId] = true;
        }
        this._rebuildGroups();
        this._renderMarkers();
    }

    isTechVisible(techId) {
        const hasFilter = Object.keys(this.state.visibleTechIds).length > 0;
        return !hasFilter || !!this.state.visibleTechIds[techId];
    }

    showAllTechs() {
        this.state.visibleTechIds = {};
        this._rebuildGroups();
        this._renderMarkers();
    }

    // ── Top bar actions ─────────────────────────────────────────────
    toggleTraffic() {
        this.state.showTraffic = !this.state.showTraffic;
        if (this.trafficLayer) {
            this.trafficLayer.setMap(this.state.showTraffic ? this.map : null);
        }
    }
    toggleTasks() {
        this.state.showTasks = !this.state.showTasks;
        this._renderMarkers();
    }
    toggleTechnicians() {
        this.state.showTechnicians = !this.state.showTechnicians;
        this._renderMarkers();
    }
    toggleRoute() {
        this.state.showRoute = !this.state.showRoute;
        if (this.state.showRoute) {
            this._renderRoute();
        } else {
            this._clearRoute();
        }
    }
    onRefresh() {
        this.state.loading = true;
        this._loadAndRender();
    }
    async openTask(taskId) {
        if (!taskId) return;
        try {
            await this.actionService.doAction(
                {
                    type: "ir.actions.act_window",
                    res_model: "fusion.technician.task",
                    res_id: taskId,
                    view_mode: "form",
                    views: [[false, "form"]],
                    target: "new",
                    context: { dialog_size: "extra-large" },
                },
                { onClose: () => this._softRefresh() },
            );
        } catch (e) {
            console.error("[FusionMap] openTask failed:", e);
            this.actionService.doAction({
                type: "ir.actions.act_window",
                res_model: "fusion.technician.task",
                res_id: taskId,
                view_mode: "form",
                views: [[false, "form"]],
                target: "current",
            });
        }
    }
    async createNewTask() {
        try {
            await this.actionService.doAction(
                {
                    type: "ir.actions.act_window",
                    res_model: "fusion.technician.task",
                    view_mode: "form",
                    views: [[false, "form"]],
                    target: "new",
                    context: { default_task_type: "delivery", dialog_size: "extra-large" },
                },
                { onClose: () => this._softRefresh() },
            );
        } catch (e) {
            console.error("[FusionMap] createNewTask failed:", e);
            this.actionService.doAction({
                type: "ir.actions.act_window",
                res_model: "fusion.technician.task",
                view_mode: "form",
                views: [[false, "form"]],
                target: "current",
                context: { default_task_type: "delivery" },
            });
        }
    }
}

window.__fusionMapOpenTask = () => {};

// ── Minimal ArchParser for <map> tags (no web_map dependency) ───────
class FusionMapArchParser {
    parse(xmlDoc, models, modelName) {
        const fieldNames = [];
        const activeFields = {};
        if (xmlDoc && xmlDoc.querySelectorAll) {
            for (const fieldEl of xmlDoc.querySelectorAll("field")) {
                const name = fieldEl.getAttribute("name");
                if (name) {
                    fieldNames.push(name);
                    activeFields[name] = { attrs: {}, options: {} };
                }
            }
        }
        return { fieldNames, activeFields };
    }
}

// ── View registration (self-contained, no @web_map dependency) ──────
const fusionTaskMapView = {
    type: "map",
    display_name: _t("Map"),
    icon: "oi-view-map",
    multiRecord: true,
    searchMenuTypes: ["filter", "groupBy", "favorite"],
    Controller: FusionTaskMapController,
    Model: RelationalModel,
    ArchParser: FusionMapArchParser,
    buttonTemplate: "fusion_claims.FusionTaskMapView.Buttons",
    props(genericProps, view, config) {
        const { resModel, fields } = genericProps;
        let archInfo = { fieldNames: [], activeFields: {} };
        if (view && view.arch) {
            archInfo = new FusionMapArchParser().parse(view.arch);
        }
        return {
            ...genericProps,
            buttonTemplate: "fusion_claims.FusionTaskMapView.Buttons",
            Model: RelationalModel,
            modelParams: {
                config: {
                    resModel,
                    fields,
                    activeFields: archInfo.activeFields || {},
                    isMonoRecord: false,
                },
                state: {
                    domain: genericProps.domain || [],
                    context: genericProps.context || {},
                    groupBy: genericProps.groupBy || [],
                    orderBy: genericProps.orderBy || [],
                },
            },
        };
    },
};
registry.category("views").add("fusion_task_map", fusionTaskMapView);
