/**
 * Shared search results page for all 4 online board search types.
 *
 * Each route page (flight, departure, arrival, route) composes this
 * component with its parsed params. This component handles:
 * - Converting parsed URL params to API search params
 * - Wiring useOnlineBoard for data fetching
 * - Wiring useLiveBoardSearch for live SignalR updates
 * - Rendering FlightList with results
 * - Navigation to flight details on card click
 *
 * @module
 */

import type { FC } from "react";
import { useCallback, useEffect } from "react";
import { ApiHttpError, ApiTimeoutError } from "@/shared/api/errors.js";
import { useNavigate } from "@modern-js/runtime/router";
import { useLocale } from "@/i18n/useLocale.js";
import { useTranslation } from "@/i18n/provider.js";
import { FlightList } from "@/ui/flights/FlightList.js";
import { FlightActions } from "./BoardDetailsHeader/FlightActions.js";
import { findClosestFlightId } from "../closestFlight.js";
import { PageLayout } from "@/ui/layout/PageLayout.js";
import { PageTabs } from "@/ui/layout/PageTabs.js";
import { SearchHistory } from "@/ui/layout/SearchHistory.js";
import { OnlineBoardFilter } from "./OnlineBoardFilter.js";
import { DayTabs } from "./DayTabs/index.js";
import { useDictionaries } from "@/shared/dictionaries/index.js";
import { useSearchHistory } from "@/shared/hooks/useSearchHistory.js";
import "./OnlineBoardSearchPage.scss";
import { JsonLdRenderer } from "@/shared/seo/json-ld.js";
import { useOnlineBoard } from "../hooks/useOnlineBoard.js";
import { useLiveBoardSearch } from "../hooks/useLiveBoardSearch.js";
import { useCalendarDays } from "../hooks/useCalendarDays.js";
import { buildOnlineBoardUrl } from "../url.js";
import { buildFlightListJsonLd } from "../json-ld.js";
import { sortFlights } from "../sortFlights.js";
import { getFlightSearchDate } from "../flightSearchDate.js";
import {
  PobedaAuroraBanner,
  shouldShowPobedaAuroraBanner,
} from "./PobedaAuroraBanner.js";
import type { SortMode } from "../sortFlights.js";
import type { OnlineBoardParams } from "../url.js";
import type { SearchFlightsParams, CalendarParams } from "../api.js";
import type { FlightRequestType, ISimpleFlight } from "../types.js";
import { boardWindowBounds } from "@/shared/dateWindow.js";

export interface OnlineBoardSearchPageProps {
  /** Parsed and validated URL params from the route */
  params: OnlineBoardParams & { type: "flight" | "departure" | "arrival" | "route" };
}

/**
 * Convert yyyyMMdd URL date to API format (yyyy-MM-ddT00:00:00).
 */
function formatDateForApi(yyyymmdd: string): string {
  if (yyyymmdd.length === 8) {
    const y = yyyymmdd.slice(0, 4);
    const m = yyyymmdd.slice(4, 6);
    const d = yyyymmdd.slice(6, 8);
    return `${y}-${m}-${d}T00:00:00`;
  }
  // Already in ISO-ish format
  return yyyymmdd.includes("T") ? yyyymmdd : `${yyyymmdd}T00:00:00`;
}

function formatDateOnlyForApi(value: Date): string {
  const y = value.getFullYear().toString();
  const m = (value.getMonth() + 1).toString().padStart(2, "0");
  const d = value.getDate().toString().padStart(2, "0");
  return `${y}-${m}-${d}`;
}

/**
 * Return the yyyyMMdd date one day after `yyyymmdd`. The API treats the
 * board range as a half-open interval: `dateFrom=D, dateTo=D` yields zero
 * rows. Matching Angular's OnlineBoardApiService.getFlightsByRoute.
 */
