<script setup lang="ts">
import { onMounted, onUnmounted, ref, Ref, computed } from 'vue';
import SectionMain from '@/Components/SectionMain.vue';
import L, {
    Map as LeafletMap,
    Rectangle,
    LayerGroup,
    Control,
    type Layer,
    type LatLng,
    type LatLngBounds,
    type LatLngBoundsExpression,
    type MapOptions,
    type Renderer,
    type RendererOptions,
    type LeafletEvent,
} from 'leaflet';
import { tileLayerWMS } from 'leaflet/src/layer/tile/TileLayer.WMS';
import axios from 'axios';
import DrawControlComponent from '@/Components/Map/draw.component.vue';
import ZoomControlComponent from '@/Components/Map/zoom.component.vue';
import { MapService } from '@/Stores/map.service';
import { OpensearchDocument } from '@/Dataset';

/**
 * Leaflet's internal renderer machinery is not part of the public @types/leaflet
 * surface, so we describe the bits we touch here. This keeps the mixin body typed
 * instead of falling back to `any`.
 */
interface RendererCapableMap extends LeafletMap {
    options: MapOptions & { renderer?: Renderer; preferCanvas?: boolean };
    _renderer?: Renderer;
    _paneRenderers: Record<string, Renderer | undefined>;
    _getPaneRenderer(name?: string): Renderer | false;
    _createRenderer(options?: RendererOptions): Renderer;
}

LeafletMap.include({
    getRenderer(this: RendererCapableMap, layer: Layer): Renderer {
        const layerOptions = layer.options as { renderer?: Renderer; pane?: string };
        let renderer: Renderer | false | undefined =
            layerOptions.renderer || this._getPaneRenderer(layerOptions.pane) || this.options.renderer || this._renderer;

        if (!renderer) {
            renderer = this._renderer = this._createRenderer();
        }

        if (!this.hasLayer(renderer)) {
            this.addLayer(renderer);
        }
        return renderer;
    },

    _getPaneRenderer(this: RendererCapableMap, name?: string): Renderer | false {
        if (name === 'overlayPane' || name === undefined) {
            return false;
        }

        let renderer = this._paneRenderers[name];
        if (renderer === undefined) {
            renderer = this._createRenderer({ pane: name });
            this._paneRenderers[name] = renderer;
        }
        return renderer;
    },

    _createRenderer(this: RendererCapableMap, options?: RendererOptions): Renderer {
        return (this.options.preferCanvas && L.canvas(options)) || L.svg(options);
    },
});

const DEFAULT_BASE_LAYER_NAME = 'BaseLayer';
const DEFAULT_BASE_LAYER_ATTRIBUTION = '&copy; <a target="_blank" href="http://osm.org/copyright">OpenStreetMap</a> contributors';

let map: LeafletMap;

const props = defineProps({
    checkable: Boolean,
    datasets: {
        type: Array<OpensearchDocument>,
        default: () => [],
    },
    mapId: {
        type: String,
        default: 'map',
    },
    // OpenSearch host is provided by the server (prop / shared data), never imported
    // from server-only modules into client code.
    opensearchHost: {
        type: String,
        default: 'localhost',
    },
    mapOptions: {
        type: Object,
        default: () => ({
            center: [48.208174, 16.373819],
            zoom: 3,
            zoomControl: false,
            attributionControl: false,
        }),
    },
});

const OPENSEARCH_HOST = computed(() => `${window.location.protocol}//${props.opensearchHost}:9200`);

const items = computed<OpensearchDocument[]>({
    get() {
        return props.datasets;
    },
    set(value) {
        props.datasets.length = 0;
        props.datasets.push(...value);
    },
});

const resultCount = computed(() => items.value.length);

const fitBounds: LatLngBoundsExpression = [
    [46.4318173285, 9.47996951665],
    [49.0390742051, 16.9796667823],
];

const drawControl = ref<InstanceType<typeof DrawControlComponent> | null>(null);
const southWest = ref<LatLng | null>(null);
const northEast = ref<LatLng | null>(null);
const mapService = MapService();
const isLoading = ref(false);
const hasSearched = ref(false);

const filterLayerGroup = new LayerGroup();

/** Minimal shape of the leaflet-draw "created" event (no @types/leaflet-draw needed). */
interface DrawCreatedEvent extends LeafletEvent {
    layer: Layer & { getBounds(): LatLngBounds };
    layerType: string;
}

