/**
 * Online Board filter component matching the Angular `online-board-filter`
 * component DOM structure and CSS class names.
 *
 * Renders PrimeNG-style accordion with 2 tabs: Flight Number, Route.
 * Matches Angular p-accordion structure.
 *
 * @module
 */

import { type FC, useState, useCallback, useEffect, useRef, type FormEvent, useMemo } from "react";
import { useNavigate } from "@modern-js/runtime/router";
import { useLocale } from "@/i18n/useLocale.js";
import { Calendar } from "primereact/calendar";
import { Slider, type SliderChangeEvent } from "primereact/slider";
import { useTranslation } from "@/i18n/provider.js";
import { CityAutocomplete, SwapCityButton } from "@/ui/city-autocomplete/index.js";
import { DayQuickPick } from "@/ui/calendar/DayQuickPick.js";
import { useDictionaries } from "@/shared/dictionaries/index.js";
import { useCalendarDays } from "../hooks/useCalendarDays.js";
import { todayYyyymmdd } from "@/shared/utils/datetime/index.js";
import type { CalendarParams } from "../api.js";
import { buildOnlineBoardUrl } from "../url.js";
import { setBoardFilter } from "@/shared/state/crossSectionNavigation.js";
import { formatDateWithTodayTomorrow } from "../dateLabels.js";
import "./OnlineBoardFilter.scss";

function minutesToTime(minutes: number): string {
  const h = Math.floor(minutes / 60);
  const m = minutes % 60;
  return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}`;
}

function minutesToHhmm(minutes: number): string {
  const h = Math.floor(minutes / 60);
  const m = minutes % 60;
  return `${String(h).padStart(2, "0")}${String(m).padStart(2, "0")}`;
}

type AccordionTab = "flight" | "route";

function dateToYyyymmdd(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}`;
}