function addOneDayYyyymmdd(yyyymmdd: string): string {
  const iso = yyyymmdd.includes("T")
    ? yyyymmdd.split("T")[0] ?? yyyymmdd
    : yyyymmdd;
  let year: number, month: number, day: number;
  if (iso.includes("-")) {
    const [y, m, d] = iso.split("-");
    year = Number(y); month = Number(m) - 1; day = Number(d);
  } else {
    year = Number(iso.slice(0, 4));
    month = Number(iso.slice(4, 6)) - 1;
    day = Number(iso.slice(6, 8));
  }
  const dt = new Date(year, month, day);
  dt.setDate(dt.getDate() + 1);
  const y = dt.getFullYear().toString();
  const m = (dt.getMonth() + 1).toString().padStart(2, "0");
  const d = dt.getDate().toString().padStart(2, "0");
  return `${y}${m}${d}`;
}

/**
 * Convert parsed online board URL params into API search params.
 * The API expects dateFrom/dateTo in yyyy-MM-ddT00:00:00 format.
 */
function toSearchParams(
  params: OnlineBoardSearchPageProps["params"],
): SearchFlightsParams {
  const apiDate = formatDateForApi(params.date);
  const apiDateTo = formatDateForApi(addOneDayYyyymmdd(params.date));
  const base: SearchFlightsParams = {
    dateFrom: apiDate,
    dateTo: apiDateTo,
  };

  switch (params.type) {
    case "flight":
      base.flightNumber = `${params.carrier}${params.flightNumber}${params.suffix ?? ""}`;
      break;
    case "departure":
      base.departure = params.station;
      break;
    case "arrival":
      base.arrival = params.station;
      break;
    case "route":
      base.departure = params.departure;
      base.arrival = params.arrival;
      break;
  }

  if ("timeFrom" in params && params.timeFrom) {
    base.timeFrom = params.timeFrom;
  }
  if ("timeTo" in params && params.timeTo) {
    base.timeTo = params.timeTo;
  }

  return base;
}

/**
 * Convert parsed params into calendar API params.
 *
 * TIRREDESIGN-12: the 31-day availability bitmask is anchored to the
 * first board-calendar date (today - 1), matching Angular's
 * `updateCalendar()` (`date = new Date(); date.setDate(date.getDate()-1)`).
 * Using the URL-selected date as the anchor shifts the window as the
 * user navigates and corrupts disabled-day parity.
 */
function toCalendarParams(
  params: OnlineBoardSearchPageProps["params"],
): CalendarParams {
  const [boardMinDate] = boardWindowBounds();
  const base: CalendarParams = {
    date: formatDateOnlyForApi(boardMinDate),
    searchType: params.type as FlightRequestType,
  };

  switch (params.type) {
    case "flight":
      base.flightNumber = `${params.carrier}${params.flightNumber}${params.suffix ?? ""}`;
      break;
    case "departure":
      base.departure = params.station;
      break;
    case "arrival":
      base.arrival = params.station;
      break;
    case "route":
      base.departure = params.departure;
      base.arrival = params.arrival;
      break;
  }

  return base;
}

/**
 * Extract live board channel params from parsed URL params.
 */
function toLiveBoardParams(
  params: OnlineBoardSearchPageProps["params"],
): { date: string; departure?: string; arrival?: string } {
  const result: { date: string; departure?: string; arrival?: string } = {
    date: params.date,
  };

  switch (params.type) {
    case "departure":
      result.departure = params.station;
      break;
    case "arrival":
      result.arrival = params.station;
      break;
    case "route":
      result.departure = params.departure;
      result.arrival = params.arrival;
      break;
  }

  return result;
}

/**
 * Shared search results page composed by all 4 search route pages.
 */
