/**
 * Flights Map start page -- container component.
 *
 * Manages filter state, drives the map and calendar hooks, and renders
 * the filter panel + map canvas + loading/empty overlays.
 *
 * Uses PageLayout for the two-column layout matching the Angular version.
 *
 * @module
 */

import { type FC, lazy, Suspense, useState, useEffect, useCallback, useMemo, useRef } from "react";
import { useLocale } from "@/i18n/useLocale.js";
import { useTranslation } from "@/i18n/provider.js";
import { PageLayout } from "@/ui/layout/PageLayout.js";
import { PageTabs } from "@/ui/layout/PageTabs.js";
import { ClientOnly } from "./ClientOnly.js";
import { FlightsMapFilter } from "./FlightsMapFilter.js";
import { useFlightsMapSearch } from "../hooks/useFlightsMapSearch.js";
import { useFlightsMapCalendar } from "../hooks/useFlightsMapCalendar.js";
import { useGeoCityDefault } from "@/shared/hooks/useGeoCityDefault.js";
import { routesToPolylines, intermediateCityIds } from "../routesToPolylines.js";
import { filterRoutes } from "../filterRoutes.js";
import { buildBuyTicketUrl, escapeHtml } from "../buyTicketUrl.js";
import { useDictionaries, getCityCodeByAirportCode } from "@/shared/dictionaries/index.js";
import { getCityZoomLevel } from "../cityCategory.js";
import {
  getMapFilter,
  setMapFilter,
} from "@/shared/state/crossSectionNavigation.js";
import type {
  IFlightsMapFilterState,
  FlightsMapSearchParams,
  FlightsMapCalendarParams,
  IMapMarker,
  IMapPolyline,
  IMapPopup,
  IFlightRoute,
} from "../types.js";
import { todayYyyymmdd } from "@/shared/utils/datetime/index.js";
import "./FlightsMapStartPage.scss";

const MapCanvas = lazy(() =>
  import("./MapCanvas.js").then((m) => ({ default: m.MapCanvas })),
);

// ---------------------------------------------------------------------------
// Date helpers
// ---------------------------------------------------------------------------

// todayYyyymmdd lives in @/shared/utils/datetime — imported below — so the
// SSR loader and the client component agree on a single value carried via
// _ROUTER_DATA. The local helper here only handles month arithmetic.

function addMonthsYyyymmdd(base: string, months: number): string {
  const y = Number(base.slice(0, 4));
  const m = Number(base.slice(4, 6)) - 1;
  const d = Number(base.slice(6, 8));
  const date = new Date(y, m + months, d);
  const ry = date.getFullYear().toString();
  const rm = (date.getMonth() + 1).toString().padStart(2, "0");
  const rd = date.getDate().toString().padStart(2, "0");
  return `${ry}${rm}${rd}`;
}

function addDaysYyyymmdd(base: string, days: number): string {
  const y = Number(base.slice(0, 4));
  const m = Number(base.slice(4, 6)) - 1;
  const d = Number(base.slice(6, 8));
  const date = new Date(y, m, d);
  date.setDate(date.getDate() + days);
  const ry = date.getFullYear().toString();
  const rm = (date.getMonth() + 1).toString().padStart(2, "0");
  const rd = date.getDate().toString().padStart(2, "0");
  return `${ry}${rm}${rd}`;
}

// ---------------------------------------------------------------------------
// Component
// ---------------------------------------------------------------------------

export interface FlightsMapStartPageProps {
  /**
   * Leaflet tile URL template (MAP_TILE_URL env). Defaults to same-origin
   * so tests and stand-alone renders keep working without wiring env. In
   * production the wrapping route page resolves it from env and passes it
   * in; absolute URLs let deployments without a /map/api/** ingress rule
   * fetch tiles from an upstream tile service (e.g. flights.test.aeroflot.ru).
   */
  tileUrl?: string;
  /**
   * yyyyMMdd of "today" computed once on the SSR server (route loader)
   * and passed in. See OnlineBoardFilterProps.today — same purpose:
   * eliminate `new Date()` calls during render that diverge between SSR
   * and CSR and trigger React hydration error #423.
   */
  today?: string;
}

