import {Controller} from '@hotwired/stimulus';
import {getComponent} from '@symfony/ux-live-component';
import L from 'leaflet';
import 'leaflet.markercluster';

window.L = window.L || L;

const DEFAULT_CENTER = [46.227638, 2.213749];
const DEFAULT_ZOOM = 6;
const DEFAULT_TILE_URL = 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png';
const DEFAULT_TILE_ATTRIBUTION = '&copy; OpenStreetMap contributors';
const DEFAULT_COLOR = '#7a1f2b';
const DEFAULT_CLUSTER_COLOR = '#24323f';
const LIVE_COMPONENT_SELECTOR = '[data-controller~="live"]';

export default class extends Controller {
    static values = {
        center: Array,
        zoom: Number,
        minZoom: Number,
        maxZoom: Number,
        markers: Array,
        selectedDepotId: String,
        tileUrl: String,
        tileAttribution: String,
        syncEvent: {type: String, default: 'depot-locator:map-sync'},
        locatorId: String,
        fitPadding: {type: Number, default: 48},
        fitOnConnect: {type: Boolean, default: true},
        fitOnUpdate: {type: Boolean, default: false},
        focusOnSelect: {type: Boolean, default: true},
        clusterRadius: {type: Number, default: 60},
    };

    liveComponent = null;
    liveComponentPromise = null;
    map = null;
    tileLayer = null;
    clusterLayer = null;
    markersByDepotId = new Map();
    selectedDepotIdInternal = null;

    connect() {
        if (typeof L === 'undefined') {
            return;
        }

        this.handleMapSync = this.handleMapSync.bind(this);
        this.handleResize = this.handleResize.bind(this);

        this.initializeMap();
        this.updateFromPayload({
            markers: this.markersValue,
            selectedDepotId: this.selectedDepotIdValue || null,
            fitBounds: this.fitOnConnectValue,
        });

        window.addEventListener(this.syncEventValue, this.handleMapSync);
        window.addEventListener('resize', this.handleResize);
    }

    disconnect() {
        window.removeEventListener(this.syncEventValue, this.handleMapSync);
        window.removeEventListener('resize', this.handleResize);

        if (this.map) {
            this.map.remove();
            this.map = null;
        }

        this.clusterLayer = null;
        this.markersByDepotId.clear();
    }

    handleResize() {
        if (!this.map) {
            return;
        }

        window.requestAnimationFrame(() => {
            this.map.invalidateSize();
        });
    }

    handleMapSync(event) {
        const payload = event?.detail ?? {};

        if (
            this.hasLocatorIdValue
            && payload.locatorId
            && payload.locatorId !== this.locatorIdValue
        ) {
            return;
        }

        this.updateFromPayload(payload);
    }

    async ensureLiveComponent() {
        if (this.liveComponent) {
            return this.liveComponent;
        }

        if (this.liveComponentPromise) {
            return this.liveComponentPromise;
        }

        const liveRoot = this.element.closest(LIVE_COMPONENT_SELECTOR);
        if (!liveRoot) {
            return null;
        }

        this.liveComponentPromise = getComponent(liveRoot)
            .then((component) => {
                this.liveComponent = component;

                return component;
            })
            .catch(() => null)
            .finally(() => {
                this.liveComponentPromise = null;
            });

        return this.liveComponentPromise;
    }

    initializeMap() {
        if (this.map) {
            return;
        }

        this.map = L.map(this.element, {
            zoomControl: true,
            scrollWheelZoom: true,
            minZoom: this.hasMinZoomValue ? this.minZoomValue : 3,
            maxZoom: this.hasMaxZoomValue ? this.maxZoomValue : 18,
        });

        this.tileLayer = L.tileLayer(
            this.hasTileUrlValue ? this.tileUrlValue : DEFAULT_TILE_URL,
            {
                attribution: this.hasTileAttributionValue
                    ? this.tileAttributionValue
                    : DEFAULT_TILE_ATTRIBUTION,
            },
        );
        this.tileLayer.addTo(this.map);

        this.clusterLayer = L.markerClusterGroup({
            maxClusterRadius: this.clusterRadiusValue,
            showCoverageOnHover: false,
            spiderfyOnMaxZoom: true,
            iconCreateFunction: (cluster) => this.buildClusterIcon(cluster),
        });

        this.map.addLayer(this.clusterLayer);

        const initialCenter = this.hasCenterValue && this.centerValue.length === 2
            ? this.centerValue
            : DEFAULT_CENTER;
        const initialZoom = this.hasZoomValue ? this.zoomValue : DEFAULT_ZOOM;

        this.map.setView(initialCenter, initialZoom);
    }

