import { AfterViewInit, Component, OnInit } from '@angular/core';
import { DictionariesService } from '@app/modules/components/page-filters/services/dictionaries-service';
import { FlightsMapFiltersStateService, IFlightsMapFilterState } from '@app/shared/services/filters/flights-map-filters-state.service';
import { environment } from '@environment';
import * as L from 'leaflet';
import { from, of, Subject, Subscription } from 'rxjs';
import { catchError, distinctUntilChanged, finalize, map, switchMap, takeUntil, tap } from 'rxjs/operators';
import { FlightsMapApiService, IDestinationsRequestParams } from '../../services/flights-map-api.service';
import { CityCategoryService } from '../../services/category-city.service';
import { IDestinationsResponse } from '@typings/responses';
import * as moment from 'moment';
import { CityModel } from '@app/modules/components/page-filters/models';
import { IDestinationResponse } from '../../../../../typings/responses';
import { airports } from '@app/shared/services';


export const markerBlue = L.icon({
  iconUrl:        'assets/img/leaflet/marker-blue.png',
  iconSize:       [15, 15],
  iconAnchor:     [5, 5],
  popupAnchor:    [0, -10] 
});

export const markerBlueSmall = L.icon({
  iconUrl:        'assets/img/leaflet/marker-blue-small.png',
  iconSize:       [11, 11],
  iconAnchor:     [5, 5],
  popupAnchor:    [0, -10]
});

export const markerOrange = L.icon({
  iconUrl:        'assets/img/leaflet/marker-orange.png',
  iconSize:       [20, 20],
  iconAnchor:     [10, 10],
  popupAnchor:    [0, -20]
});

const directRoutePolyLine : L.PolylineOptions = 
  {
    color  : '#2457ff',
    weight : 1,
    opacity: 1
  }

const dashRoutePolyLine: L.PolylineOptions =       
{
  color    : '#2433ff',
  weight   : 1,
  opacity  : 1,
  dashArray: '4 14'
}

type CountryType = 'ru' | 'other';

@Component({
  selector: 'flights-map-body',
  templateUrl: './flights-map-body.component.html',
  styleUrls: ['./flights-map-body.component.scss']
})
export class FlightsMapBodyComponent implements OnInit, AfterViewInit  {

  private currentFilterState: IFlightsMapFilterState;

  private map!: L.Map

  /** код города ИЛИ код аэропорта → маркер */
  private markerIndex  = new Map<string, L.Marker>();

  private airportToCityCode = new Map<string, string>();

  private highlighted: Set<L.Marker> = new Set<L.Marker>();
  private highlightedLayer: L.LayerGroup<L.Marker>;

  private zoomLayers: Record<CountryType, Record<number, L.LayerGroup>> = { ru: {}, other: {} };

  private departurePopup?: L.Popup;
  private routePopup?: L.Popup;

  private destinationsSub?: Subscription;
  private destinationsLayer: L.LayerGroup;
  private destinations: IDestinationsResponse = { data : { routes: []} };

  private skipNextFetchOnce = false; //флаг для пропуска повторного запроса при синхронизации UI-фильтра

  private destroy$ = new Subject<void>()

  isLoading = true;
  isNoDirections: boolean;

  constructor(
    private dictService: DictionariesService, 
    private cityCategoryService: CityCategoryService,
    private filterStateService: FlightsMapFiltersStateService,
    private apiService: FlightsMapApiService
  ) { }

  async ngOnInit(): Promise<void> {
  }

  async ngAfterViewInit(): Promise<void> {
    await this.dictService.ready$;

    this.initMap();
    this.initMarkers();
    this.watchRouteChanges(); 
    this.watchDateChanges();
    
    this.isLoading = false;
  }

  private initMap() {
    const baseMapURl = environment.mapApiUrl;

    const southWest: L.LatLngExpression = [ -70, -185 ];
    const northEast: L.LatLngExpression = [  80, 200 ];


    this.map = L.map('map', 
    {
      center: [53, 45], 
      zoom: 5,
      attributionControl: false,
      maxBounds: [ southWest, northEast ],
      maxBoundsViscosity: 1
    });

    [2, 3, 4, 5, 6].forEach(z => {
      this.zoomLayers.ru[z]    = L.layerGroup();
      this.zoomLayers.other[z] = L.layerGroup();
    });
    
    L.tileLayer(baseMapURl, {
        maxZoom: 6,
        minZoom: 3,
    }).addTo(this.map);

     this.destinationsLayer = L.layerGroup().addTo(this.map);
     this.highlightedLayer = L.layerGroup().addTo(this.map);
  }
  
