import { useState, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import type { JourneyLeg } from '../../types';
import { resolveTransitVariant, type TravelTimeEntry } from '../../hooks/useTravelTime';
import { apiUrl, authHeaders, logNonAbortError } from '../../lib/api';
import { WalkingIcon } from '../ui/icons/WalkingIcon';
import { BicycleIcon } from '../ui/icons/BicycleIcon';

interface JourneyInstructionsProps {
  postcode: string;
  entries: TravelTimeEntry[];
  /** When set, shown as a subtitle (e.g. the central postcode for a hexagon) */
  label?: string;
  /** Preloaded journey rows, useful for static demos that should not call the API. */
  presetJourneys?: JourneyInstructionPreset[];
  className?: string;
  showGoogleMapsLink?: boolean;
  shareCode?: string;
}

interface JourneyData {
  slug: string;
  label: string;
  legs: JourneyLeg[] | null;
  /** Median (50th percentile) total travel time from R5, including waiting. */
  minutes: number | null;
  /** Best-case (5th percentile) total travel time from R5. */
  bestMinutes: number | null;
  destinationLat: number | null;
  destinationLon: number | null;
  /** Whether the dashboard filter is currently using best-case time. */
  useBest: boolean;
  noChange: boolean;
  noBuses: boolean;
  loading: boolean;
}

export interface JourneyInstructionPreset {
  slug: string;
  label: string;
  legs: JourneyLeg[] | null;
  /** Median (50th percentile) total travel time, including waiting. */
  minutes: number | null;
  /** Best-case (5th percentile) total travel time. */
  bestMinutes?: number | null;
  destinationLat?: number | null;
  destinationLon?: number | null;
  useBest?: boolean;
  noChange?: boolean;
  noBuses?: boolean;
}

// Official TfL line colors + other known London transit
const ROUTE_COLORS: Record<string, { color: string; darkText?: boolean }> = {
  Bakerloo: { color: '#B36305' },
  Central: { color: '#E32017' },
  Circle: { color: '#FFD300', darkText: true },
  District: { color: '#00782A' },
  'Elizabeth line': { color: '#6950A1' },
  Elizabeth: { color: '#6950A1' },
  'Hammersmith & City': { color: '#F3A9BB', darkText: true },
  'Hammersmith and City': { color: '#F3A9BB', darkText: true },
  Jubilee: { color: '#A0A5A9', darkText: true },
  Metropolitan: { color: '#9B0056' },
  Northern: { color: '#333333' },
  Piccadilly: { color: '#003688' },
  Victoria: { color: '#0098D4' },
  'Waterloo & City': { color: '#95CDBA', darkText: true },
  'Waterloo and City': { color: '#95CDBA', darkText: true },
  DLR: { color: '#00A4A7' },
  'London Overground': { color: '#EE7C0E' },
};

const NON_TUBE_NAMES = new Set(['DLR', 'London Overground', 'Elizabeth line']);
const GOOGLE_MAPS_DEPARTURE_TIME_ZONE = 'Europe/London';
const londonDateFormatter = new Intl.DateTimeFormat('en-GB', {
  timeZone: GOOGLE_MAPS_DEPARTURE_TIME_ZONE,
  year: 'numeric',
  month: '2-digit',
  day: '2-digit',
});
const londonDateTimeFormatter = new Intl.DateTimeFormat('en-GB', {
  timeZone: GOOGLE_MAPS_DEPARTURE_TIME_ZONE,
  year: 'numeric',
  month: '2-digit',
  day: '2-digit',
  hour: '2-digit',
  minute: '2-digit',
  second: '2-digit',
  hour12: false,
  hourCycle: 'h23',
});

function dateTimeParts(formatter: Intl.DateTimeFormat, date: Date): Record<string, number> {
  const parts: Record<string, number> = {};
  formatter.formatToParts(date).forEach((part) => {
    if (part.type !== 'literal') {
      parts[part.type] = Number(part.value);
    }
  });
  return parts;
}

/** Strip trailing parenthesized GTFS route IDs and NaPTAN stop codes (e.g. "(6757261)", "(9400ZZLUCGT1)") */
function stripId(label: string): string {
  return label.replace(/\s+\([A-Za-z0-9]+\)$/, '');
}

function getRouteDisplay(mode: string): { label: string; color: string; darkText: boolean } {
  const clean = stripId(mode);
  const known = ROUTE_COLORS[clean];
  if (known) {
    const label = NON_TUBE_NAMES.has(clean) || clean.includes('line') ? clean : `${clean} line`;
    return { label, color: known.color, darkText: !!known.darkText };
  }
  if (/^\d+[A-Za-z]?$/.test(clean.trim())) {
    return { label: `Bus ${clean}`, color: '#0d9488', darkText: false };
  }
  return { label: clean, color: '#6b7280', darkText: false };
}

function londonOffsetMs(utcMs: number): number {
  const parts = dateTimeParts(londonDateTimeFormatter, new Date(utcMs));
  const londonAsUtcMs = Date.UTC(
    parts.year,
    parts.month - 1,
    parts.day,
    parts.hour,
    parts.minute,
    parts.second
  );
  return londonAsUtcMs - utcMs;
}

function londonTimeToUtcMs(
  year: number,
  month: number,
  day: number,
  hour: number,
  minute: number
): number {
  const localAsUtcMs = Date.UTC(year, month - 1, day, hour, minute, 0, 0);
  const offsetMs = londonOffsetMs(localAsUtcMs);
  const utcMs = localAsUtcMs - offsetMs;
  const correctedOffsetMs = londonOffsetMs(utcMs);
  return correctedOffsetMs === offsetMs ? utcMs : localAsUtcMs - correctedOffsetMs;
}

/** Returns a Unix timestamp for the next Monday at 07:30 Europe/London time. */
function nextMondayAt730(): number {
  const now = new Date();
  const today = dateTimeParts(londonDateFormatter, now);
  const day = new Date(Date.UTC(today.year, today.month - 1, today.day)).getUTCDay();
  const daysUntil = day === 0 ? 1 : day === 1 ? 7 : 8 - day;
  const monday = new Date(Date.UTC(today.year, today.month - 1, today.day + daysUntil));
  const utcMs = londonTimeToUtcMs(
    monday.getUTCFullYear(),
    monday.getUTCMonth() + 1,
    monday.getUTCDate(),
    7,
    30
  );
  return Math.floor(utcMs / 1000);
}

function googleMapsDestination(
  destination: string,
  destinationLat?: number | null,
  destinationLon?: number | null
): string {
  if (
    destinationLat != null &&
    destinationLon != null &&
    Number.isFinite(destinationLat) &&
    Number.isFinite(destinationLon)
  ) {
    return `${destinationLat},${destinationLon}`;
  }
  return stripId(destination).trim();
}

export function googleMapsUrl(
  origin: string,
  destination: string,
  destinationLat?: number | null,
  destinationLon?: number | null
): string {
  const ts = nextMondayAt730();
  const params = new URLSearchParams({
    api: '1',
    origin,
    destination: googleMapsDestination(destination, destinationLat, destinationLon),
    travelmode: 'transit',
    departure_time: ts.toString(),
  });
  return `https://www.google.com/maps/dir/?${params.toString()}`;
}

function invertLegs(legs: JourneyLeg[]): JourneyLeg[] {
  return [...legs]
    .reverse()
    .map((leg) => (leg.from && leg.to ? { ...leg, from: leg.to, to: leg.from } : leg));
}

function RouteBadge({ mode }: { mode: string }) {
  const { label, color, darkText } = getRouteDisplay(mode);
  return (
    <span
      className="inline-flex items-center text-[10px] font-bold px-1.5 py-px rounded-sm leading-tight tracking-wide"
      style={{ backgroundColor: color, color: darkText ? '#292524' : '#fff' }}
    >
      {label}
    </span>
  );
}

function TimelineLeg({ leg, isLast }: { leg: JourneyLeg; isLast: boolean }) {
  const { t } = useTranslation();
  const isAccess = leg.mode === 'walk' || leg.mode === 'bicycle';

  if (isAccess) {
    return (
      <div className="flex">
        <div className="flex flex-col items-center w-4 mr-2">
          <div className="w-2 h-2 rounded-full border-[1.5px] border-warm-400 dark:border-warm-500 shrink-0 mt-1" />
          {!isLast && (
            <div className="flex-1 min-h-[4px] border-l border-dashed border-warm-300 dark:border-warm-600" />
          )}
        </div>
        <div className="flex items-center gap-1.5 py-0.5 min-w-0">
          {leg.mode === 'walk' ? (
            <WalkingIcon className="w-3 h-3 text-warm-400 dark:text-warm-500 shrink-0" />
          ) : (
            <BicycleIcon className="w-3 h-3 text-warm-400 dark:text-warm-500 shrink-0" />
          )}
          <span className="text-[11px] text-warm-500 dark:text-warm-400">
            {leg.mode === 'walk' ? t('areaPane.walk') : t('areaPane.cycle')} · {leg.minutes}{' '}
            {t('common.minute')}
          </span>
        </div>
      </div>
    );
  }

  const { color } = getRouteDisplay(leg.mode);
  return (
    <div className="flex">
      <div className="flex flex-col items-center w-4 mr-2">
        <div
          className="w-2.5 h-2.5 rounded-full shrink-0 mt-0.5"
          style={{ backgroundColor: color }}
        />
        {!isLast && <div className="flex-1 min-h-[4px] w-px bg-warm-300 dark:bg-warm-600" />}
      </div>
      <div className="pb-1.5 min-w-0 flex-1">
        <div className="flex items-center gap-1.5 flex-wrap">
          <RouteBadge mode={leg.mode} />
          <span className="text-[11px] text-warm-500 dark:text-warm-400">
            {leg.minutes} {t('common.minute')}
          </span>
        </div>
        {leg.from && leg.to && (
          <div className="text-[11px] text-warm-600 dark:text-warm-300 mt-0.5">
            {stripId(leg.from)} → {stripId(leg.to)}
          </div>
        )}
      </div>
    </div>
  );
}

export default function JourneyInstructions({
  postcode,
  entries,
  label,
  presetJourneys,
  className,
  showGoogleMapsLink = true,
  shareCode,
}: JourneyInstructionsProps) {
  const { t } = useTranslation();
  const [journeys, setJourneys] = useState<JourneyData[]>([]);

  // Only transit entries with a destination set
  const transitEntries = useMemo(
    () => entries.filter((e) => e.mode === 'transit' && e.slug !== ''),
    [entries]
  );
  const hasPresetJourneys = Boolean(presetJourneys?.length);

  useEffect(() => {
    if (hasPresetJourneys) {
      setJourneys([]);
      return;
    }

    if (transitEntries.length === 0) {
      setJourneys([]);
      return;
    }

    const controller = new AbortController();
    const results: JourneyData[] = transitEntries.map((e) => ({
      slug: e.slug,
      label: e.label,
      legs: null,
      minutes: null,
      bestMinutes: null,
      destinationLat: null,
      destinationLon: null,
      useBest: e.useBest,
      noChange: e.noChange ?? false,
      noBuses: e.noBuses ?? false,
      loading: true,
    }));
    setJourneys([...results]);

    transitEntries.forEach((entry, idx) => {
      const serverMode = resolveTransitVariant(entry);
      const params = new URLSearchParams({
        postcode,
        mode: serverMode,
        slug: entry.slug,
      });
      if (shareCode) params.set('share', shareCode);
      fetch(apiUrl('journey', params), authHeaders({ signal: controller.signal }))
        .then((res) => {
          if (!res.ok) throw new Error(`HTTP ${res.status}`);
          return res.json();
        })
        .then(
          (data: {
            journey: JourneyLeg[] | null;
            minutes: number | null;
            best_minutes: number | null;
            destination_lat?: number | null;
            destination_lon?: number | null;
          }) => {
            setJourneys((prev) =>
              prev.map((j, i) =>
                i === idx
                  ? {
                      ...j,
                      legs: data.journey,
                      minutes: data.minutes,
                      bestMinutes: data.best_minutes,
                      destinationLat: data.destination_lat ?? null,
                      destinationLon: data.destination_lon ?? null,
                      loading: false,
                    }
                  : j
              )
            );
          }
        )
        .catch((err) => {
          logNonAbortError('journey', err);
          setJourneys((prev) => prev.map((j, i) => (i === idx ? { ...j, loading: false } : j)));
        });
    });

    return () => controller.abort();
  }, [postcode, hasPresetJourneys, transitEntries, shareCode]);

  if (transitEntries.length === 0 && !hasPresetJourneys) return null;

  const displayedJourneys: JourneyData[] = hasPresetJourneys
    ? (presetJourneys ?? []).map((journey) => ({
        slug: journey.slug,
        label: journey.label,
        legs: journey.legs,
        minutes: journey.minutes,
        bestMinutes: journey.bestMinutes ?? null,
        destinationLat: journey.destinationLat ?? null,
        destinationLon: journey.destinationLon ?? null,
        useBest: journey.useBest ?? false,
        noChange: journey.noChange ?? false,
        noBuses: journey.noBuses ?? false,
        loading: false,
      }))
    : journeys;

  return (
    <div className={className ?? 'mx-3 mt-2 space-y-2'}>
      {label && (
        <div className="text-xs text-warm-500 dark:text-warm-400">
          {t('areaPane.journeysFrom', { label })}
        </div>
      )}
      {displayedJourneys.map((j, index) => {
        const legSum = j.legs ? j.legs.reduce((sum, l) => sum + l.minutes, 0) : 0;
        const totalMin = j.useBest && j.bestMinutes != null ? j.bestMinutes : (j.minutes ?? legSum);
        const isBestCase = j.useBest && j.bestMinutes != null;
        const journeyLabels: string[] = [];
        if (isBestCase) journeyLabels.push(t('travel.bestCase'));
        if (j.noChange) journeyLabels.push(t('travel.noChange'));
        if (j.noBuses) journeyLabels.push(t('travel.noBuses'));
        const displayLegs = j.legs ? invertLegs(j.legs) : null;
        const destination = j.label || j.slug;

        return (
          <div key={`${j.slug}-${index}`} className="bg-warm-50 dark:bg-warm-800 rounded-lg p-2.5">
            <div className="flex items-start justify-between gap-2 mb-2">
              <span className="min-w-0 text-xs font-medium text-warm-700 dark:text-warm-300">
                {t('areaPane.to', { destination })}
              </span>
              {!j.loading && totalMin > 0 && (
                <div className="min-w-0 max-w-[58%] text-right">
                  {journeyLabels.length > 0 && (
                    <div className="text-[10px] font-semibold leading-tight text-teal-700 dark:text-teal-400">
                      {journeyLabels.join(' · ')}
                    </div>
                  )}
                  <div className="text-xs font-semibold leading-tight text-teal-700 dark:text-teal-400">
                    {totalMin} {t('common.minute')}
                  </div>
                </div>
              )}
            </div>
            {j.loading ? (
              <div className="flex items-center gap-2 py-1">
                <div className="w-3 h-3 border-2 border-teal-600 dark:border-teal-400 border-t-transparent rounded-full animate-spin" />
                <span className="text-xs text-warm-500 dark:text-warm-400">
                  {t('common.loading')}
                </span>
              </div>
            ) : displayLegs && displayLegs.length > 0 ? (
              <div>
                {displayLegs.map((leg, i) => (
                  <TimelineLeg key={i} leg={leg} isLast={i === displayLegs.length - 1} />
                ))}
                {showGoogleMapsLink && (
                  <a
                    href={googleMapsUrl(postcode, destination, j.destinationLat, j.destinationLon)}
                    target="_blank"
                    rel="noopener noreferrer"
                    className="mt-2 flex items-center justify-center gap-1.5 w-full text-[11px] font-medium text-teal-600 dark:text-teal-400 hover:text-teal-800 dark:hover:text-teal-300 bg-white dark:bg-warm-900 border border-warm-200 dark:border-warm-700 rounded-md py-1.5 transition-colors"
                  >
                    {t('areaPane.viewOnGoogleMaps')}
                    <svg
                      className="w-3 h-3"
                      viewBox="0 0 12 12"
                      fill="none"
                      stroke="currentColor"
                      strokeWidth="1.5"
                    >
                      <path
                        d="M4.5 1.5H2a.5.5 0 00-.5.5v8a.5.5 0 00.5.5h8a.5.5 0 00.5-.5V7.5M7.5 1.5H10.5V4.5M10.5 1.5L5.5 6.5"
                        strokeLinecap="round"
                        strokeLinejoin="round"
                      />
                    </svg>
                  </a>
                )}
              </div>
            ) : totalMin > 0 ? (
              <div>
                <div className="flex items-center gap-1.5 py-0.5">
                  {!isBestCase && (
                    <WalkingIcon className="w-3.5 h-3.5 text-warm-500 dark:text-warm-400 shrink-0" />
                  )}
                  <span className="text-xs text-warm-600 dark:text-warm-300">
                    {journeyLabels.length > 0 ? journeyLabels.join(' · ') : t('areaPane.walk')} ·{' '}
                    {totalMin} {t('common.minute')}
                  </span>
                </div>
                {showGoogleMapsLink && (
                  <a
                    href={googleMapsUrl(postcode, destination, j.destinationLat, j.destinationLon)}
                    target="_blank"
                    rel="noopener noreferrer"
                    className="mt-2 flex items-center justify-center gap-1.5 w-full text-[11px] font-medium text-teal-600 dark:text-teal-400 hover:text-teal-800 dark:hover:text-teal-300 bg-white dark:bg-warm-900 border border-warm-200 dark:border-warm-700 rounded-md py-1.5 transition-colors"
                  >
                    {t('areaPane.viewOnGoogleMaps')}
                    <svg
                      className="w-3 h-3"
                      viewBox="0 0 12 12"
                      fill="none"
                      stroke="currentColor"
                      strokeWidth="1.5"
                    >
                      <path
                        d="M4.5 1.5H2a.5.5 0 00-.5.5v8a.5.5 0 00.5.5h8a.5.5 0 00.5-.5V7.5M7.5 1.5H10.5V4.5M10.5 1.5L5.5 6.5"
                        strokeLinecap="round"
                        strokeLinejoin="round"
                      />
                    </svg>
                  </a>
                )}
              </div>
            ) : (
              <span className="text-xs text-warm-500 dark:text-warm-400">
                {t('areaPane.noJourneyData')}
              </span>
            )}
          </div>
        );
      })}
    </div>
  );
}