    updateFromPayload(payload = {}) {
        if (!this.map || !this.clusterLayer) {
            return;
        }

        const markers = Array.isArray(payload.markers) ? payload.markers : [];
        if (Object.prototype.hasOwnProperty.call(payload, 'selectedDepotId')) {
            this.selectedDepotIdInternal = payload.selectedDepotId;
        }

        if (Array.isArray(payload.center) && payload.center.length === 2) {
            this.map.setView(
                payload.center,
                Number.isFinite(payload.zoom) ? payload.zoom : this.map.getZoom(),
            );
        } else if (Number.isFinite(payload.zoom) && markers.length === 0) {
            this.map.setZoom(payload.zoom);
        }

        this.renderMarkers(markers);

        const shouldFit = payload.fitBounds ?? this.fitOnUpdateValue;
        if (shouldFit) {
            this.fitMarkers();
        }

        if (this.selectedDepotIdInternal) {
            this.highlightSelectedMarker(this.selectedDepotIdInternal, payload.focusOnSelect ?? this.focusOnSelectValue);
        }

        window.requestAnimationFrame(() => {
            this.map.invalidateSize();
        });
    }

    renderMarkers(markers) {
        this.clusterLayer.clearLayers();
        this.markersByDepotId.clear();

        markers
            .filter((marker) => this.isValidMarker(marker))
            .forEach((marker) => {
                const leafletMarker = L.marker([marker.lat, marker.lng], {
                    icon: this.buildMarkerIcon(marker, false),
                    title: marker.title ?? marker.name ?? '',
                });

                leafletMarker.depotData = marker;

                if (marker.popupHtml) {
                    leafletMarker.bindPopup(marker.popupHtml);
                } else {
                    leafletMarker.bindPopup(this.buildPopupHtml(marker));
                }

                leafletMarker.on('click', () => {
                    this.selectDepot(marker.id);
                });

                this.markersByDepotId.set(String(marker.id), leafletMarker);
                this.clusterLayer.addLayer(leafletMarker);
            });
    }

    fitMarkers() {
        const bounds = this.clusterLayer.getBounds();
        if (!bounds.isValid()) {
            return;
        }

        this.map.fitBounds(bounds, {
            padding: [this.fitPaddingValue, this.fitPaddingValue],
        });
    }

    async selectDepot(depotId) {
        if (!depotId) {
            return;
        }

        this.selectedDepotIdInternal = String(depotId);
        this.highlightSelectedMarker(this.selectedDepotIdInternal, this.focusOnSelectValue);
        this.dispatch('select', {detail: {depotId: this.selectedDepotIdInternal}});

        const component = await this.ensureLiveComponent();
        if (component) {
            await component.action('selectDepot', {
                depotId: this.selectedDepotIdInternal,
            });
        }
    }

    highlightSelectedMarker(depotId, focus = false) {
        const selectedDepotId = String(depotId);

        this.markersByDepotId.forEach((marker, currentDepotId) => {
            const markerData = marker.depotData ?? {};
            marker.setIcon(this.buildMarkerIcon(markerData, currentDepotId === selectedDepotId));
            marker.setZIndexOffset(currentDepotId === selectedDepotId ? 1000 : 0);
        });

        const marker = this.markersByDepotId.get(selectedDepotId);
        if (!marker) {
            return;
        }

        if (focus) {
            this.map.panTo(marker.getLatLng(), {
                animate: true,
            });
        }

        marker.openPopup();
    }

    buildMarkerIcon(marker, isSelected) {
        const category = marker.category ?? {};
        const color = category.color || DEFAULT_COLOR;
        const iconMarkup = this.resolveIconMarkup(category, marker);
        const scale = isSelected ? 1.08 : 1;

        return L.divIcon({
            className: 'depot-marker',
            html: `
                <span style="
                    display:block;
                    height:56px;
                    position:relative;
                    transform:scale(${scale});
                    transform-origin:50% 100%;
                    transition:transform 150ms ease;
                    width:40px;
                    ${isSelected ? 'filter: drop-shadow(0 0 0 4px rgba(122, 31, 43, 0.14));' : ''}
                ">
                    <span style="
                        align-items:center;
                        display:flex;
                        height:56px;
                        inset:0;
                        justify-content:center;
                        position:absolute;
                        width:40px;
                    ">
                        <svg xmlns="http://www.w3.org/2000/svg" width="40" height="56" viewBox="0 0 24 24" fill="rgba(255,255,255,0.94)" stroke="${this.escapeAttribute(color)}" stroke-width="1.85" stroke-linecap="round" stroke-linejoin="round" style="display:block; filter:drop-shadow(0 10px 20px rgba(0, 0, 0, 0.14)); position:relative; z-index:1;">
                            <path d="M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0"/>
                        </svg>
                        <span style="
                            align-items:center;
                            background:rgba(255, 255, 255, 0.98);
                            border:1px solid rgba(0, 0, 0, 0.06);
                            border-radius:9999px;
                            color:${this.escapeAttribute(color)};
                            display:flex;
                            font-size:14px;
                            height:22px;
                            justify-content:center;
                            left:50%;
                            line-height:1;
                            overflow:hidden;
                            position:absolute;
                            top:14px;
                            transform:translateX(-50%);
                            width:22px;
                            z-index:3;
                        ">${iconMarkup}</span>
                    </span>
                </span>
            `,
            iconAnchor: [20, 48],
            popupAnchor: [0, -36],
        });
    }