  private initMarkers() {
    this.dictService.citiesAll.forEach(city => {
      
      if (city.location?.lat == null || city.location?.lon == null) {
        return;
      }
    
      const marker = L.marker(
        [city.location.lat, city.location.lon], 
        {
          icon: markerBlueSmall,
          title: city.code,
        }
      )
      .on('click', ()=> this.handleMarkerClick(city.code))
      .bindTooltip(city.name, {
        permanent : true,
        direction : 'top',
        className : 'city-label',
        interactive: true,
      });

      marker.getTooltip()?.on('click', (event: L.LeafletMouseEvent) => {
        L.DomEvent.stop(event);
        this.handleMarkerClick(city.code);
      });

      const countryType: CountryType = city.country_code === 'RU' ? 'ru' : 'other';
      const zMin = this.cityCategoryService.zoomLevel(city.code);

      this.zoomLayers[countryType][zMin].addLayer(marker);

      this.markerIndex.set(city.code, marker);
    });

    this.dictService.airportsAll?.forEach(airport => {
      const cityMarker = this.markerIndex.get(airport.city_code);
      
      if (!cityMarker) return;

      this.markerIndex.set(airport.code, cityMarker); 
      this.airportToCityCode.set(airport.code, airport.city_code);
    });

    this.updateVisibility();
    this.map.on('zoomend', () => this.updateVisibility());

  }

  private getMarkerByAnyCode(code: string): L.Marker | undefined {
    return this.markerIndex.get(code);
  }

  private updateHighlight(state: IFlightsMapFilterState): void {
    
    this.highlighted.forEach(marker =>
    {
      marker.setIcon(markerBlueSmall);

      const code = (marker as any).options?.title as string;
      const zlvl  = this.cityCategoryService.zoomLevel(code);
      const countryType = this.dictService.getCityByCode(code).country_code === 'RU' ? 'ru': 'other';

      this.moveBetweenLayer(marker, this.highlightedLayer, this.zoomLayers[countryType][zlvl]);   
    });

    this.highlighted.clear();
    this.highlightedLayer.clearLayers();

    const codes = [state.departure, state.arrival].filter(Boolean) as string[];

    codes.forEach(code => {
      const marker = this.getMarkerByAnyCode(code);
      
      if (!marker) { return; }

      marker.setIcon(markerOrange);
      this.highlighted.add(marker);
      
      const zLvl = this.cityCategoryService.zoomLevel(code);
      const countryType = this.dictService.getCityByCode(code).country_code === 'RU' ? 'ru': 'other';
      this.moveBetweenLayer(marker, this.zoomLayers[countryType][zLvl], this.highlightedLayer);
    });

    this.updateVisibility();
  }

  private updateVisibility(): void 
  {
    this.updateMarkers();
    this.updateHighlightedTooltips();

    // if(this.currentFilterState){
    //   this.fetchAndDraw(this.currentFilterState);
    // }
    
  }

  private updateMarkers() {
    const z = this.map.getZoom();
  
    (['ru', 'other'] as const).forEach(countryType => {


      Object.entries(this.zoomLayers[countryType]).forEach(([lvl, layer]) => 
      {

        const layerShouldBeVisible = +lvl <= z;

        if(countryType === 'ru')
        {
          
          if(!this.currentFilterState?.international && layerShouldBeVisible)
          {
            this.map.addLayer(layer);
          }
          else {
            this.map.removeLayer(layer);
          }
        }
        else
        {
          if(!this.currentFilterState?.domestic && layerShouldBeVisible)
          {
            this.map.addLayer(layer);
          }
          else {
            this.map.removeLayer(layer);
          }
        }



      });

    });

  }

  private updateHighlightedTooltips() {
    const z = this.map.getZoom();

    if(z <= 3)
    {
      this.markerIndex.forEach((m) => {

        if(this.highlighted.has(m))
        {
          return;
        }
        m.closeTooltip();
      });
    }
    else if(this.highlighted.size >= 2)
    {
      this.markerIndex.forEach((m) => {

        if(this.highlighted.has(m))
        {
          return;
        }
        m.closeTooltip();
      });
    }
    else
    {
      this.markerIndex.forEach((m) => {
        m.openTooltip();
      });
    }
  }

