import crossfilter from "crossfilter2";
import * as d3 from "d3";
import mapboxgl from "mapbox-gl";
import 'mapbox-gl/dist/mapbox-gl.css';
import { useEffect, useRef, useMemo, useCallback } from "react";
import { renderToString } from 'react-dom/server';
import "../assets/Map.css";
import { Metric, type ParameterValues } from "./Parameters";
import { PropertyCard } from "./PropertyCard";
import { ScrollArea } from "./ui/scroll-area";
import type { GeoJSONFeatureCollection, PropertyFeature, PropertyProperties, POI } from "@/types";
import { MAP_CONFIG, HEATMAP_CONFIG, PERCENTILE_CONFIG } from "@/constants";
import { getColorSchemeForMetric, getMetricInterpretation } from "@/constants/colorSchemes";
import { clone, percentile, calculateColorStops } from "@/utils/mapUtils";

// Type declaration for the external HexgridHeatmap library
declare class HexgridHeatmap {
    _tree: {
        search: (bounds: { minX: number; maxX: number; minY: number; maxY: number }) => PropertyWithCoords[];
    };
    constructor(map: mapboxgl.Map, id: string, beforeLayer: string);
    setIntensity(value: number): void;
    setSpread(value: number): void;
    setCellDensity(value: number): void;
    setPropertyName(name: string): void;
    setData(data: GeoJSONFeatureCollection): void;
    setColorStops(stops: [number, string][]): void;
    update(): void;
}

interface PropertyWithCoords {
    properties: PropertyProperties;
}

interface CrossfilterRecord extends PropertyProperties {
    index: number;
}

interface MapProps {
    listingData: GeoJSONFeatureCollection;
    queryParameters: ParameterValues | null;
    onPropertyClick?: (property: PropertyProperties, coordinates: [number, number]) => void;
    pois?: POI[];
}

interface FilterState {
    city: string;
    country: string | null;
    mode: string;
    count?: number;
}

