import type { Station, Track } from '../types/domain';

export type LatLngTuple = readonly [number, number];

export interface NeighborEdge {
  readonly neighborId: string;
  readonly track: Track;
  readonly isForward: boolean;
}

export type TrackAdjacency = Map<string, NeighborEdge[]>;

export interface ComputeRouteParams {
  readonly startId?: string | null;
  readonly endId?: string | null;
  readonly stationById: Map<string, Station>;
  readonly adjacency: TrackAdjacency;
}

export interface RouteComputation {
  readonly stations: Station[] | null;
  readonly tracks: Track[];
  readonly totalLength: number | null;
  readonly error: string | null;
  readonly segments: LatLngTuple[][];
}

export function buildTrackAdjacency(tracks: readonly Track[]): TrackAdjacency {
  const adjacency: TrackAdjacency = new Map();

  const register = (fromId: string, toId: string, track: Track, isForward: boolean) => {
    if (!adjacency.has(fromId)) {
      adjacency.set(fromId, []);
    }
    adjacency.get(fromId)!.push({ neighborId: toId, track, isForward });
  };

  for (const track of tracks) {
    register(track.startStationId, track.endStationId, track, true);
    register(track.endStationId, track.startStationId, track, false);
  }

  return adjacency;
}

export function computeRoute({
  startId,
  endId,
  stationById,
  adjacency,
}: ComputeRouteParams): RouteComputation {
  if (!startId || !endId) {
    return emptyResult();
  }

  if (!stationById.has(startId) || !stationById.has(endId)) {
    return {
      stations: null,
      tracks: [],
      totalLength: null,
      error: 'Selected stations are no longer available.',
      segments: [],
    };
  }

  if (startId === endId) {
    const station = stationById.get(startId);
    return {
      stations: station ? [station] : null,
      tracks: [],
      totalLength: 0,
      error: null,
      segments: [],
    };
  }

  const visited = new Set<string>();
  const queue: string[] = [];
  const parent = new Map<string, { prev: string | null; edge: NeighborEdge | null }>();

  queue.push(startId);
  visited.add(startId);
  parent.set(startId, { prev: null, edge: null });

  while (queue.length > 0) {
    const current = queue.shift()!;
    if (current === endId) {
      break;
    }

    const neighbors = adjacency.get(current) ?? [];
    for (const edge of neighbors) {
      const { neighborId } = edge;
      if (visited.has(neighborId)) {
        continue;
      }
      visited.add(neighborId);
      parent.set(neighborId, { prev: current, edge });
      queue.push(neighborId);
    }
  }

  if (!parent.has(endId)) {
    return {
      stations: null,
      tracks: [],
      totalLength: null,
      error: 'No rail connection found between the selected stations.',
      segments: [],
    };
  }

  const stationPath: string[] = [];
  const trackSequence: Track[] = [];
  const directions: boolean[] = [];
  let cursor: string | null = endId;

  while (cursor) {
    const details = parent.get(cursor);
    if (!details) {
      break;
    }
    stationPath.push(cursor);
    if (details.edge) {
      trackSequence.push(details.edge.track);
      directions.push(details.edge.isForward);
    }
    cursor = details.prev;
  }

  stationPath.reverse();
  trackSequence.reverse();
  directions.reverse();

  const stations = stationPath
    .map((id) => stationById.get(id))
    .filter((station): station is Station => Boolean(station));

  const segments = buildSegments(trackSequence, directions, stationById);
  const totalLength = computeTotalLength(trackSequence, stations);

  return {
    stations,
    tracks: trackSequence,
    totalLength,
    error: null,
    segments,
  };
}

function buildSegments(
  tracks: Track[],
  directions: boolean[],
  stationById: Map<string, Station>
): LatLngTuple[][] {
  const segments: LatLngTuple[][] = [];

  for (let index = 0; index < tracks.length; index += 1) {
    const track = tracks[index];
    const isForward = directions[index] ?? true;
    const coordinates = extractTrackCoordinates(track, stationById);
    if (coordinates.length < 2) {
      continue;
    }
    segments.push(isForward ? coordinates : [...coordinates].reverse());
  }

  return segments;
}

function extractTrackCoordinates(
  track: Track,
  stationById: Map<string, Station>
): LatLngTuple[] {
  if (Array.isArray(track.coordinates) && track.coordinates.length >= 2) {
    return track.coordinates.map((pair) => [pair[0], pair[1]] as LatLngTuple);
  }

  const start = stationById.get(track.startStationId);
  const end = stationById.get(track.endStationId);
  if (!start || !end) {
    return [];
  }

  return [
    [start.latitude, start.longitude],
    [end.latitude, end.longitude],
  ];
}

function computeTotalLength(tracks: Track[], stations: Station[]): number | null {
  if (tracks.length === 0 && stations.length <= 1) {
    return 0;
  }

  const hasTrackLengths = tracks.every(
    (track) =>
      typeof track.lengthMeters === 'number' && Number.isFinite(track.lengthMeters)
  );

  if (hasTrackLengths) {
    return tracks.reduce((total, track) => total + (track.lengthMeters ?? 0), 0);
  }

  if (stations.length < 2) {
    return null;
  }

  let total = 0;
  for (let index = 0; index < stations.length - 1; index += 1) {
    total += haversineDistance(stations[index], stations[index + 1]);
  }
  return total;
}

function haversineDistance(a: Station, b: Station): number {
  const R = 6371_000;
  const toRad = (value: number) => (value * Math.PI) / 180;
  const dLat = toRad(b.latitude - a.latitude);
  const dLon = toRad(b.longitude - a.longitude);
  const lat1 = toRad(a.latitude);
  const lat2 = toRad(b.latitude);

  const sinDLat = Math.sin(dLat / 2);
  const sinDLon = Math.sin(dLon / 2);
  const root = sinDLat * sinDLat + Math.cos(lat1) * Math.cos(lat2) * sinDLon * sinDLon;
  const c = 2 * Math.atan2(Math.sqrt(root), Math.sqrt(1 - root));
  return R * c;
}

function emptyResult(): RouteComputation {
  return {
    stations: null,
    tracks: [],
    totalLength: null,
    error: null,
    segments: [],
  };
}