export const OnlineBoardSearchPage: FC<OnlineBoardSearchPageProps> = ({
  params,
}) => {
  const navigate = useNavigate();
  const { t } = useTranslation();
  const { locale, language } = useLocale();
  const { dictionaries } = useDictionaries(language);
  const { add: addHistory } = useSearchHistory(language);

  // Persist this online-board search into the `Ранее искали` sidebar
  // history. The hook dedupes by URL, so re-renders / day-tab clicks
  // don't bloat storage.
  useEffect(() => {
    const url = `/${locale}/${buildOnlineBoardUrl(params)}`;
    const isFlightNumber = params.type === "flight";
    const departure =
      params.type === "route" ? params.departure
        : params.type === "departure" ? params.station
        : undefined;
    const arrival =
      params.type === "route" ? params.arrival
        : params.type === "arrival" ? params.station
        : undefined;
    const flightNumber = isFlightNumber
      ? `${params.carrier} ${params.flightNumber}${params.type === "flight" ? params.suffix ?? "" : ""}`
      : undefined;
    const labelParts: string[] = [];
    if (departure) labelParts.push(departure);
    if (arrival) labelParts.push(arrival);
    if (flightNumber) labelParts.push(flightNumber);
    addHistory({
      type: isFlightNumber ? "flight-number" : "board-route",
      url,
      label: labelParts.join(" — ") || url,
      params: {
        ...(departure ? { departure } : {}),
        ...(arrival ? { arrival } : {}),
        ...(params.date ? { dateFrom: params.date } : {}),
        ...(flightNumber ? { flightNumber } : {}),
      },
    });
  }, [
    locale,
    params.type,
    params.type === "route" ? params.departure : undefined,
    params.type === "route" ? params.arrival : undefined,
    params.type === "departure" || params.type === "arrival"
      ? params.station
      : undefined,
    params.type === "flight" ? params.carrier : undefined,
    params.type === "flight" ? params.flightNumber : undefined,
    params.type === "flight" ? params.suffix : undefined,
    params.date,
  ]);

  // Human-readable title/breadcrumb. Angular prefers the city name when a
  // code resolves to a city (LED → 'Санкт-Петербург'); falls back to the
  // airport name only for codes that aren't city codes (SVO → 'Шереметьево').
  const describeStation = (code?: string): string => {
    if (!code || !dictionaries) return code ?? "";
    const upper = code.toUpperCase();
    const city = dictionaries.cityByCode.get(upper);
    if (city) return city.name;
    const airport = dictionaries.airportByCode.get(upper);
    if (airport) return airport.name;
    return code;
  };
  // Today's date gets rendered as 'Сегодня', matching Angular's heading.
  const dateLabel = ((): string => {
    if (!params.date || params.date.length !== 8) return "";
    const now = new Date();
    const todayYyyymmdd = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, "0")}${String(now.getDate()).padStart(2, "0")}`;
    if (params.date === todayYyyymmdd) return t("SHARED.TODAY");
    // Otherwise format as 'DD.MM.YYYY'.
    return `${params.date.slice(6, 8)}.${params.date.slice(4, 6)}.${params.date.slice(0, 4)}`;
  })();
  let searchHeading: string;
  switch (params.type) {
    case "route":
      searchHeading = `${t("BOARD.ROUTE-TEXT")}${describeStation(params.departure)} - ${describeStation(params.arrival)}`;
      if (dateLabel) searchHeading += `, ${dateLabel}`;
      break;
    case "departure":
      searchHeading = `${t("BOARD.DEPARTURE")}: ${describeStation(params.station)}`;
      if (dateLabel) searchHeading += `, ${dateLabel}`;
      break;
    case "arrival":
      searchHeading = `${t("BOARD.ARRIVAL")}: ${describeStation(params.station)}`;
      if (dateLabel) searchHeading += `, ${dateLabel}`;
      break;
    case "flight":
      searchHeading = `${t("SHARED.NUMBER")}: ${params.carrier} ${params.flightNumber}${params.suffix ?? ""}`;
      if (dateLabel) searchHeading += `, ${dateLabel}`;
      break;
    default:
      searchHeading = t("BOARD.TITLE");
  }

  // Rewrite document.title + meta description once city names resolve,
  // matching Angular's SEO (route/departure/arrival titles use resolved
  // city names and 'Сегодня' for today's date, not raw IATA codes + ISO).
  useEffect(() => {
    if (!dictionaries) return;
    const depCity =
      params.type === "route" ? describeStation(params.departure) : undefined;
    const arrCity =
      params.type === "route" ? describeStation(params.arrival) : undefined;
    const stationCity =
      params.type === "departure" || params.type === "arrival"
        ? describeStation(params.station)
        : undefined;
    const dateForSeo = dateLabel;
    let title: string | null = null;
    let desc: string | null = null;
    if (params.type === "route" && depCity && arrCity) {
      title = t("SEO.BOARD.ROUTE-SEARCH.TITLE", {
        departureCity: depCity,
        arrivalCity: arrCity,
        date: dateForSeo,
      });
      desc = t("SEO.BOARD.ROUTE-SEARCH.DESCRIPTION", {
        departureCity: depCity,
        arrivalCity: arrCity,
        date: dateForSeo,
      });
    } else if (params.type === "departure" && stationCity) {
      title = t("SEO.BOARD.DEPARTURE-SEARCH.TITLE", {
        departureCity: stationCity,
        date: dateForSeo,
      });
      desc = t("SEO.BOARD.DEPARTURE-SEARCH.DESCRIPTION", {
        departureCity: stationCity,
        date: dateForSeo,
      });
    } else if (params.type === "arrival" && stationCity) {
      title = t("SEO.BOARD.ARRIVAL-SEARCH.TITLE", {
        arrivalCity: stationCity,
        date: dateForSeo,
      });
      desc = t("SEO.BOARD.ARRIVAL-SEARCH.DESCRIPTION", {
        arrivalCity: stationCity,
        date: dateForSeo,
      });
    }
    if (title) document.title = title;
    if (desc) {
      const meta = document.querySelector('meta[name="description"]');
      if (meta) meta.setAttribute("content", desc);
    }
  }, [dictionaries, params, dateLabel, t]);

  // Data fetching
  const searchParams = toSearchParams(params);
  const { flights, loading, error, refresh, cancel } = useOnlineBoard(searchParams);

  // §4.1.12 — Escape cancels while loader is showing
  useEffect(() => {
    if (!loading) return;
    const handleKeyDown = (e: KeyboardEvent) => {
      if (e.key === "Escape") cancel();
    };
    document.addEventListener("keydown", handleKeyDown);
    return () => document.removeEventListener("keydown", handleKeyDown);
  }, [loading, cancel]);

  // Live updates via SignalR
  const liveBoardParams = toLiveBoardParams(params);
  const { flights: liveFlights, connectionStatus } = useLiveBoardSearch(
    liveBoardParams,
    flights,
    refresh,
  );

  // Calendar days
  const calendarParams = toCalendarParams(params);
  const { days: calendarDays } = useCalendarDays(calendarParams);

  // Navigation: click a flight to go to details
  const handleFlightClick = useCallback(
    (flight: ISimpleFlight) => {
      const detailsParams: OnlineBoardParams = flight.flightId.suffix
        ? {
            type: "details",
            carrier: flight.flightId.carrier,
            flightNumber: flight.flightId.flightNumber,
            suffix: flight.flightId.suffix,
            date: getFlightSearchDate(flight),
          }
        : {
            type: "details",
            carrier: flight.flightId.carrier,
            flightNumber: flight.flightId.flightNumber,
            date: getFlightSearchDate(flight),
          };
      const detailsUrl = buildOnlineBoardUrl(detailsParams);
      void navigate(`/${locale}/${detailsUrl}`);
    },
    [navigate, locale],
  );

  // Navigation: change date via calendar
  const handleDateChange = useCallback(
    (newDate: string) => {
      const newParams = { ...params, date: newDate };
      const url = buildOnlineBoardUrl(newParams);
      void navigate(`/${locale}/${url}`);
    },
    [navigate, locale, params],
  );

  // Use live flights when connected, otherwise fetched flights
  const rawFlights = connectionStatus === "live" ? liveFlights : flights;

  // §4.1.13.2 — default sort: departure modes by dep time, arrival by arr time.
  // Day ordering (yesterday < today < tomorrow) emerges from absolute timestamps.
  const sortMode: SortMode =
    params.type === "arrival" ? "arrival"
    : params.type === "flight" ? "flight-number"
    : params.type === "route" ? "route"
    : "departure";
  const displayFlights = sortFlights(rawFlights, sortMode);

  // Port of Angular's findClosestFlight — on today's search, picks the
  // flight with the smallest abs time-diff from 'now' (expands + scrolls
  // it on mount). For future/past days, picks the first/last.
  const initialCurrentFlightId = (() => {
    if (!displayFlights.length || !params.date) return null;
    const isArrival = params.type === "arrival";
    return findClosestFlightId(displayFlights, {
      searchDate: params.date,
      isArrival,
    });
  })();

  // JSON-LD for search results (rendered once we have flights)
  const searchDescription = `Online board ${params.type} search results`;
  const jsonLd = displayFlights.length > 0
    ? buildFlightListJsonLd(displayFlights, searchDescription)
    : undefined;

  // §4.1.10.1 — resolve per-status error message key
  const errorMessageKey = (() => {
    if (!error) return null;
    if (error instanceof ApiTimeoutError) return "BOARD.ERROR-TIMEOUT";
    if (error instanceof ApiHttpError) {
      return error.status >= 500 ? "BOARD.ERROR-5XX" : "BOARD.ERROR-4XX";
    }
    return "BOARD.LOAD-FAILED-MESSAGE";
  })();

  return (
    <div
      className="online-board-search"
      data-testid="online-board-search"
      // §4.1.10 — flag so CSS can pointer-events:none filter/tabs/breadcrumbs
      data-searching={loading ? "true" : undefined}
    >
      {jsonLd && <JsonLdRenderer data={jsonLd} />}
      <PageLayout
        headerLeft={<PageTabs viewType="onlineboard" />}
        title={
          <h1 className="text--white page-title">
            {searchHeading}
          </h1>
        }
        breadcrumbs={[
          // Angular stops the crumb trail at 'Онлайн-Табло'; the search
          // heading only lives in the h1 — don't repeat it.
          { label: t("BOARD.TITLE"), url: `/${locale}/onlineboard` },
        ]}
        contentLeft={
          <>
            <OnlineBoardFilter
              {...(params.type === "route"
                ? {
                    initialDeparture: params.departure,
                    initialArrival: params.arrival,
                    initialDate: params.date,
                    ...(params.timeFrom ? { initialTimeFrom: params.timeFrom } : {}),
                    ...(params.timeTo ? { initialTimeTo: params.timeTo } : {}),
                    initialTab: "route" as const,
                  }
                : params.type === "departure"
                  ? {
                      initialDeparture: params.station,
                      initialDate: params.date,
                      ...(params.timeFrom ? { initialTimeFrom: params.timeFrom } : {}),
                      ...(params.timeTo ? { initialTimeTo: params.timeTo } : {}),
                      initialTab: "route" as const,
                    }
                  : params.type === "arrival"
                    ? {
                        initialArrival: params.station,
                        initialDate: params.date,
                        ...(params.timeFrom ? { initialTimeFrom: params.timeFrom } : {}),
                        ...(params.timeTo ? { initialTimeTo: params.timeTo } : {}),
                        initialTab: "route" as const,
                      }
                    : params.type === "flight"
                      ? {
                          initialFlightNumber: `${params.flightNumber}${params.suffix ?? ""}`,
                          initialDate: params.date,
                          initialTab: "flight" as const,
                        }
                      : {})}
            />
            <SearchHistory />
          </>
        }
        stickyContent={
          <DayTabs
            selectedDate={params.date}
            availableDates={calendarDays}
            daysBefore={1}
            daysAfter={14}
            locale={language}
            onNavigate={handleDateChange}
          />
        }
      >
        {/* Connection status indicator. Live region announces re-connect
            state changes (live ⇄ reconnecting ⇄ offline) to screen readers. */}
        <div
          className="online-board-search__status"
          data-testid="connection-status"
          role="status"
          aria-live="polite"
        >
          {connectionStatus === "live" && (
            <span className="connection-badge connection-badge--live">
              {t("SHARED.CONNECTION-LIVE")}
            </span>
          )}
          {connectionStatus === "reconnecting" && (
            <span className="connection-badge connection-badge--reconnecting">
              {t("SHARED.CONNECTION-RECONNECTING")}
            </span>
          )}
          {connectionStatus === "offline" && (
            <span className="connection-badge connection-badge--offline">
              {t("SHARED.CONNECTION-OFFLINE")}
            </span>
          )}
        </div>

        {/* §4.1.12 — Cancel search button: visible while loading */}
        {loading && (
          <div className="online-board-search__loader-bar" data-testid="loader-bar">
            <button
              type="button"
              className="online-board-search__cancel-btn"
              data-testid="cancel-search-btn"
              onClick={cancel}
            >
              {t("SHARED.SEARCH-CANCEL")}
            </button>
          </div>
        )}

        {/* §4.1.10.1 — Error state with per-status message */}
        {error && (
          <section className="frame" data-testid="search-error" role="alert">
            <div className="online-board-search__error-card">
              <h3 className="online-board-search__error-title">
                {t("BOARD.LOAD-FAILED-TITLE")}
              </h3>
              <p className="online-board-search__error-message">
                {t(errorMessageKey ?? "BOARD.LOAD-FAILED-MESSAGE")}
              </p>
              <button
                type="button"
                className="online-board-search__retry-btn"
                onClick={refresh}
              >
                {t("SHARED.RETRY")}
              </button>
            </div>
          </section>
        )}

        {/* TZ §4.1.10.1 — flight-number search that returns only Pobeda/Aurora
            flights shows a redirect banner to the subsidiary carrier sites
            instead of the normal list. */}
        {!error && params.type === "flight" && !loading && (() => {
          const banner = shouldShowPobedaAuroraBanner(displayFlights);
          return banner.show ? (
            <section className="frame">
              <PobedaAuroraBanner
                hasPobeda={banner.hasPobeda}
                hasAurora={banner.hasAurora}
              />
            </section>
          ) : null;
        })()}

        {/* Flight list — wrapped in .frame for the white card + shadow */}
        {!error && !(
          params.type === "flight" &&
          !loading &&
          shouldShowPobedaAuroraBanner(displayFlights).show
        ) && (
          <section className="frame">
            <FlightList
              flights={displayFlights}
              loading={loading}
              onFlightClick={handleFlightClick}
              initialCurrentFlightId={initialCurrentFlightId}
              direction={params.type}
              renderActions={(flight) => (
                // Mirrors Angular's board search expansion: each row
                // shows [Купить] [Онлайн регистрация] alongside the
                // Details button. Visibility falls through to the
                // same canBuyTicket / canRegister rules we use in the
                // BoardDetailsHeader.
                <FlightActions
                  flight={flight}
                  locale={language}
                  showShare={false}
                />
              )}
            />
          </section>
        )}

        {/* Footer note mirrors Angular `page-footer-notes` 1:1: a
            blue-extra-light bar attached to the flight-list frame, with
            a small `*` sort-note aligned to a line holding the localized
            system-time note. */}
        {!error && !loading && displayFlights.length > 0 && (
          <div className="page-footer-note" data-testid="footer-notes">
            <div className="page-footer-note--container">
              <div className="line line1">
                <div className="sort-note">*</div>
                <div>{t("SCHEDULE.NOTE-LINE1")}</div>
              </div>
            </div>
          </div>
        )}

        {/* Off-screen hit targets for e2e tests — shares markup contract
            with previous versions of the page. */}
        {!loading && displayFlights.length > 0 && (
          <div className="online-board-search__actions" data-testid="flight-actions">
            {displayFlights.map((flight) => (
              <button
                key={flight.id}
                type="button"
                className="flight-detail-link"
                data-testid={`flight-link-${flight.id}`}
                onClick={() => handleFlightClick(flight)}
              >
                View details for {flight.flightId.carrier} {flight.flightId.flightNumber}
              </button>
            ))}
          </div>
        )}
      </PageLayout>
    </div>
  );
};