/** Shape of the OpenSearch _search response we rely on. */
interface OpenSearchHit {
    _source: OpensearchDocument & {
        bbox_xmin: number;
        bbox_xmax: number;
        bbox_ymin: number;
        bbox_ymax: number;
    };
}
interface OpenSearchSearchResponse {
    hits: {
        hits: OpenSearchHit[];
    };
}

onMounted(() => {
    initMap();
});

onUnmounted(() => {
    map.off('zoomend zoomlevelschange');
});

const initMap = async (): Promise<void> => {
    map = new LeafletMap('map', props.mapOptions as MapOptions);
    mapService.setMap(props.mapId, map);
    map.scrollWheelZoom.disable();
    map.fitBounds(fitBounds);
    drawControl.value?.toggleDraw();

    map.addLayer(filterLayerGroup);

    const attributionControl = new Control.Attribution().addTo(map);
    attributionControl.setPrefix(false);

    const osmGray = tileLayerWMS('https://ows.terrestris.de/osm-gray/service', {
        format: 'image/png',
        attribution: DEFAULT_BASE_LAYER_ATTRIBUTION,
        layers: 'OSM-WMS',
    });

    const layerOptions = {
        label: DEFAULT_BASE_LAYER_NAME,
        visible: true,
        layer: osmGray,
    };
    layerOptions.layer.addTo(map);

    map.on('Draw.Event.CREATED', handleDrawEventCreated as L.LeafletEventHandlerFn);
};

const handleDrawEventCreated = async (event: DrawCreatedEvent): Promise<void> => {
    isLoading.value = true;
    hasSearched.value = true;
    filterLayerGroup.clearLayers();
    items.value = [];

    const bounds: LatLngBounds = event.layer.getBounds();

    try {
        // NOTE: OpenSearch _search with a query body must be POST — browsers/XHR
        // drop the request body on GET, which would send an empty query.
        const response = await axios<OpenSearchSearchResponse>({
            method: 'POST',
            url: OPENSEARCH_HOST.value + '/tethys-records/_search',
            headers: { 'Content-Type': 'application/json' },
            data: {
                size: 1000,
                query: {
                    bool: {
                        must: {
                            match_all: {},
                        },
                        filter: {
                            geo_shape: {
                                geo_location: {
                                    shape: {
                                        type: 'envelope',
                                        coordinates: [
                                            [bounds.getSouthWest().lng, bounds.getNorthEast().lat],
                                            [bounds.getNorthEast().lng, bounds.getSouthWest().lat],
                                        ],
                                    },
                                    relation: 'within',
                                },
                            },
                        },
                    },
                },
            },
        });

        response.data.hits.hits.forEach((hit: OpenSearchHit) => {
            const xMin = hit._source.bbox_xmin;
            const xMax = hit._source.bbox_xmax;
            const yMin = hit._source.bbox_ymin;
            const yMax = hit._source.bbox_ymax;
            const bbox: LatLngBoundsExpression = [
                [yMin, xMin],
                [yMax, xMax],
            ];

            const rect = new Rectangle(bbox, {
                color: '#65DC21',
                weight: 2,
                fillColor: '#65DC21',
                fillOpacity: 0.2,
                className: 'animated-rectangle',
            });
            filterLayerGroup.addLayer(rect);
            items.value.push(hit._source);
        });
    } catch (error) {
        if (axios.isAxiosError(error) && error.response?.status === 400) {
            // Mapping in diesem Index unterstützt keine geo_shape-Suche
            console.warn('Geo search unavailable for this index mapping');
            // optional: ein dezentes Hinweis-Flag setzen, z. B. searchUnavailable.value = true
        } else {
            console.error(error);
        }
        items.value = [];
    } finally {
        isLoading.value = false;
    }
};
</script>