    buildClusterIcon(cluster) {
        const childMarkers = cluster.getAllChildMarkers();
        const markerData = childMarkers.map((marker) => marker.depotData ?? {});
        const background = this.buildClusterBackground(markerData);

        return L.divIcon({
            className: 'depot-cluster',
            html: `
                <span style="
                    align-items:center;
                    background:${this.escapeAttribute(background)};
                    border-radius:9999px;
                    box-shadow:0 12px 30px rgba(0, 0, 0, 0.2);
                    color:#fff;
                    display:flex;
                    height:52px;
                    justify-content:center;
                    min-width:52px;
                    padding:0 12px;
                    position:relative;
                ">
                    <strong style="
                        align-items:center;
                        background:rgba(36, 50, 63, 0.76);
                        border-radius:9999px;
                        display:flex;
                        font-size:14px;
                        font-weight:700;
                        height:28px;
                        justify-content:center;
                        line-height:1;
                        min-width:28px;
                        padding:0 8px;
                    ">${cluster.getChildCount()}</strong>
                </span>
            `,
            iconAnchor: [26, 26],
        });
    }

    resolveDominantCategory(markerData) {
        const counters = new Map();

        markerData.forEach((marker) => {
            const category = marker.category ?? {};
            const key = category.id ?? '__default__';
            const current = counters.get(key) ?? {
                count: 0,
                category,
            };

            current.count += 1;
            counters.set(key, current);
        });

        return [...counters.values()]
            .sort((left, right) => right.count - left.count)
            .shift()?.category ?? null;
    }

    buildClusterBackground(markerData) {
        const segments = this.resolveClusterSegments(markerData);

        if (segments.length === 0) {
            return DEFAULT_CLUSTER_COLOR;
        }

        if (segments.length === 1) {
            return segments[0];
        }

        const sliceSize = 100 / segments.length;
        const gradientSegments = segments.map((color, index) => {
            const start = (index * sliceSize).toFixed(2);
            const end = ((index + 1) * sliceSize).toFixed(2);

            return `${color} ${start}% ${end}%`;
        });

        return `conic-gradient(${gradientSegments.join(', ')})`;
    }

    resolveClusterSegments(markerData) {
        const segments = new Map();

        markerData.forEach((marker) => {
            const category = marker.category ?? {};
            const key = category.id ?? category.name ?? category.color ?? '__default__';
            const color = category.color || DEFAULT_CLUSTER_COLOR;

            if (!segments.has(key)) {
                segments.set(key, color);
            }
        });

        return [...segments.values()];
    }

    resolveIconMarkup(category, marker = {}) {
        if (category.iconHtml) {
            return `<span style="display:flex; align-items:center; justify-content:center; width:14px; height:14px; position:relative; z-index:4;">${category.iconHtml}</span>`;
        }

        if (category.iconClass) {
            return `<i class="${this.escapeAttribute(category.iconClass)}" aria-hidden="true" style="display:block; font-size:14px; line-height:1; position:relative; z-index:4;"></i>`;
        }

        if (category.icon) {
            if (String(category.icon).trim().startsWith('<')) {
                return category.icon;
            }

            return `<i class="${this.escapeAttribute(category.icon)}" aria-hidden="true" style="display:block; font-size:14px; line-height:1; position:relative; z-index:4;"></i>`;
        }

        if (marker.iconText) {
            return `<span style="font-size:14px; font-weight:600; line-height:1; position:relative; z-index:4;">${this.escapeHtml(marker.iconText)}</span>`;
        }

        return '';
    }

    resolveClusterIconMarkup(category = {}) {
        if (category.iconHtml) {
            return category.iconHtml;
        }

        if (category.iconClass) {
            return `<i class="${this.escapeAttribute(category.iconClass)}" aria-hidden="true"></i>`;
        }

        if (category.icon) {
            if (String(category.icon).trim().startsWith('<')) {
                return category.icon;
            }

            return `<i class="${this.escapeAttribute(category.icon)}" aria-hidden="true"></i>`;
        }

        return '';
    }

    buildPopupHtml(marker) {
        const lines = [
            marker.title ?? marker.name ?? null,
            marker.address ?? null,
            this.formatLocality(marker),
        ].filter(Boolean);

        if (lines.length === 0) {
            return '';
        }

        return `
            <div style="min-width:180px;">
                <strong style="display:block; margin-bottom:6px;">${this.escapeHtml(lines[0])}</strong>
                ${lines.slice(1).map((line) => `<div>${this.escapeHtml(line)}</div>`).join('')}
            </div>
        `;
    }

    formatLocality(marker) {
        return [marker.postal, marker.city, marker.country]
            .filter(Boolean)
            .join(' ');
    }

    isValidMarker(marker) {
        return marker
            && marker.id
            && Number.isFinite(Number(marker.lat))
            && Number.isFinite(Number(marker.lng));
    }

    escapeHtml(value) {
        return String(value)
            .replaceAll('&', '&amp;')
            .replaceAll('<', '&lt;')
            .replaceAll('>', '&gt;')
            .replaceAll('"', '&quot;')
            .replaceAll("'", '&#039;');
    }

    escapeAttribute(value) {
        return this.escapeHtml(value);
    }
}