  private updateIntermediateTooltip() {
    const routes = this.destinations?.data.routes;
    
    if(!routes) {return;}

    for (let i = 0; i < routes.length; i++) 
    {
      const route = routes[i].route;
      
      if(route.length <= 2){ continue; }

      for (let cityIndex = 1; cityIndex < route.length - 1; cityIndex++) 
      {
        const intermediateCityCode = route[cityIndex];
        
        const intermediateCityMarker = this.getMarkerByAnyCode(intermediateCityCode);

        if(!intermediateCityMarker) {continue;}

        intermediateCityMarker.openTooltip();
      }
    }
  }

  private watchRouteChanges() {
    this.filterStateService.state$
    .pipe(
      distinctUntilChanged(
        (a, b) => a.departure === b.departure && 
                  a.arrival === b.arrival &&
                  a.connections === b.connections &&
                  a.domestic === b.domestic &&
                  a.international === b.international
      ),
      takeUntil(this.destroy$),
      tap(_=> this.isLoading = true)
    )
    .subscribe(state => {
        this.currentFilterState = state;
        this.updateHighlight(state);

        this.hideNoDirections();
        
        if (this.skipNextFetchOnce) {
          this.skipNextFetchOnce = false; // пропускаем ровно один раз
          return;
        }

        this.fetchAndDraw(state);
      }
    );
  }

  private watchDateChanges() {
    this.filterStateService.state$
    .pipe(
      distinctUntilChanged(
        (a, b) => a.date === b.date
      ),
      takeUntil(this.destroy$)
    )
    .subscribe(state => {
        this.currentFilterState = state;
        
        if(this.destinations?.data?.routes?.length > 0)
        {
          this.showRoutePopup(this.destinations.data.routes);
        }
      }
    );
  }

  private handleMarkerClick(cityCode: string) : void
  {
    if(!this.currentFilterState.departure)
    {
      this.filterStateService.setDeparture(cityCode);
    }
    else if(this.currentFilterState.departure && !this.currentFilterState.arrival)
    {
      if (cityCode !== this.currentFilterState.departure) 
      {
        this.filterStateService.setArrival(cityCode);
      }
    }
    else
    {
      this.filterStateService.setDeparture(cityCode);
      this.filterStateService.setArrival(undefined);
    }

  }

  
  private fetchAndDraw(state: IFlightsMapFilterState)
  {

    this.clearRoutes();
    this.clearPopup();

    const dateFrom = new Date();
    dateFrom.setDate(dateFrom.getDate() - 1);
    dateFrom.setHours(0, 0, 0, 0);

    const dateTo = new Date();
    dateTo.setMonth(dateFrom.getMonth() + 6);
    dateTo.setHours(0, 0, 0, 0);
    
    
    if((state.departure && state.arrival) && (state.departure != state.arrival))
    {
      this.fetchAndDrawRoute(state.departure, state.arrival, dateFrom, dateTo, state.connections? 1: 0);
    }
    else if(state.departure && !state.arrival)
    {
      this.fetchAndDrawSpider(state.departure, dateFrom, dateTo);
    }else
      {
      this.isLoading = false;
    }
    
  }

  private fetchAndDrawRoute(departure: string, arrival: string, dateFrom: Date, dateTo: Date, connections: number) {
    
    const base: IDestinationsRequestParams = {
      departure: departure,
      arrival: arrival,
      dateFrom: dateFrom,
      dateTo: dateTo,
      connections: connections
    };

    const withConn1: IDestinationsRequestParams = { ...base, connections: 1 };

    this.destinationsSub = this.apiService.getDestinations(base).pipe(
      switchMap(first => {
        const hasRoutes = !!first?.data?.routes?.length;
        
        if (hasRoutes || base.connections === 1) {
          return of({ res: first, usedConn: base.connections ?? 0 });
        }
        // второй заход с пересадками
        return this.apiService.getDestinations(withConn1).pipe(
          map((second: IDestinationsResponse) => {

            const hasSecond = !!second?.data?.routes?.length;
            
            return {
              res: hasSecond ? second : first,
              usedConn: hasSecond ? 1 : 0
            };
          })
        );
    }),
      tap(({ usedConn }) => {
        // если фолбэк (показывать с пересадкой) дал маршруты и в UI ещё выключены пересадки — включим их
        
        this.isLoading = true;

        if (usedConn === 1 && !this.currentFilterState.connections) {
          this.skipNextFetchOnce = true; // не дёргать повторно fetch
          this.filterStateService.setConnections(true); // обновим UI-состояние
        }
      }),
      map(res => this.filterRoutes(res.res)),
      catchError(() => of<IDestinationsResponse>({ data: { routes: [] } })),
      takeUntil(this.destroy$),
      finalize(() => (this.isLoading = false))
    )
    .subscribe(destinations => {
      this.destinations = destinations;
      this.buildRoute(destinations);
      this.updateIntermediateTooltip();
    });
  }