<template>
    <SectionMain>
        <div class="map-shell">
            <!-- Header -->
            <div class="map-header">
                <div class="map-header-title">
                    <span class="map-dot"></span>
                    <h2>Geospatial Discovery</h2>
                </div>
                <transition name="badge">
                    <div v-if="hasSearched && !isLoading" class="result-badge" :class="{ 'is-empty': resultCount === 0 }">
                        {{ resultCount }} {{ resultCount === 1 ? 'dataset' : 'datasets' }}
                    </div>
                </transition>
            </div>

            <div class="map-container-wrapper">
                <!-- Loading Overlay -->
                <transition name="fade">
                    <div v-if="isLoading" class="loading-overlay">
                        <div class="loading-spinner"></div>
                        <p class="loading-text">Searching datasets…</p>
                    </div>
                </transition>

                <!-- Floating instruction chip -->
                <div class="map-instructions">
                    <svg class="instruction-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                        <circle cx="12" cy="12" r="10" />
                        <path d="M12 16v-4M12 8h.01" />
                    </svg>
                    <p class="instruction-text"><strong>Tip:</strong> draw an area to discover datasets within it</p>
                </div>

                <!-- Floating "no results" hint -->
                <transition name="fade">
                    <div v-if="hasSearched && !isLoading && resultCount === 0" class="empty-hint">
                        No datasets in the selected area — try a larger region.
                    </div>
                </transition>

                <div id="map" class="map-container">
                    <ZoomControlComponent ref="zoomControl" :mapId="mapId"></ZoomControlComponent>
                    <DrawControlComponent ref="drawControl" :preserve="false" :mapId="mapId" :southWest="southWest" :northEast="northEast">
                    </DrawControlComponent>
                </div>
            </div>
        </div>
    </SectionMain>
</template>

<style scoped>
.map-shell {
    border-radius: 1.25rem;
    overflow: hidden;
    background: white;
    box-shadow:
        0 10px 30px -12px rgba(0, 0, 0, 0.25),
        0 4px 8px -4px rgba(0, 0, 0, 0.1);
    border: 1px solid rgba(0, 0, 0, 0.05);
}

.dark .map-shell {
    background: #1f2937;
    border-color: rgba(255, 255, 255, 0.06);
}

/* Header */
.map-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 1rem 1.5rem;
    background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
    color: white;
}

.map-header-title {
    display: flex;
    align-items: center;
    gap: 0.65rem;
}

.map-header-title h2 {
    font-size: 1rem;
    font-weight: 700;
    letter-spacing: 0.01em;
    margin: 0;
}

.map-dot {
    width: 0.6rem;
    height: 0.6rem;
    border-radius: 9999px;
    background: #65dc21;
    box-shadow: 0 0 0 0 rgba(101, 220, 33, 0.6);
    animation: pulseDot 2s infinite;
}

@keyframes pulseDot {
    0% {
        box-shadow: 0 0 0 0 rgba(101, 220, 33, 0.6);
    }
    70% {
        box-shadow: 0 0 0 0.5rem rgba(101, 220, 33, 0);
    }
    100% {
        box-shadow: 0 0 0 0 rgba(101, 220, 33, 0);
    }
}

.result-badge {
    font-size: 0.8rem;
    font-weight: 600;
    padding: 0.35rem 0.85rem;
    border-radius: 9999px;
    background: rgba(101, 220, 33, 0.15);
    color: #a3f57c;
    border: 1px solid rgba(101, 220, 33, 0.4);
}

.result-badge.is-empty {
    background: rgba(148, 163, 184, 0.15);
    color: #cbd5e1;
    border-color: rgba(148, 163, 184, 0.35);
}

/* Map Container */
.map-container-wrapper {
    position: relative;
    background: #f9fafb;
}

.dark .map-container-wrapper {
    background: #111827;
}

/* Floating instruction chip */
.map-instructions {
    position: absolute;
    top: 1rem;
    left: 1rem;
    z-index: 500;
    display: flex;
    align-items: center;
    gap: 0.6rem;
    padding: 0.65rem 1rem;
    border-radius: 9999px;
    background: rgba(255, 255, 255, 0.85);
    backdrop-filter: blur(10px);
    -webkit-backdrop-filter: blur(10px);
    box-shadow: 0 6px 16px -6px rgba(0, 0, 0, 0.25);
    border: 1px solid rgba(255, 255, 255, 0.6);
    max-width: calc(100% - 2rem);
}

.dark .map-instructions {
    background: rgba(31, 41, 55, 0.8);
    border-color: rgba(255, 255, 255, 0.08);
}

.instruction-icon {
    width: 1.15rem;
    height: 1.15rem;
    color: #65dc21;
    flex-shrink: 0;
}

.instruction-text {
    font-size: 0.8125rem;
    color: #4b5563;
    margin: 0;
    white-space: nowrap;
}

