import repl from 'node:repl';

import fs from 'fs';

import { Query, RangeQuery, Router, StopsIndex, Timetable } from '../router.js';
import type { Stop } from '../stops/stops.js';
import type { RawPickUpDropOffType } from '../timetable/route.js';
import { PickUpDropOffTypes, Route } from '../timetable/route.js';
import { timeFromString, timeToString } from '../timetable/time.js';
import { plotGraphToDotFile } from './utils.js';

export const startRepl = (stopsPath: string, timetablePath: string) => {
  const stopsIndex = StopsIndex.fromData(fs.readFileSync(stopsPath));
  const timetable = Timetable.fromData(fs.readFileSync(timetablePath));
  console.log(`Minotor Transit Router CLI`);
  console.log(
    'Enter your stop (.find) or routing (.route) queries. Type ".exit" to quit.',
  );
  const replServer = repl.start({
    prompt: 'minotor> ',
    ignoreUndefined: true,
  });
  replServer.context.stopFinder = stopsIndex;
  replServer.defineCommand('find', {
    help: 'Find stops by name using .find <query>',
    action(query: string) {
      this.clearBufferedCommand();
      let stops = [];
      const stopBySourceId = stopsIndex.findStopBySourceStopId(query);
      if (stopBySourceId !== undefined) {
        stops.push(stopBySourceId);
      } else if (!isNaN(Number(query))) {
        const stopById = stopsIndex.findStopById(Number(query));
        if (stopById !== undefined) {
          stops.push(stopById);
        }
      } else {
        stops = stopsIndex.findStopsByName(query);
      }
      stops.forEach((stop) => {
        console.log(`${stop.name} (${stop.sourceStopId} - ${stop.id})`);
      });
      this.displayPrompt();
    },
  });
  const routeSyntax =
    '.route from <stop> to <stop> at <HH:mm> [before <HH:mm>] [with <N> transfers] [wait max <N> minutes]';

  replServer.defineCommand('route', {
    help: `Find a route using ${routeSyntax}`,
    action(routeQuery: string) {
      this.clearBufferedCommand();
      const parts = routeQuery.split(' ').filter(Boolean);

      const fromIndex = parts.indexOf('from');
      const toIndex = parts.indexOf('to');
      const atIndex = parts.indexOf('at');
      const beforeIndex = parts.indexOf('before');
      const withIndex = parts.indexOf('with');
      const waitIndex = parts.indexOf('wait');
      const routeClauseIndexes = [beforeIndex, withIndex, waitIndex].filter(
        (index) => index !== -1,
      );

      if (fromIndex === -1 || toIndex === -1 || atIndex === -1) {
        console.log(`Usage: ${routeSyntax}`);
        this.displayPrompt();
        return;
      }

      const fromId = parts.slice(fromIndex + 1, toIndex).join(' ');
      const toId = parts.slice(toIndex + 1, atIndex).join(' ');

      // atTime ends at 'before', 'with', 'wait', or the end of the input.
      const atTimeEnd = Math.min(
        ...routeClauseIndexes.filter((index) => index > atIndex),
        parts.length,
      );
      const atTime = parts.slice(atIndex + 1, atTimeEnd).join(' ');

      // beforeTime is only present when the 'before' keyword appears.
      const beforeTimeEnd = Math.min(
        ...[withIndex, waitIndex].filter(
          (index) => index !== -1 && index > beforeIndex,
        ),
        parts.length,
      );
      const beforeTime =
        beforeIndex !== -1
          ? parts.slice(beforeIndex + 1, beforeTimeEnd).join(' ')
          : undefined;

      const maxTransfers =
        withIndex !== -1 && parts[withIndex + 1] !== undefined
          ? parseInt(parts[withIndex + 1] as string)
          : 4;
      const maxInitialWaitingTime =
        waitIndex !== -1 &&
        parts[waitIndex + 1] === 'max' &&
        parts[waitIndex + 2] !== undefined
          ? Number(parts[waitIndex + 2])
          : undefined;
      const waitUnit = parts[waitIndex + 3];
      const hasInvalidWaitClause =
        waitIndex !== -1 &&
        (parts[waitIndex + 1] !== 'max' ||
          maxInitialWaitingTime === undefined ||
          !Number.isFinite(maxInitialWaitingTime) ||
          maxInitialWaitingTime < 0 ||
          (waitUnit !== 'minute' && waitUnit !== 'minutes'));

      if (!fromId || !toId || !atTime || hasInvalidWaitClause) {
        console.log(`Usage: ${routeSyntax}`);
        this.displayPrompt();
        return;
      }

      const fromStop =
        stopsIndex.findStopBySourceStopId(fromId) ||
        (isNaN(Number(fromId))
          ? undefined
          : stopsIndex.findStopById(Number(fromId))) ||
        stopsIndex.findStopsByName(fromId)[0];
      const toStop =
        stopsIndex.findStopBySourceStopId(toId) ||
        (isNaN(Number(toId))
          ? undefined
          : stopsIndex.findStopById(Number(toId))) ||
        stopsIndex.findStopsByName(toId)[0];

      if (!fromStop) {
        console.log(`No stop found for 'from' ID or name: ${fromId}`);
        this.displayPrompt();
        return;
      }

      if (!toStop) {
        console.log(`No stop found for 'to' ID or name: ${toId}`);
        this.displayPrompt();
        return;
      }

      try {
        const departureTime = timeFromString(atTime);
        const router = new Router(timetable, stopsIndex);
        if (beforeTime !== undefined) {
          const lastDepartureTime = timeFromString(beforeTime);
          const queryBuilder = new RangeQuery.Builder()
            .from(fromStop.id)
            .to(toStop.id)
            .departureTime(departureTime)
            .lastDepartureTime(lastDepartureTime)
            .maxTransfers(maxTransfers);
          if (maxInitialWaitingTime !== undefined) {
            queryBuilder.maxInitialWaitingTime(maxInitialWaitingTime);
          }
          const query = queryBuilder.build();

          const result = router.rangeRoute(query);

          if (result.size === 0) {
            console.log(
              `No journeys found from ${fromStop.name} to ${toStop.name} ` +
                `between ${atTime} and ${beforeTime}.`,
            );
          } else {
            console.log(
              `Found ${result.size} Pareto-optimal journey${result.size === 1 ? '' : 's'} ` +
                `from ${fromStop.name} to ${toStop.name} ` +
                `(window ${atTime}–${beforeTime}):`,
            );
            const routes = result.getRoutes();
            routes.forEach((route, index) => {
              const journeyNumber = index + 1;
              console.log(`\nJourney ${journeyNumber}:`);
              console.log(route.toString());
            });
          }
        } else {
          const queryBuilder = new Query.Builder()
            .from(fromStop.id)
            .to(toStop.id)
            .departureTime(departureTime)
            .maxTransfers(maxTransfers);
          if (maxInitialWaitingTime !== undefined) {
            queryBuilder.maxInitialWaitingTime(maxInitialWaitingTime);
          }
          const query = queryBuilder.build();

          const result = router.route(query);
          const arrivalTime = result.arrivalAt(toStop.id);

          if (arrivalTime === undefined) {
            console.log(`Destination not reachable`);
          } else {
            const transfers = Math.max(0, arrivalTime.legNumber - 1);
            console.log(
              `Arriving to ${toStop.name} at ${timeToString(arrivalTime.arrival)} ` +
                `with ${transfers} transfer${transfers === 1 ? '' : 's'} ` +
                `from ${fromStop.name}.`,
            );
          }

          const bestRoute = result.bestRoute(toStop.id);
          if (bestRoute) {
            console.log(`Found route from ${fromStop.name} to ${toStop.name}:`);
            console.log(bestRoute.toString());
          } else {
            console.log('No route found');
          }
        }
      } catch (error) {
        console.log('Error querying route:', error);
      }

      this.displayPrompt();
    },
  });
  replServer.defineCommand('plot', {
    help: 'Plot a network graph using .plot from <stationId> to <stationId> at <HH:mm> [with <N> transfers] [to <graph.dot>]',
    action(routeQuery: string) {
      this.clearBufferedCommand();
      const parts = routeQuery.split(' ').filter(Boolean);
      const withTransfersIndex = parts.indexOf('with');
      const maxTransfers =
        withTransfersIndex !== -1 && parts[withTransfersIndex + 1] !== undefined
          ? parseInt(parts[withTransfersIndex + 1] as string)
          : 1;
      const atTimeIndex = parts.indexOf('at');
      const atTime = parts
        .slice(
          atTimeIndex + 1,
          withTransfersIndex === -1
            ? parts.indexOf('to', atTimeIndex) >= 0
              ? parts.indexOf('to', atTimeIndex)
              : parts.length
            : withTransfersIndex,
        )
        .join(' ');
      const fromIndex = parts.indexOf('from');
      const toIndex = parts.indexOf('to');
      const toFileIndex =
        toIndex !== -1 && parts.indexOf('to', toIndex + 1) !== -1
          ? parts.indexOf('to', toIndex + 1)
          : -1;
      const fromId = parts.slice(fromIndex + 1, toIndex).join(' ');
      const toId = parts.slice(toIndex + 1, atTimeIndex).join(' ');
      const outputFile =
        toFileIndex !== -1
          ? parts.slice(toFileIndex + 1).join(' ')
          : `${fromId.replace(/ /g, '')}-${toId.replace(/ /g, '')}-${atTime.replace(/:/g, '')}.dot`;

      if (!fromId || !toId || !atTime || isNaN(maxTransfers)) {
        console.log(
          'Usage: .plot from <stationId> to <stationId> at <HH:mm> [with <N> transfers] [to <graph.dot>]',
        );
        this.displayPrompt();
        return;
      }

      const fromStop =
        stopsIndex.findStopBySourceStopId(fromId) ||
        stopsIndex.findStopsByName(fromId)[0];
      const toStop =
        stopsIndex.findStopBySourceStopId(toId) ||
        stopsIndex.findStopsByName(toId)[0];

      if (!fromStop) {
        console.log(`No stop found for 'from' ID or name: ${fromId}`);
        this.displayPrompt();
        return;
      }

      if (!toStop) {
        console.log(`No stop found for 'to' ID or name: ${toId}`);
        this.displayPrompt();
        return;
      }

      const departureTime = timeFromString(atTime);
      try {
        const query = new Query.Builder()
          .from(fromStop.id)
          .to(toStop.id)
          .departureTime(departureTime)
          .maxTransfers(maxTransfers)
          .build();

        const router = new Router(timetable, stopsIndex);

        const result = router.route(query);
        plotGraphToDotFile(result, outputFile);
      } catch (error) {
        console.log('Error plotting route:', error);
      }

      this.displayPrompt();
    },
  });

  const formatPickupDropoffType = (type: RawPickUpDropOffType): string => {
    switch (type) {
      case PickUpDropOffTypes.REGULAR:
        return 'R';
      case PickUpDropOffTypes.NOT_AVAILABLE:
        return 'N';
      case PickUpDropOffTypes.MUST_PHONE_AGENCY:
        return 'A';
      case PickUpDropOffTypes.MUST_COORDINATE_WITH_DRIVER:
        return 'D';
      default:
        return '?';
    }
  };

  replServer.defineCommand('inspect', {
    help: 'Inspect a route or stop using .inspect route <routeId> or .inspect stop <stopId>',
    action(inspectQuery: string) {
      this.clearBufferedCommand();

      const parts = inspectQuery.trim().split(' ');
      if (parts.length !== 2) {
        console.log(
          'Usage: .inspect route <routeId> or .inspect stop <stopId>',
        );
        this.displayPrompt();
        return;
      }

      const [type, idStr] = parts;
      if (type !== 'route' && type !== 'stop') {
        console.log(
          'Usage: .inspect route <routeId> or .inspect stop <stopId>',
        );
        this.displayPrompt();
        return;
      }

      const inspectRoute = (routeIdStr: string) => {
        const routeId = parseInt(routeIdStr.trim());
        if (isNaN(routeId)) {
          console.log('Usage: .inspect route <routeId>');
          return;
        }

        const route = timetable.getRoute(routeId);
        if (!route) {
          console.log(`Route ${routeId} not found`);
          return;
        }

        const serviceRouteInfo = timetable.getServiceRouteInfo(route);
        const routeName = serviceRouteInfo.name;
        const routeType = serviceRouteInfo.type;

        console.log(`\n=== Route ${routeId} ===`);
        console.log(`Service Route: ${routeName}`);
        console.log(`Type: ${routeType}`);
        console.log(`Number of stops: ${route.getNbStops()}`);
        console.log(`Number of trips: ${route.getNbTrips()}`);

        console.log('\n--- Stops ---');
        for (let i = 0; i < route.stops.length; i++) {
          const stopId = route.stopId(i);
          // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
          const stop = stopsIndex.findStopById(stopId)!;
          const platform = stop.platform ? ` (Pl. ${stop.platform})` : '';
          console.log(
            `${i + 1}. ${stop.name}${platform} (${stopId}, ${stop.sourceStopId})`,
          );
        }

        console.log('\n--- Trips ---');
        for (let tripIndex = 0; tripIndex < route.getNbTrips(); tripIndex++) {
          console.log(`\nTrip ${tripIndex}:`);
          for (let stopIndex = 0; stopIndex < route.stops.length; stopIndex++) {
            const stopId = route.stopId(stopIndex);
            // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
            const stop = stopsIndex.findStopById(stopId)!;

            const departure = route.departureFrom(stopIndex, tripIndex);
            const arrival = route.arrivalAt(stopIndex, tripIndex);
            const pickupType = route.pickUpTypeFrom(stopIndex, tripIndex);
            const dropOffType = route.dropOffTypeAt(stopIndex, tripIndex);

            const pickupStr = formatPickupDropoffType(pickupType);
            const dropOffStr = formatPickupDropoffType(dropOffType);

            console.log(
              `  ${stopIndex + 1}. ${stop.name}: arr ${timeToString(arrival)} (${pickupStr}) → dep ${timeToString(departure)} (${dropOffStr})`,
            );
          }
        }

        console.log();
      };

      const inspectStop = (stopIdStr: string) => {
        let stop: Stop | undefined;
        const stopBySourceId = stopsIndex.findStopBySourceStopId(stopIdStr);
        if (stopBySourceId !== undefined) {
          stop = stopBySourceId;
        } else if (!isNaN(Number(stopIdStr))) {
          const stopById = stopsIndex.findStopById(Number(stopIdStr));
          if (stopById !== undefined) {
            stop = stopById;
          }
        } else {
          const stops = stopsIndex.findStopsByName(stopIdStr);
          if (stops.length > 0) {
            // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
            stop = stops[0]!;
          }
        }

        if (!stop) {
          console.log(`Stop not found: ${stopIdStr}`);
          return;
        }

        console.log(`\n=== Stop ${stop.id} ===`);
        console.log(`Name: ${stop.name}`);
        if (stop.platform) {
          console.log(`Platform: ${stop.platform}`);
        }
        console.log(`Source ID: ${stop.sourceStopId}`);

        const routes: Route[] = timetable.routesPassingThrough(stop.id);
        console.log(`Number of routes: ${routes.length}`);

        const equivalentStops = stopsIndex
          .equivalentStops(stop.id)
          .filter((equivStop) => equivStop.id !== stop.id);
        console.log(`Number of equivalent stops: ${equivalentStops.length}`);

        if (equivalentStops.length > 0) {
          console.log('\n--- Equivalent Stops ---');
          equivalentStops.forEach((equivStop, index) => {
            const platform = equivStop.platform
              ? ` (Pl. ${equivStop.platform})`
              : '';
            console.log(
              `${index + 1}. ${equivStop.name}${platform} (${equivStop.id}, ${equivStop.sourceStopId})`,
            );
          });
        }

        if (routes.length > 0) {
          console.log('\n--- Routes ---');
          routes.forEach((route, index) => {
            const serviceRouteInfo = timetable.getServiceRouteInfo(route);
            console.log(
              `${index + 1}. Route ${route.id}: ${serviceRouteInfo.name} (${serviceRouteInfo.type})`,
            );
          });
        }

        const transfers = timetable.getTransfers(stop.id);
        console.log(`Number of transfers: ${transfers.length}`);

        if (transfers.length > 0) {
          console.log('\n--- Transfers ---');
          transfers.forEach((transfer, index) => {
            const destStop = stopsIndex.findStopById(transfer.destination);
            const platform = destStop?.platform
              ? ` (Pl. ${destStop.platform})`
              : '';
            const minTime = transfer.minTransferTime
              ? ` (min: ${transfer.minTransferTime}min)`
              : '';
            console.log(
              `${index + 1}. ${transfer.type} to ${destStop?.name ?? 'Unknown'}${platform} (${transfer.destination}, ${destStop?.sourceStopId ?? 'N/A'})${minTime}`,
            );
          });
        }

        let totalContinuations = 0;
        console.log('\n--- Trip Continuations ---');

        routes.forEach((route: Route) => {
          const serviceRouteInfo = timetable.getServiceRouteInfo(route);
          const stopIndices = route.stopRouteIndices(stop.id);

          for (let tripIndex = 0; tripIndex < route.getNbTrips(); tripIndex++) {
            for (const stopIndex of stopIndices) {
              const continuations = timetable.getContinuousTrips(
                stopIndex,
                route.id,
                tripIndex,
              );

              for (const continuation of continuations) {
                totalContinuations++;
                // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
                const destRoute = timetable.getRoute(continuation.routeId)!;
                const destStopId = destRoute.stopId(continuation.stopIndex);
                // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
                const destStop = stopsIndex.findStopById(destStopId)!;
                const destPlatform = destStop.platform
                  ? ` (Pl. ${destStop.platform})`
                  : '';

                const destServiceRouteInfo =
                  timetable.getServiceRouteInfo(destRoute);

                const originTime = route.departureFrom(stopIndex, tripIndex);
                const continuationTime = destRoute.departureFrom(
                  continuation.stopIndex,
                  continuation.tripIndex,
                );

                console.log(
                  `${totalContinuations}. From Route ${route.id} (${serviceRouteInfo.name}) Trip ${tripIndex} at ${timeToString(originTime)} → ` +
                    `Route ${continuation.routeId} (${destServiceRouteInfo.name}) Trip ${continuation.tripIndex} at ${timeToString(continuationTime)} ` +
                    `at ${destStop.name}${destPlatform} (${destStopId}, ${destStop.sourceStopId})`,
                );
              }
            }
          }
        });

        if (totalContinuations === 0) {
          console.log('No trip continuations found.');
        } else {
          console.log(`\nTotal trip continuations: ${totalContinuations}`);
        }
        let totalGuaranteedTransfers = 0;
        console.log('\n--- Guaranteed Trip Transfers ---');
        routes.forEach((route: Route) => {
          const serviceRouteInfo = timetable.getServiceRouteInfo(route);
          const stopIndices = route.stopRouteIndices(stop.id);

          for (let tripIndex = 0; tripIndex < route.getNbTrips(); tripIndex++) {
            for (const stopIndex of stopIndices) {
              const guaranteedTransfers = timetable.getGuaranteedTripTransfers(
                stopIndex,
                route.id,
                tripIndex,
              );

              for (const guaranteedTrip of guaranteedTransfers) {
                totalGuaranteedTransfers++;
                // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
                const destRoute = timetable.getRoute(guaranteedTrip.routeId)!;

                const destServiceRouteInfo =
                  timetable.getServiceRouteInfo(destRoute);

                const originTime = route.arrivalAt(stopIndex, tripIndex);
                const destinationTime = destRoute.departureFrom(
                  guaranteedTrip.stopIndex,
                  guaranteedTrip.tripIndex,
                );

                console.log(
                  `${totalGuaranteedTransfers}. From Route ${route.id} (${serviceRouteInfo.name}) Trip ${tripIndex} at ${timeToString(originTime)} → ` +
                    `Route ${guaranteedTrip.routeId} (${destServiceRouteInfo.name}) Trip ${guaranteedTrip.tripIndex} at ${timeToString(destinationTime)} `,
                );
                console.log(stopIndex, route.id, tripIndex);
              }
            }
          }
        });

        if (totalGuaranteedTransfers === 0) {
          console.log('No guaranteed trip transfers found.');
        } else {
          console.log(
            `\nTotal guaranteed trip transfers: ${totalGuaranteedTransfers}`,
          );
        }
        console.log();
      };

      if (type === 'route') {
        inspectRoute(idStr ?? '');
      } else {
        inspectStop(idStr ?? '');
      }

      this.displayPrompt();
    },
  });
};