  private fetchAndDrawSpider(cityFromCode: string, dateFrom: Date, dateTo: Date)
  {
    const params: IDestinationsRequestParams = {
      departure : cityFromCode,
      dateFrom: dateFrom,
      dateTo: dateTo
    };

    this.currentFilterState.connections = false;
    this.destinationsSub = this.getDestinations(params)
      .pipe(
        finalize(() => (this.isLoading = false))
      )
      .subscribe(destinations => 
        this.buildSpider(destinations)
      );
  }

  private getDestinations(params: IDestinationsRequestParams) {
    return this.apiService.getDestinations(params).pipe(
        catchError(() => of<IDestinationsResponse>({
          data: { routes: [] }
        }))
      );
  }

  private filterRoutes(res: IDestinationsResponse): IDestinationsResponse {
    const routes = res?.data?.routes ?? [];
    const { domestic, international, connections } = this.currentFilterState;

    const isDomestic = (r: { route: string[] }) =>
      r.route.every(code => this.dictService.ruCitiesCodes.has(this.dictService.getCityCodeByAirportCode(code)));

    const isInternational = (r: { route: string[] }) =>
      r.route.some(code => this.dictService.otherCitiesCodes.has(this.dictService.getCityCodeByAirportCode(code)));

    const hasConnections = (r: { isDirect: boolean }) => !r.isDirect;

    const predicates: Array<(r: any) => boolean> = [];

    if (domestic && !international) {
      predicates.push(isDomestic);
    } else if (international && !domestic) {
      predicates.push(isInternational);
    }

    if (connections) {
      predicates.push(hasConnections);
    }

    const filtered = predicates.length
      ? routes.filter(r => predicates.every(p => p(r)))
      : routes;

    return {
      ...res,
      data: {
        ...res.data,
        routes: filtered
      }
    };
  }

  private buildRoute(res: IDestinationsResponse): void
  {
    const routes = res.data?.routes ?? [];

    if(routes.length == 0)
    {
      this.isNoDirections = true;
    }

    if (!routes.length) { return; }   

    let line : L.Polyline;

    routes.filter(_ => _.isDirect).forEach(path => {
        this.drawPolyline(path.route, directRoutePolyLine, this.destinationsLayer);
    });
    
    routes.filter(_ => !_.isDirect).forEach(path => {
        this.drawPolyline(path.route, dashRoutePolyLine, this.destinationsLayer);
    });


    this.showRoutePopup(routes);
  }

  private buildSpider(res: IDestinationsResponse): void
  {
    const routes = res.data?.routes ?? [];
    if (!routes.length) { return; }

    const fromCode = routes[0].route[0];
    const destCodes = new Set<string>();

    routes.forEach(path => {
      if (Array.isArray(path.route) && path.route.length > 1) 
      {
        const dest = path.route[path.route.length - 1];
        if (dest !== fromCode) { destCodes.add(dest); }
      }
    });

    destCodes.forEach(code => {
      this.drawPolyline([fromCode, code], directRoutePolyLine, this.destinationsLayer);
    });

  }

  private drawPolyline(cities: string[], style: L.PolylineOptions, target: L.LayerGroup | L.Map = this.map): L.Polyline | undefined 
  {
    const segments: L.LatLng[] = [];

    const visibleCities = cities.filter(code => {
      const m = this.getMarkerByAnyCode(code);
      return m && this.map.hasLayer(m);
    });

    if (visibleCities.length < 2) { return; }

    for (let i = 0; i < visibleCities.length - 1; i++) {
      const from = this.getMarkerByAnyCode(visibleCities[i])!;
      const to   = this.getMarkerByAnyCode(visibleCities[i + 1])!;

      const arc = this.buildGreatCircle(from.getLatLng(), to.getLatLng());
      segments.push(...(i === 0 ? arc : arc.slice(1)));
    }

    return L.polyline(segments, style).addTo(target);

  }

  private clearRoutes()
  {
    this.destinationsSub?.unsubscribe();
    this.destinationsLayer?.clearLayers();
    this.destinations =  {data: {routes:[]}};
  }

  private clearPopup(){
    if (this.routePopup) {
      this.map.removeLayer(this.routePopup);
      this.routePopup = undefined;
    }

    if(this.departurePopup){
      this.map.removeLayer(this.departurePopup);
      this.departurePopup = undefined;
    }
  }