.dark .instruction-text {
    color: #d1d5db;
}

.instruction-text strong {
    color: #4d9e1a;
    font-weight: 700;
}

.dark .instruction-text strong {
    color: #65dc21;
}

/* Floating empty hint */
.empty-hint {
    position: absolute;
    bottom: 1rem;
    left: 50%;
    transform: translateX(-50%);
    z-index: 500;
    padding: 0.55rem 1.1rem;
    border-radius: 9999px;
    font-size: 0.8125rem;
    font-weight: 500;
    color: #475569;
    background: rgba(255, 255, 255, 0.9);
    backdrop-filter: blur(10px);
    -webkit-backdrop-filter: blur(10px);
    box-shadow: 0 6px 16px -6px rgba(0, 0, 0, 0.25);
}

.dark .empty-hint {
    background: rgba(31, 41, 55, 0.85);
    color: #cbd5e1;
}

/* Loading Overlay */
.loading-overlay {
    position: absolute;
    inset: 0;
    background: rgba(255, 255, 255, 0.9);
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    z-index: 1000;
    backdrop-filter: blur(6px);
    -webkit-backdrop-filter: blur(6px);
}

.dark .loading-overlay {
    background: rgba(17, 24, 39, 0.9);
}

.loading-spinner {
    width: 3rem;
    height: 3rem;
    border: 4px solid #e5e7eb;
    border-top-color: #65dc21;
    border-radius: 50%;
    animation: spin 1s linear infinite;
}

.dark .loading-spinner {
    border-color: #374151;
    border-top-color: #65dc21;
}

@keyframes spin {
    to {
        transform: rotate(360deg);
    }
}

.loading-text {
    margin-top: 1rem;
    font-size: 0.875rem;
    font-weight: 600;
    color: #4d9e1a;
}

.dark .loading-text {
    color: #65dc21;
}

/* Map Container */
.map-container {
    position: relative;
    height: 600px;
    width: 100%;
    background: #f9fafb;
}

.dark .map-container {
    background: #111827;
}

/* Leaflet Overrides */
:deep(.leaflet-container) {
    height: 600px;
    width: 100%;
    background: transparent;
    font-family: inherit;
}

:deep(.leaflet-container .leaflet-pane) {
    z-index: 30 !important;
}

/* Enhanced Rectangle Styling */
:deep(.animated-rectangle) {
    animation: pulseRectangle 2s ease-in-out infinite;
}

@keyframes pulseRectangle {
    0%,
    100% {
        opacity: 0.6;
    }
    50% {
        opacity: 1;
    }
}

/* Control Enhancements */
:deep(.leaflet-control) {
    border-radius: 0.65rem;
    box-shadow: 0 4px 12px -4px rgba(0, 0, 0, 0.2);
    border: none;
    overflow: hidden;
}

:deep(.leaflet-bar a) {
    border-radius: 0;
    transition: all 0.2s ease;
}

:deep(.leaflet-bar a:hover) {
    background: #65dc21;
    color: white;
}

:deep(.leaflet-draw-toolbar a) {
    background: white;
    transition: all 0.2s ease;
}

.dark :deep(.leaflet-draw-toolbar a) {
    background: #374151;
    color: #d1d5db;
}

:deep(.leaflet-draw-toolbar a:hover) {
    background: #65dc21;
}

/* Popup Enhancements */
:deep(.leaflet-popup-content-wrapper) {
    border-radius: 0.85rem;
    box-shadow: 0 12px 24px -8px rgba(0, 0, 0, 0.2);
}

:deep(.leaflet-popup-tip) {
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

/* Transitions */
.fade-enter-active,
.fade-leave-active {
    transition: opacity 0.25s ease;
}
.fade-enter-from,
.fade-leave-to {
    opacity: 0;
}

.badge-enter-active,
.badge-leave-active {
    transition: all 0.25s ease;
}
.badge-enter-from,
.badge-leave-to {
    opacity: 0;
    transform: scale(0.85);
}

/* Responsive Design */
@media (max-width: 768px) {
    .map-container,
    :deep(.leaflet-container) {
        height: 420px;
    }

    .instruction-text {
        white-space: normal;
    }
}

@media (max-width: 640px) {
    .map-container,
    :deep(.leaflet-container) {
        height: 360px;
    }
}
</style>
