import * as L from 'leaflet';
import OpenrouteserviceModule from 'openrouteservice-js';

/**
 * Minimal typings for openrouteservice-js (since official types are incomplete)
 */
interface ORSDirectionsOptions {
  api_key: string;
}

interface ORSStep {
  instruction: string;
  distance: number;
  duration: number;
  way_points: [number, number];
}

interface ORSSegment {
  distance: number;
  duration: number;
  steps: ORSStep[];
}

interface ORSRoute {
  geometry: string;
  segments: ORSSegment[];
  way_points: [number, number];
}

interface ORSResponse {
  routes: ORSRoute[];
}

interface ORSRequestOptions {
  api_key?: string;
  coordinates?: [number, number][];
  [key: string]: unknown;
}

interface ORSDirectionsClient {
  calculate(options: ORSRequestOptions): Promise<ORSResponse>;
}

interface OpenrouteserviceNamespace {
  Directions: new (options: ORSDirectionsOptions) => ORSDirectionsClient;
}

const Openrouteservice = OpenrouteserviceModule as unknown as OpenrouteserviceNamespace;

/**
 * Leaflet waypoint type
 */
interface Waypoint {
  latLng: L.LatLng;
}

interface RouteSummary {
  totalDistance: number;
  totalTime: number;
}

interface Instruction {
  text: string;
  distance: number;
  time: number;
  index: number;
}

export interface AlternativeRoute {
  name: string;
  coordinates: L.LatLng[];
  instructions: Instruction[];
  summary: RouteSummary;
  segments: RouteSummary[];
  inputWaypoints: Waypoint[];
  waypoints: L.LatLng[];
}

type RouteCallback = (
  error: { status: string; message: string } | null,
  routes?: AlternativeRoute[],
) => void;

export class OpenRouteServiceV2 extends L.Class {
  private _apiKey: string;
  private _orsOptions: ORSRequestOptions;

  constructor(apiKey = '', orsOptions: ORSRequestOptions = {}, options?: Record<string, unknown>) {
    super();
    this._apiKey = apiKey;
    this._orsOptions = orsOptions;

    L.Util.setOptions(this, options);
  }

  route(waypoints: Waypoint[], callback: RouteCallback, context?: unknown): this {
    const orsDirections = new Openrouteservice.Directions({
      api_key: this._apiKey,
    });

    const coordinates: [number, number][] = waypoints.map((wp) => [wp.latLng.lng, wp.latLng.lat]);

    this._orsOptions.coordinates = coordinates;

    orsDirections
      .calculate(this._orsOptions)
      .then((json: ORSResponse) => {
        this._routeDone(json, waypoints, callback, context);
      })
      .catch((err: unknown) => {
        console.error(err);
        callback(
          {
            status: 'REQUEST_FAILED',
            message: String(err),
          },
          undefined,
        );
      });

    return this;
  }

  private _routeDone(
    response: ORSResponse,
    inputWaypoints: Waypoint[],
    callback: RouteCallback,
    context?: unknown,
  ): void {
    const alts: AlternativeRoute[] = [];
    const ctx = context ?? callback;

    if (!response.routes) {
      callback.call(ctx, {
        status: 'NO_ROUTES',
        message: 'No routes found in response',
      });
      return;
    }

    response.routes.forEach((path, i) => {
      const coordinates = this._decodePolyline(path.geometry);

      const instructions: Instruction[] = [];
      const waypoints: L.LatLng[] = [];
      const segments: RouteSummary[] = [];
      let totalTime = 0;
      let totalDistance = 0;

      path.segments.forEach((leg) => {
        segments.push({
          totalDistance: leg.distance,
          totalTime: leg.duration,
        });
        totalDistance += leg.distance;
        totalTime += leg.duration;

        leg.steps.forEach((step) => {
          instructions.push(this._convertInstructions(step));

          const wpIndex = path.way_points[1];
          if (coordinates[wpIndex]) {
            waypoints.push(coordinates[wpIndex]);
          }
        });
      });

      alts.push({
        name: `Route: ${i + 1}`,
        coordinates,
        instructions,
        summary: {
          totalDistance,
          totalTime,
        },
        segments,
        inputWaypoints,
        waypoints,
      });
    });

    callback.call(ctx, null, alts);
  }

  private _decodePolyline(encoded: string, includeElevation = false): L.LatLng[] {
    const points: L.LatLng[] = [];

    let index = 0;
    let lat = 0;
    let lng = 0;

    while (index < encoded.length) {
      let result = 0;
      let shift = 0;
      let b: number;

      do {
        b = encoded.charCodeAt(index++) - 63;
        result |= (b & 0x1f) << shift;
        shift += 5;
      } while (b >= 0x20);

      lat += result & 1 ? ~(result >> 1) : result >> 1;

      result = 0;
      shift = 0;

      do {
        b = encoded.charCodeAt(index++) - 63;
        result |= (b & 0x1f) << shift;
        shift += 5;
      } while (b >= 0x20);

      lng += result & 1 ? ~(result >> 1) : result >> 1;

      if (includeElevation) {
        result = 0;
        shift = 0;

        do {
          b = encoded.charCodeAt(index++) - 63;
          result |= (b & 0x1f) << shift;
          shift += 5;
        } while (b >= 0x20);

        // Parse and ignore elevation component when present.
        result = result & 1 ? ~(result >> 1) : result >> 1;
      }

      points.push(L.latLng(lat / 1e5, lng / 1e5));
    }

    return points;
  }

  private _convertInstructions(step: ORSStep): Instruction {
    return {
      text: step.instruction,
      distance: step.distance,
      time: step.duration,
      index: step.way_points[0],
    };
  }
}

/**
 * Leaflet factory function (typed)
 */
export function openrouteserviceV2(
  apiKeyOrOptions: string | ORSRequestOptions,
  orsOptions?: ORSRequestOptions,
  options?: Record<string, unknown>,
): OpenRouteServiceV2 {
  if (typeof apiKeyOrOptions === 'string') {
    return new OpenRouteServiceV2(apiKeyOrOptions, orsOptions ?? {}, options);
  }

  const resolvedApiKey = typeof apiKeyOrOptions.api_key === 'string' ? apiKeyOrOptions.api_key : '';
  return new OpenRouteServiceV2(
    resolvedApiKey,
    apiKeyOrOptions,
    orsOptions as Record<string, unknown>,
  );
}

// Do not mutate the ESM namespace import (`import * as L`) because it is non-extensible
// in production bundles. Consumers should import and use `openrouteserviceV2` directly.
