import { DOCUMENT, isPlatformBrowser } from '@angular/common';
import {
  AfterViewInit,
  Component,
  ElementRef,
  Inject,
  Input,
  OnChanges,
  OnDestroy,
  PLATFORM_ID,
  SimpleChanges,
  ViewChild,
} from '@angular/core';
import type { LayerGroup, Map as LeafletMap, LatLngBounds, LatLngTuple, TileLayer } from 'leaflet';

export type MapStyle = 'minimal-light' | 'standard';

const MAP_LAYERS: Record<
  MapStyle,
  {
    url: string;
    attribution: string;
    maxZoom: number;
  }
> = {
  'minimal-light': {
    url: 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png',
    attribution:
      '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors &copy; <a href="https://carto.com/attributions">CARTO</a>',
    maxZoom: 20,
  },
  standard: {
    url: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
    attribution:
      '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
    maxZoom: 19,
  },
};

export interface MapLocation {
  id: string | number;
  label: string;
  latitude: number;
  longitude: number;
  address?: string;
}

@Component({
  selector: 'app-location-map',
  templateUrl: './location-map.component.html',
  styleUrl: './location-map.component.scss',
})
export class LocationMapComponent implements AfterViewInit, OnChanges, OnDestroy {
  @Input({ required: true }) locations: readonly MapLocation[] = [];
  @Input() ariaLabel = 'Mapa de ubicaciones';
  @Input() fallbackCenter: LatLngTuple = [-32.94682, -60.63932];
  @Input() fallbackZoom = 13;
  @Input() maxFitZoom = 15;
  @Input() mapStyle: MapStyle = 'minimal-light';
  @Input() markerColor = '#24a9e0';

  @ViewChild('mapContainer', { static: true })
  private readonly mapContainer!: ElementRef<HTMLDivElement>;

  private leaflet: typeof import('leaflet') | null = null;
  private map: LeafletMap | null = null;
  private baseLayer: TileLayer | null = null;
  private markerLayer: LayerGroup | null = null;

  constructor(
    @Inject(PLATFORM_ID) private readonly platformId: object,
    @Inject(DOCUMENT) private readonly document: Document,
  ) {}

  ngAfterViewInit(): void {
    if (isPlatformBrowser(this.platformId)) {
      void this.initializeMap();
    }
  }

  ngOnChanges(changes: SimpleChanges): void {
    if (changes['mapStyle'] && this.map) {
      this.renderBaseLayer();
    }

    if ((changes['locations'] || changes['markerColor']) && this.map) {
      this.renderLocations();
    }
  }

  ngOnDestroy(): void {
    this.map?.remove();
    this.map = null;
    this.baseLayer = null;
    this.markerLayer = null;
  }

  protected directionsUrl(location: MapLocation): string {
    const destination = `${location.latitude},${location.longitude}`;
    return `https://www.google.com/maps/dir/?api=1&destination=${encodeURIComponent(destination)}`;
  }

  private async initializeMap(): Promise<void> {
    const leafletModule = await import('leaflet');
    const leaflet =
      (
        leafletModule as unknown as {
          default?: typeof import('leaflet');
        }
      ).default ?? leafletModule;

    if (!this.mapContainer.nativeElement.isConnected || this.map) {
      return;
    }

    this.leaflet = leaflet;
    this.map = leaflet.map(this.mapContainer.nativeElement, {
      center: this.fallbackCenter,
      zoom: this.fallbackZoom,
      scrollWheelZoom: false,
    });

    this.renderBaseLayer();
    this.markerLayer = leaflet.layerGroup().addTo(this.map);
    this.renderLocations();
  }

  private renderBaseLayer(): void {
    if (!this.leaflet || !this.map) {
      return;
    }

    if (this.baseLayer) {
      this.map.removeLayer(this.baseLayer);
    }

    const layer = MAP_LAYERS[this.mapStyle];
    this.baseLayer = this.leaflet
      .tileLayer(layer.url, {
        maxZoom: layer.maxZoom,
        attribution: layer.attribution,
        crossOrigin: true,
      })
      .addTo(this.map);
  }

  private renderLocations(): void {
    if (!this.leaflet || !this.map || !this.markerLayer) {
      return;
    }

    this.markerLayer.clearLayers();
    const validLocations = this.locations.filter(
      ({ latitude, longitude }) =>
        Number.isFinite(latitude) &&
        Number.isFinite(longitude) &&
        latitude >= -90 &&
        latitude <= 90 &&
        longitude >= -180 &&
        longitude <= 180,
    );

    if (validLocations.length === 0) {
      this.map.setView(this.fallbackCenter, this.fallbackZoom);
      return;
    }

    const bounds: LatLngBounds = this.leaflet.latLngBounds([]);

    for (const location of validLocations) {
      const coordinates: LatLngTuple = [location.latitude, location.longitude];
      bounds.extend(coordinates);

      const popup = this.document.createElement('div');
      const title = this.document.createElement('strong');
      title.textContent = location.label;
      popup.append(title);

      if (location.address) {
        const address = this.document.createElement('div');
        address.textContent = location.address;
        popup.append(address);
      }

      const directions = this.document.createElement('a');
      directions.href = this.directionsUrl(location);
      directions.target = '_blank';
      directions.rel = 'noopener noreferrer';
      directions.textContent = 'Cómo llegar';
      popup.append(directions);

      const tooltip = this.document.createElement('span');
      tooltip.textContent = location.label;

      this.leaflet
        .circleMarker(coordinates, {
          radius: 8,
          color: '#ffffff',
          weight: 3,
          fillColor: this.markerColor,
          fillOpacity: 1,
        })
        .bindTooltip(tooltip)
        .bindPopup(popup)
        .addTo(this.markerLayer);
    }

    if (validLocations.length === 1) {
      this.map.setView(bounds.getCenter(), this.maxFitZoom);
    } else {
      this.map.fitBounds(bounds, {
        padding: [32, 32],
        maxZoom: this.maxFitZoom,
      });
    }
  }
}