  private showRoutePopup(rotues: IDestinationResponse[]): void {

    const firstRoute = rotues[0].route;
    const departureCode = firstRoute[0];
    const arrivalCode = firstRoute[firstRoute.length -1];

    const markerDeparture = this.getMarkerByAnyCode(departureCode);
    const markerArrival = this.getMarkerByAnyCode(arrivalCode);
    if (!markerArrival) { return; }

    
    const cityDeparture = this.dictService.getCityByCode(this.airportToCityCode.get(departureCode));
    const cityArrival = this.dictService.getCityByCode(this.airportToCityCode.get(arrivalCode));

    if(!cityArrival) { return; }


    this.clearPopup();

    const linkUrl = this.getLink();

    //ToDo: translate
    const buyTicketText = 'Купить билет';

    const htmlDeparture = `
      <div class="popup-header-test">
        <span>${cityDeparture.name}</span>
      </div>
    `;

    const htmlInermediate = `
      <div class="popup-header-test">
        <span>${cityDeparture.name}</span>
      </div>
    `;

    const htmlArrival = `
      <div class="popup-header-test">
        <span>${cityArrival.name}</span>

      </div>
      <div style="text-align:center;">
        <a href="${linkUrl}" target="_blank" class="popup-buy-ticket">
          ${buyTicketText}
        </a>
      </div>
    `;

    this.departurePopup = L.popup({
      closeButton: true, 
      autoClose: false,
      closeOnClick: false
    })
    .setLatLng(markerDeparture.getLatLng())
    .setContent(htmlDeparture)
    .openOn(this.map);
    

    this.routePopup = L.popup({
      closeButton: true, 
      autoClose: false,
      closeOnClick: false
    })
    .setLatLng(markerArrival.getLatLng())
    .setContent(htmlArrival)
    .openOn(this.map);
  }

  private moveBetweenLayer(marker: L.Marker, from: L.LayerGroup, to: L.LayerGroup)
  {
    if (from?.hasLayer(marker)) {
      from.removeLayer(marker);
    }

    if (!to?.hasLayer(marker)) {
      to.addLayer(marker);
    }
  }

  private buildGreatCircle(from: L.LatLng, to: L.LatLng, segments = 64): L.LatLng[]
  {
    const φ1 = this.deg2rad(from.lat);
    const λ1 = this.deg2rad(from.lng);
    const φ2 = this.deg2rad(to.lat);
    const λ2 = this.deg2rad(to.lng);

    const Δ = 2 * Math.asin(
      Math.sqrt(
        Math.sin((φ2 - φ1) / 2) ** 2 +
        Math.cos(φ1) * Math.cos(φ2) *
        Math.sin((λ2 - λ1) / 2) ** 2
      )
    );

    if (Δ === 0) { return [from, to]; }   // совпадают

    const points: L.LatLng[] = [];

    for (let i = 0; i <= segments; i++) 
    {
      const f  = i / segments;
      const A  = Math.sin((1 - f) * Δ) / Math.sin(Δ);
      const B  = Math.sin(f * Δ)       / Math.sin(Δ);

      const x =
        A * Math.cos(φ1) * Math.cos(λ1) +
        B * Math.cos(φ2) * Math.cos(λ2);

      const y =
        A * Math.cos(φ1) * Math.sin(λ1) +
        B * Math.cos(φ2) * Math.sin(λ2);

      const z =
        A * Math.sin(φ1) +
        B * Math.sin(φ2);

      const φi = Math.atan2(z, Math.sqrt(x * x + y * y));
      const λi = Math.atan2(y, x);

      points.push(L.latLng(this.rad2deg(φi), this.rad2deg(λi)));
    }

    return points;
  }

  private deg2rad(deg: number): number { return (deg * Math.PI) / 180; }
  private rad2deg(rad: number): number { return (rad * 180) / Math.PI; }

  private getLink(): string {
    const state = this.currentFilterState;

    const d = new Date();
    d.setHours(0,0,0,0);

    const date = moment(state.date ?? d).format('YYYYMMDD');
    const params = `${state.departure}.${date}.${state.arrival}`;

    return `https://www.aeroflot.ru/sb/app/ru-ru#/search?adults=1&cabin=economy&children=0&infants=0&routes=${params}&autosearch=Y&utm_source=aflwebbot&utm_medium=referral&utm_campaign=ref_3015_general_rf_button.index__all_flight.map`;
  }

  hideNoDirections(): void {
    this.isNoDirections = false;
  }
}