function dateToIsoYmd(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}`;
}

interface NormalizedFlightNumber {
  flightNumber: string;
  suffix?: string;
  display: string;
}

/**
 * Normalise the Angular flight-number field:
 * `38` -> `0038`, `38D` -> `0038D`, `123d` -> `0123D`.
 */
function normalizeFlightNumberInput(value: string): NormalizedFlightNumber | null {
  const raw = value.trim();
  const match = /^(\d{1,4})([A-Za-z])?$/.exec(raw);
  if (!match) return null;
  const digits = match[1];
  if (!digits) return null;
  const flightNumber = digits.padStart(4, "0");
  const suffix = match[2]?.toUpperCase();
  return suffix
    ? { flightNumber, suffix, display: `${flightNumber}${suffix}` }
    : { flightNumber, display: flightNumber };
}

function formatFlightNumberInput(value: string): string {
  const normalized = normalizeFlightNumberInput(value);
  return normalized?.display ?? value.toUpperCase();
}

function validateFlightNumber(value: string): string | null {
  const raw = value.trim();
  if (!raw) {
    return "BOARD.FLIGHT_NUMBER-ERROR-EMPTY";
  }
  if (!normalizeFlightNumberInput(raw)) {
    return "BOARD.FLIGHT_NUMBER-ERROR-ONLY-NUMBER";
  }
  return null;
}

export interface OnlineBoardFilterProps {
  /** Pre-populate filter from URL params on search results pages */
  initialDeparture?: string;
  initialArrival?: string;
  initialDate?: string;
  initialTimeFrom?: string;
  initialTimeTo?: string;
  initialTab?: AccordionTab;
  initialFlightNumber?: string;
  /**
   * yyyyMMdd of "today" computed once on the SSR server (route loader)
   * and passed through. Eliminates `new Date()` calls during render that
   * would diverge between SSR and CSR and trigger React hydration errors.
   * Optional with `new Date()` fallback so unit tests don't have to wire
   * a route loader.
   */
  today?: string;
}

function hhmmToMinutes(value: string | undefined, fallback: number): number {
  if (!value || value.length !== 4) return fallback;
  const h = Number(value.slice(0, 2));
  const m = Number(value.slice(2, 4));
  if (Number.isNaN(h) || Number.isNaN(m)) return fallback;
  return h * 60 + m;
}

function yyyymmddToDate(yyyymmdd: string): Date {
  const y = parseInt(yyyymmdd.slice(0, 4), 10);
  const m = parseInt(yyyymmdd.slice(4, 6), 10) - 1;
  const d = parseInt(yyyymmdd.slice(6, 8), 10);
  return new Date(y, m, d);
}

// TZ §4.1.9.3 Table 11/12: board calendar window is -1/+14 days from today.
// Both helpers now take a base Date (parsed from the SSR-stable `today` prop)
// so identical values are computed during SSR and the matching client hydration.
function getBoardMinDate(todayDate: Date): Date {
  const d = new Date(todayDate);
  d.setHours(0, 0, 0, 0);
  d.setDate(d.getDate() - 1);
  return d;
}

function getBoardMaxDate(todayDate: Date): Date {
  const d = new Date(todayDate);
  d.setHours(0, 0, 0, 0);
  d.setDate(d.getDate() + 14);
  return d;
}

/**
 * Given the list of available yyyyMMdd date strings the API returned,
 * compute the dates inside [minDate, maxDate] that are NOT available.
 * Matches Angular's filter logic: every day in the window that isn't in
 * the bitmask is pushed to disabledDates so PrimeReact's Calendar
 * greys them out.
 */
function computeDisabledDates(
  availableYmd: string[],
  minDate: Date,
  maxDate: Date,
): Date[] {
  const available = new Set(availableYmd);
  const out: Date[] = [];
  const cursor = new Date(minDate);
  cursor.setHours(0, 0, 0, 0);
  while (cursor.getTime() <= maxDate.getTime()) {
    const ymd = dateToYyyymmdd(cursor);
    if (!available.has(ymd)) {
      out.push(new Date(cursor));
    }
    cursor.setDate(cursor.getDate() + 1);
  }
  return out;
}

export const OnlineBoardFilter: FC<OnlineBoardFilterProps> = ({
  initialDeparture,
  initialArrival,
  initialDate,
  initialTimeFrom,
  initialTimeTo,
  initialTab,
  initialFlightNumber,
  today,
}) => {
  // SSR-stable today: prefer the loader-supplied value; fall back to a
  // fresh `new Date()` only when the prop is omitted (unit tests). The
  // fallback is captured in a ref so subsequent re-renders don't drift.
  const fallbackTodayYmd = useRef(todayYyyymmdd()).current;
  const todayYmd = today ?? fallbackTodayYmd;
  const todayDate = useMemo(() => yyyymmddToDate(todayYmd), [todayYmd]);
  const { t } = useTranslation();
  const navigate = useNavigate();
  const { locale, language } = useLocale();
  const { dictionaries } = useDictionaries(language);

  const [activeTab, setActiveTab] = useState<AccordionTab>(initialTab ?? "route");

  // Flight number fields
  const [flightNumber, setFlightNumber] = useState(initialFlightNumber ?? "");
  // Start blank to match Angular's `ДД.ММ.ГГГГ` placeholder; the search
  // handler defaults to today when the field is left empty so the
  // legacy "search today" UX still works.
  const [flightDate, setFlightDate] = useState<Date | null>(
    initialTab === "flight" && initialDate ? yyyymmddToDate(initialDate) : null,
  );
  const [flightNumberError, setFlightNumberError] = useState<string | null>(null);
  // Mirrors Angular OnlineBoardRouteFilterValidationService state: a
  // single rejection message shown next to the arrival input when
  // departure === arrival. Cleared as soon as the user edits either field.
  const [routeSameCitiesError, setRouteSameCitiesError] = useState<string | null>(null);

  // Route fields
  const [routeDepartureCode, setRouteDepartureCode] = useState<string>(initialDeparture ?? "");
  const [routeArrivalCode, setRouteArrivalCode] = useState<string>(initialArrival ?? "");
  const [routeDate, setRouteDate] = useState<Date | null>(
    initialDate ? yyyymmddToDate(initialDate) : null,
  );
  const [timeRange, setTimeRange] = useState<[number, number]>([
    hhmmToMinutes(initialTimeFrom, 0),
    hhmmToMinutes(initialTimeTo, 1440),
  ]);

  // Computed from the SSR-stable todayDate, so SSR and CSR see the same
  // boundaries. useMemo over useRef so a date roll-over during the page's
  // lifetime updates the calendar window correctly on subsequent renders.
  const boardMinDate = useMemo(() => getBoardMinDate(todayDate), [todayDate]);
  const boardMaxDate = useMemo(() => getBoardMaxDate(todayDate), [todayDate]);
  const boardCalendarBaseDate = useMemo(
    () => dateToIsoYmd(boardMinDate),
    [boardMinDate],
  );

  // TIRREDESIGN-12: fetch the 31-day operating-days bitmask for the
  // current tab so non-operating days in the [minDate, maxDate] window
  // render as disabled in the PrimeReact calendar. Only query when the
  // user has typed enough input to resolve a calendar segment — an empty
  // flight number or empty route produces no API call.
  const flightCalendarParams = useMemo<CalendarParams | null>(() => {
    if (activeTab !== "flight") return null;
    const normalized = normalizeFlightNumberInput(flightNumber);
    if (!normalized) return null;
    return {
      date: boardCalendarBaseDate,
      searchType: "flight",
      flightNumber: `SU${normalized.flightNumber}${normalized.suffix ?? ""}`,
    };
  }, [activeTab, flightNumber, boardCalendarBaseDate]);

  const routeCalendarParams = useMemo<CalendarParams | null>(() => {
    if (activeTab !== "route") return null;
    const dep = routeDepartureCode.trim().toUpperCase();
    const arr = routeArrivalCode.trim().toUpperCase();
    if (!dep && !arr) return null;
    if (dep && arr) {
      if (dep === arr) return null;
      return { date: boardCalendarBaseDate, searchType: "route", departure: dep, arrival: arr };
    }
    if (dep) return { date: boardCalendarBaseDate, searchType: "departure", departure: dep };
    return { date: boardCalendarBaseDate, searchType: "arrival", arrival: arr };
  }, [activeTab, routeDepartureCode, routeArrivalCode, boardCalendarBaseDate]);

  const {
    days: flightAvailableDays,
    loaded: flightCalendarLoaded,
  } = useCalendarDays(flightCalendarParams);
  const {
    days: routeAvailableDays,
    loaded: routeCalendarLoaded,
  } = useCalendarDays(routeCalendarParams);

  const flightDisabledDates = useMemo(
    () =>
      !flightCalendarLoaded
        ? []
        : computeDisabledDates(flightAvailableDays, boardMinDate, boardMaxDate),
    [flightAvailableDays, flightCalendarLoaded, boardMinDate, boardMaxDate],
  );
  const routeDisabledDates = useMemo(
    () =>
      !routeCalendarLoaded
        ? []
        : computeDisabledDates(routeAvailableDays, boardMinDate, boardMaxDate),
    [routeAvailableDays, routeCalendarLoaded, boardMinDate, boardMaxDate],
  );

  // §4.1.10 — submit button locked for 30 seconds after each search.
  // Value is the timestamp when the lock expires (or 0 if unlocked).
  // The 30-second constant is intentionally hardcoded (not configurable).
  const [submitLockedUntil, setSubmitLockedUntil] = useState(0);
  const [lockedSearchSignature, setLockedSearchSignature] = useState<string | null>(null);
  const [now, setNow] = useState(() => Date.now());
  // Tick every second while the lock is active so the disabled state
  // updates reactively.
  useEffect(() => {
    if (submitLockedUntil === 0 || now >= submitLockedUntil) return;
    const id = setTimeout(() => setNow(Date.now()), 1000);
    return () => clearTimeout(id);
  }, [submitLockedUntil, now]);
  const flightSearchSignature = useMemo(() => {
    const dateParam = dateToYyyymmdd(flightDate ?? todayDate);
    return [
      "flight",
      flightNumber.trim(),
      dateParam,
    ].join("|");
  }, [flightNumber, flightDate, todayDate]);

  const routeSearchSignature = useMemo(() => {
    const dateParam = dateToYyyymmdd(routeDate ?? todayDate);
    const timePart =
      timeRange[0] !== 0 || timeRange[1] !== 1440
        ? `${minutesToHhmm(timeRange[0])}-${minutesToHhmm(timeRange[1])}`
        : "full-day";
    return [
      "route",
      routeDepartureCode.trim().toUpperCase(),
      routeArrivalCode.trim().toUpperCase(),
      dateParam,
      timePart,
    ].join("|");
  }, [routeDepartureCode, routeArrivalCode, routeDate, timeRange, todayDate]);

  const activeSearchSignature =
    activeTab === "flight" ? flightSearchSignature : routeSearchSignature;
  const isSubmitLocked = useMemo(
    () =>
      submitLockedUntil > 0 &&
      now < submitLockedUntil &&
      lockedSearchSignature === activeSearchSignature,
    [submitLockedUntil, now, lockedSearchSignature, activeSearchSignature],
  );

  // Swap the Calendar input's display text to "Сегодня" / "Завтра" per
  // TZ §4.1.9 Tables 11+12 — Angular ships this and the raw 'DD.MM.YYYY'
  // reads clinical in comparison.
  const flightDateInputRef = useRef<HTMLInputElement>(null);
  const routeDateInputRef = useRef<HTMLInputElement>(null);
  useEffect(() => {
    const apply = (ref: HTMLInputElement | null, date: Date | null) => {
      if (!ref || !date) return;
      const label = formatDateWithTodayTomorrow(date, t);
      // Only override when the helper returns a special label (today/tomorrow);
      // for plain dates PrimeReact's own dateFormat rendering is correct.
      const today = new Date();
      today.setHours(0, 0, 0, 0);
      const d = new Date(date);
      d.setHours(0, 0, 0, 0);
      const delta = Math.round((d.getTime() - today.getTime()) / 86_400_000);
      if (delta === 0 || delta === 1) {
        ref.value = label;
      }
    };
    apply(flightDateInputRef.current, flightDate);
    apply(routeDateInputRef.current, routeDate);
  }, [flightDate, routeDate, t, activeTab]);

  // When the parent feeds new initial* props (e.g. a popular-request click
  // pushes ?departure=SVO&arrival=LED into the URL), keep the fields in
  // sync. useState only reads initial values once, so without this effect
  // clicking a popular route left the sidebar untouched.
  const lastInitialRef = useRef({
    departure: initialDeparture,
    arrival: initialArrival,
    date: initialDate,
    tab: initialTab,
    flightNumber: initialFlightNumber,
    timeFrom: initialTimeFrom,
    timeTo: initialTimeTo,
  });
  useEffect(() => {
    const prev = lastInitialRef.current;
    if (prev.departure !== initialDeparture) {
      setRouteDepartureCode(initialDeparture ?? "");
    }
    if (prev.arrival !== initialArrival) {
      setRouteArrivalCode(initialArrival ?? "");
    }
    if (prev.date !== initialDate && initialDate) {
      setRouteDate(yyyymmddToDate(initialDate));
      if (initialTab === "flight") {
        setFlightDate(yyyymmddToDate(initialDate));
      }
    }
    if (prev.tab !== initialTab && initialTab) {
      setActiveTab(initialTab);
    }
    if (prev.flightNumber !== initialFlightNumber) {
      setFlightNumber(initialFlightNumber ?? "");
    }
    if (prev.timeFrom !== initialTimeFrom || prev.timeTo !== initialTimeTo) {
      setTimeRange([
        hhmmToMinutes(initialTimeFrom, 0),
        hhmmToMinutes(initialTimeTo, 1440),
      ]);
    }
    lastInitialRef.current = {
      departure: initialDeparture,
      arrival: initialArrival,
      date: initialDate,
      tab: initialTab,
      flightNumber: initialFlightNumber,
      timeFrom: initialTimeFrom,
      timeTo: initialTimeTo,
    };
  }, [
    initialDeparture,
    initialArrival,
    initialDate,
    initialTab,
    initialFlightNumber,
    initialTimeFrom,
    initialTimeTo,
  ]);

  const handleTabClick = useCallback((tab: AccordionTab) => {
    setActiveTab((prev) => (prev === tab ? prev : tab));
  }, []);

  const handleExchange = useCallback(() => {
    const prevDep = routeDepartureCode;
    setRouteDepartureCode(routeArrivalCode);
    setRouteArrivalCode(prevDep);
  }, [routeDepartureCode, routeArrivalCode]);

  const handleFlightSubmit = useCallback(
    (e: FormEvent) => {
      e.preventDefault();
      if (isSubmitLocked) return;

      const error = validateFlightNumber(flightNumber);
      setFlightNumberError(error);
      if (error) return;

      // Empty date defaults to today, matching Angular's behaviour when
      // the placeholder ДД.ММ.ГГГГ is left untouched.
      const dateParam = dateToYyyymmdd(flightDate ?? new Date());
      const carrier = "SU";
      const normalized = normalizeFlightNumberInput(flightNumber);
      if (!normalized) return;

      // TZ §4.1.8: persist filter snapshot for cross-section hydration.
      setBoardFilter({
        mode: "flight-number",
        flightNumber: `${carrier}${normalized.flightNumber}${normalized.suffix ?? ""}`,
        date: dateParam,
        timeFrom: "0000",
        timeTo: "2400",
        searchExecuted: true,
      });

      // Lock submit for 30 seconds (§4.1.10 — hardcoded, not configurable)
      setLockedSearchSignature(flightSearchSignature);
      setSubmitLockedUntil(Date.now() + 30_000);
      setNow(Date.now());

      const urlParams = normalized.suffix
        ? {
            type: "flight" as const,
            carrier,
            flightNumber: normalized.flightNumber,
            suffix: normalized.suffix,
            date: dateParam,
          }
        : {
            type: "flight" as const,
            carrier,
            flightNumber: normalized.flightNumber,
            date: dateParam,
          };
      const url = buildOnlineBoardUrl(urlParams);
      void navigate(`/${locale}/${url}`);
    },
    [flightNumber, flightDate, navigate, locale, isSubmitLocked, flightSearchSignature],
  );

  const handleRouteSubmit = useCallback(
    (e: FormEvent) => {
      e.preventDefault();
      if (isSubmitLocked) return;

      // Mirrors Angular's OnlineBoardFilterService.toRoutePage + the
      // UrlBuilder.getRoutePageUrl switch: one-sided searches (only
      // departure or only arrival) route to /departure/ or /arrival/
      // respectively; both sides route to /route/. No-op only when
      // neither side is filled (Angular's
      // `if (!departure && !arrival) return;`).
      const dateParam = dateToYyyymmdd(routeDate ?? new Date());
      const depCode = routeDepartureCode.trim().toUpperCase();
      const arrCode = routeArrivalCode.trim().toUpperCase();
      if (!depCode && !arrCode) return;

      // Angular's validation rejects identical departure/arrival codes.
      // Surface the same translated message and bail out before building
      // a URL that would return an empty board.
      if (depCode && arrCode && depCode === arrCode) {
        setRouteSameCitiesError("SHARED.ARRIVAL-EQUALS-DEPARTURE-ERROR");
        return;
      }
      setRouteSameCitiesError(null);

      // Only send time range when the user has narrowed the slider, so
      // default 00:00–24:00 searches keep the URL clean (Angular spreads
      // `...this.timeRange` which is undefined until interaction).
      const timeExtras =
        timeRange[0] !== 0 || timeRange[1] !== 1440
          ? { timeFrom: minutesToHhmm(timeRange[0]), timeTo: minutesToHhmm(timeRange[1]) }
          : {};

      let url: string;
      if (depCode && !arrCode) {
        // TZ §4.1.8: persist filter snapshot for cross-section hydration.
        setBoardFilter({
          mode: "departure",
          departure: depCode,
          date: dateParam,
          timeFrom: timeExtras.timeFrom ?? "0000",
          timeTo: timeExtras.timeTo ?? "2400",
          searchExecuted: true,
        });
        url = buildOnlineBoardUrl({ type: "departure", station: depCode, date: dateParam, ...timeExtras });
      } else if (!depCode && arrCode) {
        // TZ §4.1.8: persist filter snapshot for cross-section hydration.
        setBoardFilter({
          mode: "arrival",
          arrival: arrCode,
          date: dateParam,
          timeFrom: timeExtras.timeFrom ?? "0000",
          timeTo: timeExtras.timeTo ?? "2400",
          searchExecuted: true,
        });
        url = buildOnlineBoardUrl({ type: "arrival", station: arrCode, date: dateParam, ...timeExtras });
      } else {
        // TZ §4.1.8: persist filter snapshot for cross-section hydration.
        setBoardFilter({
          mode: "route",
          departure: depCode,
          arrival: arrCode,
          date: dateParam,
          timeFrom: timeExtras.timeFrom ?? "0000",
          timeTo: timeExtras.timeTo ?? "2400",
          searchExecuted: true,
        });
        url = buildOnlineBoardUrl({ type: "route", departure: depCode, arrival: arrCode, date: dateParam, ...timeExtras });
      }
      // Lock submit for 30 seconds (§4.1.10 — hardcoded, not configurable)
      setLockedSearchSignature(routeSearchSignature);
      setSubmitLockedUntil(Date.now() + 30_000);
      setNow(Date.now());
      void navigate(`/${locale}/${url}`);
    },
    [routeDepartureCode, routeArrivalCode, routeDate, timeRange, navigate, locale, isSubmitLocked, routeSearchSignature],
  );

  return (
    <div className="online-board-filter" data-testid="flight-filter">
      <section className="frame">
        <div className="p-accordion" data-testid="filter-accordion">
          {/* Flight Number Tab */}
          <div className={`p-accordion-tab${activeTab === "flight" ? " p-accordion-tab--active" : ""}`}>
            <div
              className={`p-accordion-header${activeTab === "flight" ? " p-highlight" : ""}`}
              data-testid="search-type-flight"
            >
              <a
                role="button"
                tabIndex={0}
                aria-expanded={activeTab === "flight"}
                onClick={() => handleTabClick("flight")}
                onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") handleTabClick("flight"); }}
              >
                <span className="p-header">
                  <span>{t("BOARD.FLIGHT_NUMBER")}</span>
                  <span className={`arrow-icon${activeTab === "flight" ? " arrow-icon--rotated" : ""}`} aria-hidden="true">
                    <svg viewBox="0 0 12 8" width="12" height="8">
                      <path d="M1 1L6 6L11 1" stroke="currentColor" strokeWidth="1.5" fill="none" />
                    </svg>
                  </span>
                </span>
              </a>
            </div>
            {activeTab === "flight" && (
              <div className="p-accordion-content">
                <form
                  onSubmit={handleFlightSubmit}
                  data-testid="search-form"
                  aria-label={t("BOARD.FLIGHT_NUMBER")}
                >
                  <div className="filter-content">
                    <div className="p-field">
                      <label className="label--filter">
                        {t("SHARED.FLIGHT_NUMBER")}
                      </label>
                      {flightNumberError && (
                        <div
                          className="validation-tooltip"
                          data-testid="flight-number-error"
                          role="alert"
                        >
                          {t(flightNumberError)}
                        </div>
                      )}
                      <div className={`number-input-composite${flightNumber ? " has-value" : ""}${flightNumberError ? " has-error" : ""}`}>
                        <div className="prefix">SU</div>
                        <input
                          type="text"
                          className="input--filter input--flight-number"
                          placeholder={t("SHARED.FLIGHT_NUMBER_PLACEHOLDER")}
                          value={flightNumber}
                          maxLength={5}
                          aria-invalid={Boolean(flightNumberError)}
                          onChange={(e) => {
                            setFlightNumber(e.target.value);
                            if (flightNumberError) setFlightNumberError(null);
                          }}
                          onBlur={(e) => setFlightNumber(formatFlightNumberInput(e.target.value))}
                          data-testid="flight-number-input"
                        />
                        {flightNumber && (
                          <button
                            className="button-clear"
                            type="button"
                            aria-label={t("SHARED.A11Y-CLEAR")}
                            onClick={() => {
                              setFlightNumber("");
                              setFlightNumberError(null);
                            }}
                            data-testid="flight-number-clear"
                          >
                            &times;
                          </button>
                        )}
                      </div>
                    </div>

                    <div className="calendar">
                      <label className="label--filter">
                        {t("SHARED.FLIGHT_DATE")}
                      </label>
                      <DayQuickPick
                        value={flightDate}
                        locale={language}
                        onChange={setFlightDate}
                      />
                      <div className="calendar-input-wrapper">
                        <Calendar
                          value={flightDate}
                          onChange={(e) => setFlightDate(e.value as Date | null)}
                          minDate={boardMinDate}
                          maxDate={boardMaxDate}
                          disabledDates={flightDisabledDates}
                          dateFormat="dd.mm.yy"
                          placeholder={t("SHARED.DATE_FORMAT")}
                          showIcon
                          className="input--filter"
                          data-testid="date-input"
                          inputId="search-date-input"
                          inputRef={flightDateInputRef}
                        />
                        {flightDate && (
                          <button
                            type="button"
                            className="calendar-clear-btn"
                            aria-label={t("SHARED.A11Y-CLEAR")}
                            data-testid="flight-date-clear"
                            onClick={() => setFlightDate(null)}
                          >
                            &times;
                          </button>
                        )}
                      </div>
                    </div>
                  </div>

                  <div className="filter-button">
                    <button
                      type="submit"
                      className="search-button"
                      data-testid="search-submit"
                      disabled={isSubmitLocked}
                      aria-disabled={isSubmitLocked}
                    >
                      <span>{t("SHARED.SEARCH")}</span>
                    </button>
                  </div>
                </form>
              </div>
            )}
          </div>

          {/* Route Tab */}
          <div className={`p-accordion-tab${activeTab === "route" ? " p-accordion-tab--active" : ""}`}>
            <div
              className={`p-accordion-header${activeTab === "route" ? " p-highlight" : ""}`}
              data-testid="search-type-route"
            >
              <a
                role="button"
                tabIndex={0}
                aria-expanded={activeTab === "route"}
                onClick={() => handleTabClick("route")}
                onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") handleTabClick("route"); }}
              >
                <span className="p-header">
                  <span>{t("BOARD.ROUTE")}</span>
                  <span className={`arrow-icon${activeTab === "route" ? " arrow-icon--rotated" : ""}`} aria-hidden="true">
                    <svg viewBox="0 0 12 8" width="12" height="8">
                      <path d="M1 1L6 6L11 1" stroke="currentColor" strokeWidth="1.5" fill="none" />
                    </svg>
                  </span>
                </span>
              </a>
            </div>
            {activeTab === "route" && (
              <div className="p-accordion-content">
                <form
                  onSubmit={handleRouteSubmit}
                  data-testid="search-form"
                  aria-label={t("BOARD.ROUTE")}
                >
                  <div className="filter-content">
                    <CityAutocomplete
                      label={t("SHARED.DEPARTURE_CITY")}
                      placeholder={
                        routeArrivalCode && !routeDepartureCode
                          ? t("SHARED.ALL_DIRECTIONS")
                          : t("SHARED.CITY_PLACEHOLDER")
                      }
                      value={routeDepartureCode}
                      onChange={(code) => {
                        setRouteDepartureCode(code);
                        if (routeSameCitiesError) setRouteSameCitiesError(null);
                      }}
                      dictionaries={dictionaries}
                      testIdPrefix="route-departure"
                    />

                    <SwapCityButton
                      onClick={handleExchange}
                      testId="swap-cities-button"
                    />

                    <CityAutocomplete
                      label={t("SHARED.ARRIVAL_CITY")}
                      placeholder={
                        routeDepartureCode && !routeArrivalCode
                          ? t("SHARED.ALL_DIRECTIONS")
                          : t("SHARED.CITY_PLACEHOLDER")
                      }
                      value={routeArrivalCode}
                      onChange={(code) => {
                        setRouteArrivalCode(code);
                        if (routeSameCitiesError) setRouteSameCitiesError(null);
                      }}
                      dictionaries={dictionaries}
                      testIdPrefix="route-arrival"
                    />
                    {routeSameCitiesError && (
                      <div
                        className="validation-tooltip"
                        data-testid="route-same-cities-error"
                        role="alert"
                      >
                        {t(routeSameCitiesError)}
                      </div>
                    )}

                    <div className="calendar">
                      <label className="label--filter">
                        {t("SHARED.FLIGHT_DATE")}
                      </label>
                      <DayQuickPick
                        value={routeDate}
                        locale={language}
                        onChange={setRouteDate}
                      />
                      <div className="calendar-input-wrapper">
                        <Calendar
                          value={routeDate}
                          onChange={(e) => setRouteDate(e.value as Date | null)}
                          minDate={boardMinDate}
                          maxDate={boardMaxDate}
                          disabledDates={routeDisabledDates}
                          dateFormat="dd.mm.yy"
                          placeholder={t("SHARED.DATE_FORMAT")}
                          showIcon
                          className="input--filter"
                          data-testid="date-input"
                          inputId="route-date-input"
                          inputRef={routeDateInputRef}
                        />
                        {routeDate && (
                          <button
                            type="button"
                            className="calendar-clear-btn"
                            aria-label={t("SHARED.A11Y-CLEAR")}
                            data-testid="route-date-clear"
                            onClick={() => setRouteDate(null)}
                          >
                            &times;
                          </button>
                        )}
                      </div>
                    </div>
                  </div>

                  <div className="wrapper--time-selector compact-view" data-testid="time-selector">
                    <div className="time-selector__label-value">
                      <div className="time-selector__label">{t("SHARED.FLIGHT_TIME")}</div>
                      <div className="time-selector__value">
                        {minutesToTime(timeRange[0])} — {minutesToTime(timeRange[1])}
                      </div>
                    </div>
                    <div className="time-selector">
                      <Slider
                        value={timeRange}
                        onChange={(e: SliderChangeEvent) => {
                          const raw = e.value as [number, number];
                          const MIN_GAP = 60;
                          let from = Math.min(raw[0], raw[1]);
                          let to = Math.max(raw[0], raw[1]);
                          if (to - from < MIN_GAP) {
                            if (from + MIN_GAP <= 1440) {
                              to = from + MIN_GAP;
                            } else {
                              to = 1440;
                              from = to - MIN_GAP;
                            }
                          }
                          setTimeRange([from, to]);
                        }}
                        range
                        min={0}
                        max={1440}
                        step={60}
                      />
                    </div>
                  </div>

                  <div className="filter-button">
                    <button
                      type="submit"
                      className="search-button"
                      data-testid="search-submit"
                      disabled={isSubmitLocked}
                      aria-disabled={isSubmitLocked}
                    >
                      <span>{t("SHARED.SEARCH")}</span>
                    </button>
                  </div>
                </form>
              </div>
            )}
          </div>
        </div>
      </section>
    </div>
  );
};