export const FlightsMapStartPage: FC<FlightsMapStartPageProps> = ({
  tileUrl: tileUrlProp,
  today,
}) => {
  // Capture a single fallback today for unit tests that don't wire the
  // route loader. Production always supplies the prop; the ref is unused
  // there but always called to keep hook order stable.
  const fallbackTodayYmd = useRef(todayYyyymmdd()).current;
  const todayYmd = today ?? fallbackTodayYmd;
  const searchDateFromYmd = useMemo(
    () => addDaysYyyymmdd(todayYmd, -1),
    [todayYmd],
  );
  const { t } = useTranslation();
  const { language } = useLocale();

  const {
    dictionaries,
    loading: dictionariesLoading,
    error: dictionariesError,
  } = useDictionaries(language);

  const [filterState, setFilterState] = useState<IFlightsMapFilterState>(() => {
    // TZ §4.1.8 / 4.1.1-R26: Map filter is stored independently and NEVER
    // projected from/to Board or Schedule.
    const mapSnap = getMapFilter();
    if (mapSnap) {
      return {
        departure: mapSnap.departure ?? undefined,
        arrival: mapSnap.arrival ?? undefined,
        date: mapSnap.date ?? undefined,
        connections: mapSnap.showTransfers,
        domestic: mapSnap.showInternal,
        international: mapSnap.showInternational,
      };
    }
    return {
      connections: false,
      domestic: false,
      international: false,
    };
  });

  useGeoCityDefault({
    dictionaries,
    shouldApply: () => !filterState.departure && !filterState.arrival,
    onCity: (cityCode) =>
      setFilterState((prev) =>
        prev.departure || prev.arrival
          ? prev
          : {
              ...prev,
              departure: cityCode,
              // TZ §4.1.1 ¶4: with geo consent, enable domestic + international
              domestic: true,
              international: true,
              // connections stays OFF per R14
            },
      ),
  });

  const [effectiveConnections, setEffectiveConnections] = useState<0 | 1>(
    filterState.connections ? 1 : 0,
  );

  const persistFilterState = useCallback((newState: IFlightsMapFilterState) => {
    setFilterState(newState);
    // TZ §4.1.8 / 4.1.1-R26: persist map filter independently from Board/Schedule.
    setMapFilter({
      departure: newState.departure ?? null,
      arrival: newState.arrival ?? null,
      date: newState.date ?? null,
      showInternal: newState.domestic,
      showInternational: newState.international,
      showTransfers: newState.connections,
    });
  }, []);

  // Sync user toggle → effective.
  useEffect(() => {
    setEffectiveConnections(filterState.connections ? 1 : 0);
  }, [filterState.connections]);

  // Build search params from filter state
  const searchParams = useMemo<FlightsMapSearchParams | null>(() => {
    if (!filterState.departure) return null;

    return {
      departure: filterState.departure,
      arrival: filterState.arrival,
      // Angular starts the flights-map destinations window at minDateCalendar
      // (today - 1 day). Keeping this aligned preserves Moscow domestic
      // airport-internal directions such as DME-SVO and ZIA-SVO.
      dateFrom: searchDateFromYmd,
      dateTo: addMonthsYyyymmdd(searchDateFromYmd, 6),
      connections: filterState.arrival ? effectiveConnections : 0,
    };
  }, [
    filterState.departure,
    filterState.arrival,
    effectiveConnections,
    searchDateFromYmd,
  ]);

  // Build calendar params
  const calendarParams = useMemo<FlightsMapCalendarParams | null>(() => {
    if (!filterState.departure) return null;

    return {
      date: searchDateFromYmd,
      departure: filterState.departure,
      arrival: filterState.arrival,
      connections: filterState.arrival ? filterState.connections : false,
    };
  }, [
    filterState.departure,
    filterState.arrival,
    filterState.connections,
    searchDateFromYmd,
  ]);

  const { routes, loading, error } = useFlightsMapSearch(searchParams);
  const { availableDays } = useFlightsMapCalendar(calendarParams);

  // Auto-fallback: empty result, route mode, connections=0 → retry with 1.
  useEffect(() => {
    if (loading || error) return;
    if (effectiveConnections !== 0) return;
    if (!filterState.departure || !filterState.arrival) return;
    if (routes.length > 0) return;
    setEffectiveConnections(1);
  }, [
    loading,
    error,
    effectiveConnections,
    filterState.departure,
    filterState.arrival,
    routes,
  ]);

  // Reflect fallback in the UI toggle once.
  useEffect(() => {
    if (filterState.arrival && effectiveConnections === 1 && !filterState.connections) {
      setFilterState((prev) => ({ ...prev, connections: true }));
    }
  }, [effectiveConnections, filterState.arrival, filterState.connections]);

  const handleFilterChange = useCallback((newState: IFlightsMapFilterState) => {
    persistFilterState(newState);
  }, [persistFilterState]);

  const handleMarkerClick = useCallback(
    (markerId: string) => {
      if (!filterState.departure) {
        persistFilterState({ ...filterState, departure: markerId });
      } else if (!filterState.arrival && markerId !== filterState.departure) {
        persistFilterState({ ...filterState, arrival: markerId });
      } else {
        persistFilterState({
          ...filterState,
          departure: markerId,
          arrival: undefined,
          // Angular fetchAndDrawSpider forces connections=false before drawing.
          // Keep the disabled UI state and spider-mode route rendering aligned.
          connections: false,
        });
      }
    },
    [filterState, persistFilterState],
  );

  const markers = useMemo<IMapMarker[]>(() => {
    if (!dictionaries) return [];

    return dictionaries.cities
      .filter(
        (c) =>
          typeof c.location?.lat === "number" &&
          typeof c.location?.lon === "number" &&
          Number.isFinite(c.location.lat) &&
          Number.isFinite(c.location.lon),
      )
      .map((city) => {
        const isHighlighted =
          city.code === filterState.departure ||
          city.code === filterState.arrival;
        return {
          id: city.code,
          lat: city.location.lat,
          lng: city.location.lon,
          // Permanent on-map label shows the localized city name (dictionary
          // is loaded per current locale). TZ §4.1.24.3 R24 describes the
          // separate hover tooltip as showing the IATA code; that's not the
          // same element as this always-visible marker label.
          label: city.name,
          tooltipPermanent: true,
          style: isHighlighted ? "orange" : "blue-small",
          zoomLevel: getCityZoomLevel(city.code),
          countryType: city.country_code === "RU" ? "ru" : "other",
          highlighted: isHighlighted,
        };
      });
  }, [dictionaries, filterState.departure, filterState.arrival]);

  const filteredRoutes = useMemo<IFlightRoute[]>(
    () => (dictionaries ? filterRoutes(routes, filterState, dictionaries) : []),
    [
      routes,
      filterState.domestic,
      filterState.international,
      filterState.connections,
      dictionaries,
    ],
  );

  const polylines = useMemo<IMapPolyline[]>(
    () =>
      dictionaries
        ? routesToPolylines(filteredRoutes, filterState, dictionaries)
        : [],
    [filteredRoutes, filterState.departure, filterState.arrival, dictionaries],
  );

  const intermediateIds = useMemo<string[]>(
    () => (dictionaries ? intermediateCityIds(filteredRoutes, dictionaries) : []),
    [filteredRoutes, dictionaries],
  );

  const popups = useMemo<IMapPopup[]>(() => {
    if (!dictionaries) return [];
    if (!filterState.departure || !filterState.arrival) return [];
    if (filteredRoutes.length === 0) return [];

    const firstRoute = filteredRoutes[0]?.route;
    const depCode = firstRoute?.[0];
    const arrCode = firstRoute?.[firstRoute.length - 1];
    if (!firstRoute || !depCode || !arrCode) return [];

    const depCityCode = getCityCodeByAirportCode(dictionaries, depCode) ?? depCode;
    const arrCityCode = getCityCodeByAirportCode(dictionaries, arrCode) ?? arrCode;

    const depCity = dictionaries.cityByCode.get(depCityCode);
    const arrCity = dictionaries.cityByCode.get(arrCityCode);
    if (!depCity || !arrCity) return [];

    // TZ §4.1.24.6: When no date is set, SB transition must be performed
    // without a date. Pass filterState.date directly (may be undefined).
    const buyUrl = buildBuyTicketUrl(depCityCode, arrCityCode, filterState.date);

    const depHtml = `
    <div class="popup-header-test"><span>${escapeHtml(depCity.name)}</span></div>
  `;

    const arrHtml = `
    <div class="popup-header-test"><span>${escapeHtml(arrCity.name)}</span></div>
    <div style="text-align:center;">
      <a href="${buyUrl}" target="_blank" class="popup-buy-ticket">
        ${t("FLIGHTS-MAP.BUY_TICKET_BTN")}
      </a>
    </div>
  `;

    return [
      { lat: depCity.location.lat, lng: depCity.location.lon, content: depHtml },
      { lat: arrCity.location.lat, lng: arrCity.location.lon, content: arrHtml },
    ];
  }, [
    dictionaries,
    filterState.departure,
    filterState.arrival,
    filterState.date,
    filteredRoutes,
    t,
  ]);

  // Tile URL comes from the route page (reads MAP_TILE_URL env on the
  // server). Fallback matches Angular's environment.ts mapApiUrl so
  // stand-alone renders + tests keep working without threading env.
  const tileUrl = tileUrlProp ?? "/map/api/tile/{z}/{x}/{y}.jpeg";

  return (
    <div className="flights-map-start-page" data-testid="flights-map-start">
      <PageLayout
        headerLeft={
          <PageTabs viewType="flights-map" />
        }
        title={
          <h1 className="text--white page-title">
            {t("FLIGHTS-MAP.TITLE")}
          </h1>
        }
        breadcrumbs={[]}
        contentLeft={
          <FlightsMapFilter
            value={filterState}
            availableDays={availableDays}
            today={todayYmd}
            onChange={handleFilterChange}
          />
        }
      >
        <section className="frame">
          <div className="flights-map-start__map-wrapper">
            <ClientOnly
              fallback={
                <div className="flights-map-start__spinner" aria-busy="true" data-testid="map-loading">
                  <div className="page-loader__loader">
                    <div className="loader-circle" />
                    <div className="loader-line-mask">
                      <div className="loader-line" />
                    </div>
                  </div>
                </div>
              }
            >
              <Suspense
                fallback={
                  <div className="flights-map-start__spinner" aria-busy="true" data-testid="map-loading">
                    <div className="page-loader__loader">
                      <div className="loader-circle" />
                      <div className="loader-line-mask">
                        <div className="loader-line" />
                      </div>
                    </div>
                  </div>
                }
              >
                <MapCanvas
                  markers={markers}
                  polylines={polylines}
                  popups={popups}
                  tileUrl={tileUrl}
                  onMarkerClick={handleMarkerClick}
                  className="flights-map-start__map"
                  domestic={filterState.domestic}
                  international={filterState.international}
                  intermediateIds={intermediateIds}
                />
              </Suspense>
            </ClientOnly>

            {(loading || dictionariesLoading) && (
              // Mirrors Angular `loader-sheet` — translucent overlay with
              // a 60×60 ring loader centered on a plane glyph.
              <div
                className="loading-sheet"
                aria-busy="true"
                data-testid="map-loader"
              >
                <div className="page-loader__loader">
                  <div className="loader-circle" />
                  <div className="loader-line-mask">
                    <div className="loader-line" />
                  </div>
                  <svg
                    xmlns="http://www.w3.org/2000/svg"
                    width="20"
                    height="20"
                    viewBox="0 0 33.795 32.307"
                    aria-hidden="true"
                  >
                    <path
                      id="plane_afl_small"
                      d="M20.824,12.956c0,.388,0,.823,0,1.29L35.729,26.527l.007,1.633-15.1-7.255a70.654,70.654,0,0,1-.947,9.23l6.09,3.575.011.462-6.457-2.2c-.139.5-.276.788-.426.785a.561.561,0,0,1-.437-.774,35.562,35.562,0,0,0-6.415,2.288l-.006-.469,6.037-3.658a71.009,71.009,0,0,1-1.149-9.227L1.96,28.408l-.02-1.627,14.708-12.5c-.011-.469-.015-.9-.019-1.291-.048-6.082.858-11.015,2.014-11.021s2.138,4.91,2.18,10.99"
                      transform="translate(36,-2) rotate(90)"
                      fill="currentColor"
                    />
                  </svg>
                </div>
              </div>
            )}

            {!loading && !dictionariesLoading && (error || dictionariesError) && (
              <div
                className="flights-map-start__error"
                role="alert"
                data-testid="map-error"
              >
                {t("BOARD.LOAD-FAILED")}
              </div>
            )}

            {!loading && !dictionariesLoading && !error && !dictionariesError && searchParams !== null && routes.length === 0 && (
              // Mirrors Angular `no-directions-sheet`: a translucent grey
              // overlay above the map with a white centered info card.
              <div
                className="no-directions-sheet"
                data-testid="map-no-directions"
                role="alert"
              >
                <div className="no-directions-card">
                  <p>{t("FLIGHTS-MAP.NO_DIRECTIONS_INFO")}</p>
                </div>
              </div>
            )}
          </div>
        </section>
      </PageLayout>
    </div>
  );
};