export function Map(props: MapProps) {
    const data = props.listingData;

    const mapRef = useRef<mapboxgl.Map | null>(null);
    const mapContainerRef = useRef<HTMLDivElement | null>(null);
    const heatmapRef = useRef<HexgridHeatmap | null>(null);
    const updateTimeoutRef = useRef<number | null>(null);
    const isMapLoadedRef = useRef<boolean>(false);
    const lastDataLengthRef = useRef<number>(0);
    const poiMarkersRef = useRef<mapboxgl.Marker[]>([]);

    const filter: FilterState = { city: 'London', country: null, mode: Metric.qmprice };
    if (props.queryParameters) {
        filter.mode = props.queryParameters.metric;
    }

    // Get appropriate color scheme based on metric
    const colorScheme = useMemo(() => {
        return getColorSchemeForMetric(filter.mode);
    }, [filter.mode]);

    const metricInfo = useMemo(() => {
        return getMetricInterpretation(filter.mode);
    }, [filter.mode]);

    // Calculate average price per sqm for property cards
    const avgPricePerSqm = useMemo(() => {
        const validPrices = data.features
            .map((f) => f.properties.qmprice)
            .filter((p): p is number => typeof p === 'number' && p > 0);
        return validPrices.length > 0
            ? validPrices.reduce((a, b) => a + b, 0) / validPrices.length
            : 0;
    }, [data]);

    // Build crossfilter data
    const buildCrossfilterData = useCallback(() => {
        return data.features.map(function (d: PropertyFeature, i: number) {
            const propsCopy = clone(d.properties) as CrossfilterRecord;
            propsCopy.index = i;
            return propsCopy;
        });
    }, [data]);

    const updateHeatmap = useCallback(() => {
        if (!mapRef.current || !isMapLoadedRef.current) return;

        const crossData = buildCrossfilterData();
        const cf = crossfilter(crossData);
        const qmDim = cf.dimension(function (d: CrossfilterRecord) { return d.qm; });
        const cityDim = cf.dimension(function (d: CrossfilterRecord) { return d.city; });
        const countryDim = cf.dimension(function (d: CrossfilterRecord) { return d.country; });
        const indexDim = cf.dimension(function (d: CrossfilterRecord) { return d.index; });

        // Create heatmap if it doesn't exist
        if (!heatmapRef.current) {
            heatmapRef.current = new HexgridHeatmap(mapRef.current, "hexgrid-heatmap", "waterway-label");
            heatmapRef.current.setIntensity(HEATMAP_CONFIG.INTENSITY);
            heatmapRef.current.setSpread(HEATMAP_CONFIG.SPREAD);
            heatmapRef.current.setCellDensity(HEATMAP_CONFIG.CELL_DENSITY);
        }

        const heatmap = heatmapRef.current;
        heatmap.setPropertyName(filter.mode);

        if (filter.mode === Metric.qmprice) {
            qmDim.filter((d) => (d as number) > 0);
        }

        if (filter.city) {
            cityDim.filterExact(filter.city);
        } else if (filter.country) {
            countryDim.filterExact(filter.country);
        }

        const subset: GeoJSONFeatureCollection = { type: "FeatureCollection", features: [] };
        indexDim.top(Infinity).forEach(function (i: CrossfilterRecord) {
            subset.features.push(data.features[i.index]);
        });

        // Update heatmap data
        heatmap.setData(subset);
        let values = subset.features.map(function (d: PropertyFeature) {
            return d.properties[filter.mode as keyof PropertyProperties] as number;
        });
        values = values.sort(function (a: number, b: number) { return a - b; });

        const minIndex = Math.round(values.length * PERCENTILE_CONFIG.MIN_BOUND);
        const maxIndex = Math.round(values.length * PERCENTILE_CONFIG.MAX_BOUND);
        const min = values[minIndex];
        const max = values[maxIndex];

        makeLegend(colorScheme, min, max);
        const colorStopsValue = calculateColorStops(colorScheme, min, max);
        heatmap.setColorStops(colorStopsValue);
        heatmap.update();

        // Fit bounds only on first load or significant data change
        if (lastDataLengthRef.current === 0 && subset.features.length > 0) {
            const longitudes = subset.features.map(function (d: PropertyFeature) { return d.geometry.coordinates[0]; }).sort(function (a: number, b: number) { return a - b; });
            const latitudes = subset.features.map(function (d: PropertyFeature) { return d.geometry.coordinates[1]; }).sort(function (a: number, b: number) { return a - b; });
            const minlng = percentile(longitudes, PERCENTILE_CONFIG.BOUNDS_CLIP_MIN);
            const maxlng = percentile(longitudes, PERCENTILE_CONFIG.BOUNDS_CLIP_MAX);
            const minlat = percentile(latitudes, PERCENTILE_CONFIG.BOUNDS_CLIP_MIN);
            const maxlat = percentile(latitudes, PERCENTILE_CONFIG.BOUNDS_CLIP_MAX);

            mapRef.current?.fitBounds([
                [minlng, minlat],
                [maxlng, maxlat]
            ], { duration: 0 });
        }

        lastDataLengthRef.current = subset.features.length;
    }, [data, filter.mode, filter.city, filter.country, colorScheme, buildCrossfilterData]);

    // Initialize map
    useEffect(() => {
        if (!mapContainerRef.current) return;

        mapboxgl.accessToken = MAP_CONFIG.MAPBOX_TOKEN;
        mapRef.current = new mapboxgl.Map({
            container: mapContainerRef.current,
            style: MAP_CONFIG.STYLE,
            center: MAP_CONFIG.DEFAULT_CENTER,
            zoom: MAP_CONFIG.DEFAULT_ZOOM
        });
        mapRef.current.on('load', function () {
            isMapLoadedRef.current = true;
            lastDataLengthRef.current = 0;
            updateHeatmap();
        });
        mapRef.current.on('click', function (e: mapboxgl.MapMouseEvent) {
            if (!mapRef.current) return;
            const layers = ['hexgrid-heatmap', 'hexgrid-heatmap-back']
                .filter(l => mapRef.current!.getLayer(l));
            if (layers.length === 0) return;
            const features = mapRef.current.queryRenderedFeatures(e.point, { layers });
            if (features.length > 0) {
                const props = features[0].properties;
                if (props && props.searchMinX !== undefined) {
                    openListingsDialog(e.lngLat.lng, e.lngLat.lat, {
                        minX: props.searchMinX,
                        minY: props.searchMinY,
                        maxX: props.searchMaxX,
                        maxY: props.searchMaxY
                    });
                }
            }
        });
        mapRef.current.on('mousemove', function (e: mapboxgl.MapMouseEvent) {
            if (!mapRef.current) return;
            const layers = ['hexgrid-heatmap', 'hexgrid-heatmap-back']
                .filter(l => mapRef.current!.getLayer(l));
            if (layers.length === 0) return;
            const features = mapRef.current.queryRenderedFeatures(e.point, { layers });
            mapRef.current.getCanvas().style.cursor = features.length > 0 ? 'pointer' : '';
        });
        return () => {
            if (updateTimeoutRef.current) {
                clearTimeout(updateTimeoutRef.current);
            }
            heatmapRef.current = null;
            isMapLoadedRef.current = false;
            mapRef.current?.remove();
        };
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, []);

    // Debounced update effect - only update after 200ms of no changes
    useEffect(() => {
        if (!isMapLoadedRef.current) return;

        // Clear any pending update
        if (updateTimeoutRef.current) {
            clearTimeout(updateTimeoutRef.current);
        }

        // Schedule new update after 200ms of no changes
        updateTimeoutRef.current = window.setTimeout(() => {
            updateHeatmap();
        }, 200);

        return () => {
            if (updateTimeoutRef.current) {
                clearTimeout(updateTimeoutRef.current);
            }
        };
    }, [data, updateHeatmap]);

    // Update POI markers when pois prop changes
    useEffect(() => {
        if (!mapRef.current || !isMapLoadedRef.current) return;

        // Remove existing markers
        poiMarkersRef.current.forEach(m => m.remove());
        poiMarkersRef.current = [];

        if (!props.pois) return;

        for (const poi of props.pois) {
            const el = document.createElement('div');
            el.className = 'poi-marker';
            el.style.cssText = 'width:24px;height:24px;background:#ef4444;border:2px solid white;border-radius:50%;box-shadow:0 2px 4px rgba(0,0,0,0.3);cursor:pointer;';
            el.title = poi.name;

            const marker = new mapboxgl.Marker({ element: el })
                .setLngLat([poi.longitude, poi.latitude])
                .setPopup(new mapboxgl.Popup({ offset: 12 }).setHTML(
                    `<div style="padding:4px 8px"><strong>${poi.name}</strong><br/><span style="color:#666;font-size:12px">${poi.address}</span></div>`
                ))
                .addTo(mapRef.current);

            poiMarkersRef.current.push(marker);
        }
    }, [props.pois]);

    function makeLegend(colorstops: [number, string][], minValue: number, maxValue: number) {
        const svg_height = 280, svg_width = 80;
        d3.select('#svg').selectAll('*').remove();
        const svg = d3.select('#svg');
        svg
            .attr('height', svg_height)
            .attr('width', svg_width);

        // Add metric name at top
        svg.append("text")
            .attr("x", svg_width / 2)
            .attr("y", 12)
            .attr("text-anchor", "middle")
            .attr("font-size", "11px")
            .attr("font-weight", "600")
            .attr("fill", "#374151")
            .text(metricInfo.name);

        const gradientTop = 30;
        const gradientHeight = svg_height - 70;

        const linearGradient = svg.append("defs")
            .append("linearGradient")
            .attr("id", "linear-gradient");

        linearGradient
            .attr("x1", "0%")
            .attr("y1", "100%")
            .attr("x2", "0%")
            .attr("y2", "0%");

        svg.append("rect")
            .attr("x", 0)
            .attr("y", gradientTop)
            .attr("width", svg_width * 0.35)
            .attr("height", gradientHeight)
            .attr('rx', 4)
            .style("fill", "url(#linear-gradient)");

        colorstops.forEach(function (d: [number, string]) {
            linearGradient.append("stop")
                .attr("offset", d[0] + "%")
                .attr("stop-color", d[1]);
        });

        const xScale = d3.scaleLinear().range([gradientHeight - 10, 0]).domain([minValue, maxValue]);
        const xAxis = d3.axisRight(xScale).ticks(5).tickFormat((d) => {
            const num = d as number;
            if (num >= 1000) {
                return `${(num / 1000).toFixed(1)}k`;
            }
            return String(Math.round(num));
        });

        svg.append("g")
            .attr("class", "axis")
            .attr("transform", "translate(" + (svg_width * 0.38) + "," + (gradientTop + 5) + ")")
            .call(xAxis)
            .selectAll("text")
            .attr("font-size", "10px");

        // Add interpretation labels at bottom
        svg.append("text")
            .attr("x", svg_width / 2)
            .attr("y", svg_height - 25)
            .attr("text-anchor", "middle")
            .attr("font-size", "9px")
            .attr("fill", "#22c55e")
            .text(metricInfo.low);

        svg.append("text")
            .attr("x", svg_width / 2)
            .attr("y", svg_height - 10)
            .attr("text-anchor", "middle")
            .attr("font-size", "9px")
            .attr("fill", "#ef4444")
            .text(metricInfo.high);
    }

    function openListingsDialog(longitude: number, latitude: number, searchBounds: { minX: number; minY: number; maxX: number; maxY: number }) {
        if (!heatmapRef.current || !mapRef.current) return;

        const properties = heatmapRef.current._tree.search(searchBounds);
        if (properties.length > 0) {
            const listingDialogPopup = getListingDialog(properties);
            new mapboxgl.Popup()
                .setLngLat([longitude, latitude])
                .setHTML(renderToString(listingDialogPopup))
                .setMaxWidth("450px")
                .addTo(mapRef.current);
        }
    }

    function getListingDialog(properties: PropertyWithCoords[]) {
        return (
            <ScrollArea className="rounded-md">
                <div className="overflow-y-auto max-h-[500px] w-[420px]">
                    <div className="px-3 py-2 text-sm font-medium border-b bg-muted/50">
                        <strong>{properties.length}</strong> properties in this area
                    </div>
                    <div className="p-2 space-y-2">
                        {properties.map((property) => (
                            <PropertyCard
                                key={property.properties.url}
                                property={property.properties}
                                variant="full"
                                avgPricePerSqm={avgPricePerSqm}
                            />
                        ))}
                    </div>
                </div>
            </ScrollArea>
        );
    }

    return (
        <div className="relative w-full h-full">
            <div id='map-container' ref={mapContainerRef}></div>
            <div id="legend">
                <svg id="svg"></svg>
            </div>
        </div>
    );
}

// Re-export types for backwards compatibility
export { Metric, type ParameterValues } from "./Parameters";
