import { html, type TemplateResult } from '@design.estate/dees-element';
import maplibregl from 'maplibre-gl';
import { VoiceSynthesisManager, type IVoiceConfig } from './geo-map.voice.js';
import type { IOSRMRoute, IOSRMStep } from './geo-map.navigation.js';

// ─── Types ────────────────────────────────────────────────────────────────────

export interface IGPSPosition {
  lng: number;
  lat: number;
  heading?: number;     // 0-360 degrees, 0 = North
  speed?: number;       // meters/second
  accuracy?: number;    // meters
  timestamp: Date;
}

export interface INavigationGuideState {
  isNavigating: boolean;
  currentStepIndex: number;
  distanceToNextManeuver: number;  // meters
  distanceRemaining: number;        // meters
  timeRemaining: number;            // seconds
  currentPosition: IGPSPosition | null;
  isOffRoute: boolean;
  hasArrived: boolean;
}

export type TGuidanceEventType =
  | 'approach-maneuver'   // "In 200m, turn left"
  | 'execute-maneuver'    // "Turn left now"
  | 'step-change'         // Moved to next step
  | 'off-route'           // User deviated from route
  | 'arrived'             // Reached destination
  | 'position-updated';   // Position was updated

export interface IGuidanceEvent {
  type: TGuidanceEventType;
  position: IGPSPosition;
  stepIndex: number;
  step?: IOSRMStep;
  distanceToManeuver: number;
  instruction?: string;
}

export interface INavigationGuideCallbacks {
  onGuidanceEvent: (event: IGuidanceEvent) => void;
  onRequestUpdate: () => void;
  getMap: () => maplibregl.Map | null;
}

// ─── Distance Thresholds ──────────────────────────────────────────────────────

const GUIDANCE_THRESHOLDS = {
  FAR: 500,        // "In 500 meters..."
  APPROACHING: 200, // "In 200 meters..."
  NEAR: 50,        // "Turn left ahead"
  AT_MANEUVER: 20, // "Turn left now"
  OFF_ROUTE: 50,   // Distance from route line to trigger off-route
  ARRIVED: 30,     // Distance from destination to trigger arrival
};

// ─── Utility Functions ────────────────────────────────────────────────────────

/**
 * Calculate distance between two points using Haversine formula
 */
function haversineDistance(
  lat1: number, lng1: number,
  lat2: number, lng2: number
): number {
  const R = 6371000; // Earth's radius in meters
  const dLat = (lat2 - lat1) * Math.PI / 180;
  const dLng = (lng2 - lng1) * Math.PI / 180;
  const a =
    Math.sin(dLat / 2) * Math.sin(dLat / 2) +
    Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
    Math.sin(dLng / 2) * Math.sin(dLng / 2);
  const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
  return R * c;
}

/**
 * Calculate distance from a point to a line segment
 */
function pointToLineDistance(
  px: number, py: number,
  x1: number, y1: number,
  x2: number, y2: number
): number {
  const A = px - x1;
  const B = py - y1;
  const C = x2 - x1;
  const D = y2 - y1;

  const dot = A * C + B * D;
  const lenSq = C * C + D * D;
  let param = -1;

  if (lenSq !== 0) {
    param = dot / lenSq;
  }

  let xx: number, yy: number;

  if (param < 0) {
    xx = x1;
    yy = y1;
  } else if (param > 1) {
    xx = x2;
    yy = y2;
  } else {
    xx = x1 + param * C;
    yy = y1 + param * D;
  }

  return haversineDistance(py, px, yy, xx);
}

/**
 * Find the minimum distance from a point to a polyline
 */
function pointToPolylineDistance(
  px: number, py: number,
  coordinates: [number, number][]
): number {
  let minDist = Infinity;

  for (let i = 0; i < coordinates.length - 1; i++) {
    const [x1, y1] = coordinates[i];
    const [x2, y2] = coordinates[i + 1];
    const dist = pointToLineDistance(px, py, x1, y1, x2, y2);
    if (dist < minDist) {
      minDist = dist;
    }
  }

  return minDist;
}

/**
 * Format distance for voice instructions
 */
function formatDistanceForVoice(meters: number): string {
  if (meters < 100) {
    return `${Math.round(meters / 10) * 10} meters`;
  } else if (meters < 1000) {
    return `${Math.round(meters / 50) * 50} meters`;
  } else {
    const km = meters / 1000;
    if (km < 10) {
      return `${km.toFixed(1)} kilometers`;
    }
    return `${Math.round(km)} kilometers`;
  }
}

/**
 * Format maneuver type for voice
 */
function formatManeuverForVoice(type: string, modifier?: string): string {
  switch (type) {
    case 'turn':
      if (modifier === 'left') return 'turn left';
      if (modifier === 'right') return 'turn right';
      if (modifier === 'slight left') return 'bear left';
      if (modifier === 'slight right') return 'bear right';
      if (modifier === 'sharp left') return 'take a sharp left';
      if (modifier === 'sharp right') return 'take a sharp right';
      return `turn ${modifier || ''}`.trim();
    case 'depart':
      return 'head forward';
    case 'arrive':
      return 'arrive at your destination';
    case 'merge':
      return `merge ${modifier || ''}`.trim();
    case 'fork':
      if (modifier === 'left') return 'keep left at the fork';
      if (modifier === 'right') return 'keep right at the fork';
      return 'take the fork';
    case 'roundabout':
    case 'rotary':
      return 'enter the roundabout';
    case 'continue':
      return 'continue straight';
    case 'end of road':
      return `at the end of the road, turn ${modifier || 'around'}`;
    case 'new name':
      return 'continue straight';
    default:
      return type;
  }
}

// ─── Camera Configuration ────────────────────────────────────────────────────

export interface INavigationCameraConfig {
  followPosition: boolean;   // Camera follows GPS position (default: true)
  followBearing: boolean;    // Camera rotates with heading (default: true)
  pitch: number;             // 3D tilt angle in degrees (default: 60)
  zoom: number;              // Navigation zoom level (default: 17)
}

// ─── NavigationGuideController ────────────────────────────────────────────────

/**
 * Controller for real-time GPS navigation guidance
 * Tracks position, step progression, and fires guidance events
 */
export class NavigationGuideController {
  // State
  public state: INavigationGuideState = {
    isNavigating: false,
    currentStepIndex: 0,
    distanceToNextManeuver: 0,
    distanceRemaining: 0,
    timeRemaining: 0,
    currentPosition: null,
    isOffRoute: false,
    hasArrived: false,
  };

  // Route data
  private route: IOSRMRoute | null = null;
  private allSteps: IOSRMStep[] = [];

  // Voice synthesis
  private voiceManager: VoiceSynthesisManager;

  // Position marker
  private positionMarker: maplibregl.Marker | null = null;

  // Track last position time for smooth animation timing
  private lastPositionTimestamp: number = 0;

  // Camera configuration for navigation mode
  private cameraConfig: INavigationCameraConfig = {
    followPosition: true,
    followBearing: true,
    pitch: 60,
    zoom: 17,
  };

  // Announcement tracking to avoid repetition
  private lastAnnouncedThreshold: number | null = null;
  private lastAnnouncedStepIndex: number = -1;

  // Callbacks
  private callbacks: INavigationGuideCallbacks;

  constructor(callbacks: INavigationGuideCallbacks, voiceConfig?: Partial<IVoiceConfig>) {
    this.callbacks = callbacks;
    this.voiceManager = new VoiceSynthesisManager(voiceConfig);
  }

  // ─── Configuration ──────────────────────────────────────────────────────────

  /**
   * Configure voice settings
   */
  public configureVoice(config: Partial<IVoiceConfig>): void {
    this.voiceManager.configure(config);
  }

  /**
   * Enable/disable voice
   */
  public setVoiceEnabled(enabled: boolean): void {
    if (enabled) {
      this.voiceManager.enable();
    } else {
      this.voiceManager.disable();
    }
  }

  /**
   * Check if voice is enabled
   */
  public isVoiceEnabled(): boolean {
    return this.voiceManager.isEnabled();
  }

  /**
   * Get voice manager for direct access
   */
  public getVoiceManager(): VoiceSynthesisManager {
    return this.voiceManager;
  }

  // ─── Camera Configuration ──────────────────────────────────────────────────

  /**
   * Enable/disable camera following the GPS position
   */
  public setFollowPosition(enabled: boolean): void {
    this.cameraConfig.followPosition = enabled;
  }

  /**
   * Check if camera is following position
   */
  public isFollowingPosition(): boolean {
    return this.cameraConfig.followPosition;
  }

  /**
   * Enable/disable camera rotating with heading
   */
  public setFollowBearing(enabled: boolean): void {
    this.cameraConfig.followBearing = enabled;
  }

  /**
   * Check if camera is following bearing
   */
  public isFollowingBearing(): boolean {
    return this.cameraConfig.followBearing;
  }

  /**
   * Set the navigation camera pitch (3D tilt angle)
   * @param pitch - Angle in degrees (0 = flat, 60 = tilted)
   */
  public setPitch(pitch: number): void {
    this.cameraConfig.pitch = Math.max(0, Math.min(85, pitch));
  }

  /**
   * Get current pitch setting
   */
  public getPitch(): number {
    return this.cameraConfig.pitch;
  }

  /**
   * Set the navigation zoom level
   * @param zoom - Zoom level (typically 15-19 for street-level navigation)
   */
  public setZoom(zoom: number): void {
    this.cameraConfig.zoom = Math.max(1, Math.min(22, zoom));
  }

  /**
   * Get current zoom setting
   */
  public getZoom(): number {
    return this.cameraConfig.zoom;
  }

  /**
   * Get the full camera configuration
   */
  public getCameraConfig(): INavigationCameraConfig {
    return { ...this.cameraConfig };
  }

  /**
   * Set the full camera configuration
   */
  public setCameraConfig(config: Partial<INavigationCameraConfig>): void {
    if (config.followPosition !== undefined) {
      this.cameraConfig.followPosition = config.followPosition;
    }
    if (config.followBearing !== undefined) {
      this.cameraConfig.followBearing = config.followBearing;
    }
    if (config.pitch !== undefined) {
      this.cameraConfig.pitch = Math.max(0, Math.min(85, config.pitch));
    }
    if (config.zoom !== undefined) {
      this.cameraConfig.zoom = Math.max(1, Math.min(22, config.zoom));
    }
  }

  // ─── Navigation Lifecycle ───────────────────────────────────────────────────

  /**
   * Start navigation guidance for a route
   */
  public startGuidance(route: IOSRMRoute): void {
    this.route = route;
    this.allSteps = route.legs.flatMap(leg => leg.steps);

    this.state = {
      isNavigating: true,
      currentStepIndex: 0,
      distanceToNextManeuver: this.allSteps[0]?.distance ?? 0,
      distanceRemaining: route.distance,
      timeRemaining: route.duration,
      currentPosition: null,
      isOffRoute: false,
      hasArrived: false,
    };

    this.lastAnnouncedThreshold = null;
    this.lastAnnouncedStepIndex = -1;

    this.callbacks.onRequestUpdate();
  }

  /**
   * Stop navigation guidance
   */
  public stopGuidance(): void {
    this.state = {
      isNavigating: false,
      currentStepIndex: 0,
      distanceToNextManeuver: 0,
      distanceRemaining: 0,
      timeRemaining: 0,
      currentPosition: null,
      isOffRoute: false,
      hasArrived: false,
    };

    this.route = null;
    this.allSteps = [];
    this.lastAnnouncedThreshold = null;
    this.lastAnnouncedStepIndex = -1;
    this.lastPositionTimestamp = 0;

    this.voiceManager.stop();
    this.removePositionMarker();

    this.callbacks.onRequestUpdate();
  }

  /**
   * Check if currently navigating
   */
  public isNavigating(): boolean {
    return this.state.isNavigating;
  }

  // ─── Position Updates ───────────────────────────────────────────────────────

  /**
   * Update position from external GPS source
   */
  public updatePosition(position: IGPSPosition): void {
    if (!this.state.isNavigating || !this.route) return;

    this.state.currentPosition = position;

    // Update position marker
    this.updatePositionMarker(position);

    // Check if off-route
    const distanceFromRoute = this.calculateDistanceFromRoute(position);
    const wasOffRoute = this.state.isOffRoute;
    this.state.isOffRoute = distanceFromRoute > GUIDANCE_THRESHOLDS.OFF_ROUTE;

    if (this.state.isOffRoute && !wasOffRoute) {
      this.handleOffRoute(position);
      return;
    }

    // Find current step and calculate distances
    this.updateStepProgress(position);

    // Check for arrival
    this.checkArrival(position);

    // Fire position updated event
    this.emitGuidanceEvent('position-updated', position);

    this.callbacks.onRequestUpdate();
  }

  /**
   * Set position (convenience method for external use)
   */
  public setPosition(lng: number, lat: number, heading?: number, speed?: number): void {
    this.updatePosition({
      lng,
      lat,
      heading,
      speed,
      timestamp: new Date(),
    });
  }

  // ─── Step Progress Tracking ─────────────────────────────────────────────────

  /**
   * Update step progress based on current position
   */
  private updateStepProgress(position: IGPSPosition): void {
    if (this.allSteps.length === 0) return;

    // Find the closest step maneuver point
    let closestStepIndex = this.state.currentStepIndex;
    let minDistance = Infinity;

    // Look ahead a few steps (don't go backwards)
    const searchEnd = Math.min(this.state.currentStepIndex + 3, this.allSteps.length);

    for (let i = this.state.currentStepIndex; i < searchEnd; i++) {
      const step = this.allSteps[i];
      const [maneuverLng, maneuverLat] = step.maneuver.location;
      const distance = haversineDistance(position.lat, position.lng, maneuverLat, maneuverLng);

      if (distance < minDistance) {
        minDistance = distance;
        closestStepIndex = i;
      }
    }

    // Check if we've passed the current maneuver (close to it)
    const currentStep = this.allSteps[this.state.currentStepIndex];
    if (currentStep) {
      const [maneuverLng, maneuverLat] = currentStep.maneuver.location;
      const distanceToManeuver = haversineDistance(position.lat, position.lng, maneuverLat, maneuverLng);

      // If we're very close to the maneuver or past it, advance to next step
      if (distanceToManeuver < GUIDANCE_THRESHOLDS.AT_MANEUVER &&
          this.state.currentStepIndex < this.allSteps.length - 1) {
        this.advanceToNextStep(position);
        return;
      }

      // Update distance to maneuver
      this.state.distanceToNextManeuver = distanceToManeuver;

      // Check guidance thresholds
      this.checkGuidanceThresholds(position, distanceToManeuver, currentStep);
    }

    // Calculate remaining distance and time
    this.calculateRemaining(position);
  }

  /**
   * Advance to the next step
   */
  private advanceToNextStep(position: IGPSPosition): void {
    const previousStepIndex = this.state.currentStepIndex;
    this.state.currentStepIndex++;

    if (this.state.currentStepIndex >= this.allSteps.length) {
      // Reached end of route
      return;
    }

    const newStep = this.allSteps[this.state.currentStepIndex];

    // Reset announcement tracking for new step
    this.lastAnnouncedThreshold = null;

    // Emit step change event
    this.emitGuidanceEvent('step-change', position, newStep);

    // Announce the new step if it's not just departing
    if (this.lastAnnouncedStepIndex !== previousStepIndex) {
      this.lastAnnouncedStepIndex = previousStepIndex;
    }
  }

  /**
   * Check guidance thresholds and announce instructions
   */
  private checkGuidanceThresholds(position: IGPSPosition, distance: number, step: IOSRMStep): void {
    // Determine current threshold zone
    let currentThreshold: number | null = null;

    if (distance <= GUIDANCE_THRESHOLDS.AT_MANEUVER) {
      currentThreshold = GUIDANCE_THRESHOLDS.AT_MANEUVER;
    } else if (distance <= GUIDANCE_THRESHOLDS.NEAR) {
      currentThreshold = GUIDANCE_THRESHOLDS.NEAR;
    } else if (distance <= GUIDANCE_THRESHOLDS.APPROACHING) {
      currentThreshold = GUIDANCE_THRESHOLDS.APPROACHING;
    } else if (distance <= GUIDANCE_THRESHOLDS.FAR) {
      currentThreshold = GUIDANCE_THRESHOLDS.FAR;
    }

    // Only announce if we've entered a new threshold zone
    if (currentThreshold !== null && currentThreshold !== this.lastAnnouncedThreshold) {
      this.lastAnnouncedThreshold = currentThreshold;
      this.announceManeuver(position, distance, step, currentThreshold);
    }
  }

  /**
   * Announce maneuver based on threshold
   */
  private announceManeuver(position: IGPSPosition, distance: number, step: IOSRMStep, threshold: number): void {
    const maneuver = formatManeuverForVoice(step.maneuver.type, step.maneuver.modifier);
    const streetName = step.name !== '' ? step.name : undefined;

    if (threshold === GUIDANCE_THRESHOLDS.AT_MANEUVER) {
      // Execute maneuver
      this.voiceManager.speakManeuver(maneuver, true);
      this.emitGuidanceEvent('execute-maneuver', position, step, maneuver);
    } else if (threshold === GUIDANCE_THRESHOLDS.NEAR) {
      // "Turn left ahead"
      const instruction = `${maneuver} ahead`;
      this.voiceManager.speak(instruction);
      this.emitGuidanceEvent('approach-maneuver', position, step, instruction);
    } else {
      // Approach - "In 200 meters, turn left"
      const distanceStr = formatDistanceForVoice(distance);
      this.voiceManager.speakApproach(distanceStr, maneuver, streetName);
      // Use "on" for continue, "onto" for turns/merges
      const preposition = maneuver.startsWith('continue') ? 'on' : 'onto';
      const instruction = `In ${distanceStr}, ${maneuver}${streetName ? ` ${preposition} ${streetName}` : ''}`;
      this.emitGuidanceEvent('approach-maneuver', position, step, instruction);
    }
  }

  // ─── Route Calculations ─────────────────────────────────────────────────────

  /**
   * Calculate distance from current position to route line
   */
  private calculateDistanceFromRoute(position: IGPSPosition): number {
    if (!this.route) return 0;

    const coords = this.route.geometry.coordinates as [number, number][];
    return pointToPolylineDistance(position.lng, position.lat, coords);
  }

  /**
   * Calculate remaining distance and time
   */
  private calculateRemaining(position: IGPSPosition): void {
    if (!this.route || this.allSteps.length === 0) return;

    // Sum distance/duration of remaining steps
    let remainingDistance = 0;
    let remainingDuration = 0;

    for (let i = this.state.currentStepIndex; i < this.allSteps.length; i++) {
      remainingDistance += this.allSteps[i].distance;
      remainingDuration += this.allSteps[i].duration;
    }

    // Subtract distance already traveled in current step
    if (this.state.currentStepIndex < this.allSteps.length) {
      const currentStep = this.allSteps[this.state.currentStepIndex];
      const stepProgress = 1 - (this.state.distanceToNextManeuver / currentStep.distance);
      const progressDistance = currentStep.distance * Math.max(0, Math.min(1, stepProgress));
      remainingDistance -= progressDistance;
    }

    this.state.distanceRemaining = Math.max(0, remainingDistance);
    this.state.timeRemaining = Math.max(0, remainingDuration);
  }

  /**
   * Check if user has arrived at destination
   */
  private checkArrival(position: IGPSPosition): void {
    if (!this.route || this.state.hasArrived) return;

    // Get the last coordinate of the route (destination)
    const coords = this.route.geometry.coordinates;
    const destination = coords[coords.length - 1] as [number, number];

    const distanceToDestination = haversineDistance(
      position.lat, position.lng,
      destination[1], destination[0]
    );

    if (distanceToDestination <= GUIDANCE_THRESHOLDS.ARRIVED) {
      this.state.hasArrived = true;
      this.voiceManager.speakArrival();
      this.emitGuidanceEvent('arrived', position);
    }
  }

  /**
   * Handle off-route scenario
   */
  private handleOffRoute(position: IGPSPosition): void {
    this.voiceManager.speakOffRoute();
    this.emitGuidanceEvent('off-route', position);
  }

  // ─── Position Marker ────────────────────────────────────────────────────────

  /**
   * Update position marker on map and move camera to follow
   */
  private updatePositionMarker(position: IGPSPosition): void {
    const map = this.callbacks.getMap();
    if (!map) return;

    if (!this.positionMarker) {
      // Create marker element with inline styles (Shadow DOM fix)
      // MapLibre markers are added to the light DOM, so CSS classes from
      // Shadow DOM won't apply. Using inline styles ensures proper styling.
      const el = document.createElement('div');
      el.style.cssText = 'width: 32px; height: 32px; cursor: default; filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3));';
      el.innerHTML = this.createPositionMarkerSVG();

      this.positionMarker = new maplibregl.Marker({
        element: el,
        anchor: 'center',
        rotationAlignment: 'map',
        pitchAlignment: 'viewport',
      })
        .setLngLat([position.lng, position.lat])
        .addTo(map);
    } else {
      this.positionMarker.setLngLat([position.lng, position.lat]);
    }

    // Apply rotation via MapLibre's marker rotation (correct anchor handling)
    if (position.heading !== undefined) {
      this.positionMarker.setRotation(position.heading);
    }

    // Calculate animation duration based on time since last update
    // This creates smooth, continuous animation that matches the GPS update interval
    const now = Date.now();
    const timeSinceLastUpdate = this.lastPositionTimestamp > 0
      ? now - this.lastPositionTimestamp
      : 1000;
    this.lastPositionTimestamp = now;

    // Clamp duration: min 100ms (prevents jarring), max 2000ms (prevents lag)
    const animationDuration = Math.max(100, Math.min(2000, timeSinceLastUpdate));

    // Move camera to follow position
    if (this.cameraConfig.followPosition) {
      const bearing = this.cameraConfig.followBearing && position.heading !== undefined
        ? position.heading
        : map.getBearing();

      map.easeTo({
        center: [position.lng, position.lat],
        bearing,
        pitch: this.cameraConfig.pitch,
        zoom: this.cameraConfig.zoom,
        duration: animationDuration,
      });
    }
  }

  /**
   * Create SVG for position marker with heading indicator
   * Note: Rotation is handled by MapLibre's marker.setRotation() for correct anchor handling
   */
  private createPositionMarkerSVG(): string {
    return `
      <svg width="32" height="32" viewBox="0 0 32 32">
        <!-- Blue circle -->
        <circle cx="16" cy="16" r="10" fill="#3b82f6" stroke="#fff" stroke-width="3"/>
        <!-- Heading indicator triangle (pointing up/north, rotation handled by MapLibre) -->
        <polygon points="16,2 12,10 20,10" fill="#fff"/>
        <!-- Accuracy circle (semi-transparent) -->
        <circle cx="16" cy="16" r="14" fill="rgba(59, 130, 246, 0.2)" stroke="none"/>
      </svg>
    `;
  }

  /**
   * Remove position marker from map
   */
  private removePositionMarker(): void {
    if (this.positionMarker) {
      this.positionMarker.remove();
      this.positionMarker = null;
    }
  }

  // ─── Event Emission ─────────────────────────────────────────────────────────

  /**
   * Emit a guidance event
   */
  private emitGuidanceEvent(
    type: TGuidanceEventType,
    position: IGPSPosition,
    step?: IOSRMStep,
    instruction?: string
  ): void {
    const event: IGuidanceEvent = {
      type,
      position,
      stepIndex: this.state.currentStepIndex,
      step,
      distanceToManeuver: this.state.distanceToNextManeuver,
      instruction,
    };

    this.callbacks.onGuidanceEvent(event);
  }

  // ─── Render ─────────────────────────────────────────────────────────────────

  /**
   * Render guidance panel UI
   */
  public render(): TemplateResult {
    if (!this.state.isNavigating || !this.route) {
      return html``;
    }

    const currentStep = this.allSteps[this.state.currentStepIndex];
    const distance = this.formatDistance(this.state.distanceToNextManeuver);
    const maneuverIcon = this.getManeuverIcon(currentStep?.maneuver.type, currentStep?.maneuver.modifier);
    const instruction = currentStep ? this.formatInstruction(currentStep) : '';

    return html`
      <div class="guidance-panel">
        <div class="guidance-maneuver">
          <div class="guidance-maneuver-icon">${maneuverIcon}</div>
          <div class="guidance-maneuver-distance">${distance}</div>
        </div>
        <div class="guidance-instruction">${instruction}</div>
        <div class="guidance-remaining">
          ${this.formatDistance(this.state.distanceRemaining)} remaining
          &bull;
          ${this.formatDuration(this.state.timeRemaining)}
        </div>
        ${this.state.isOffRoute ? html`
          <div class="guidance-off-route">
            Off route - recalculating...
          </div>
        ` : ''}
      </div>
    `;
  }

  // ─── Formatting Utilities ───────────────────────────────────────────────────

  /**
   * Format distance for display
   */
  private formatDistance(meters: number): string {
    if (meters < 1000) {
      return `${Math.round(meters)} m`;
    }
    return `${(meters / 1000).toFixed(1)} km`;
  }

  /**
   * Format duration for display
   */
  private formatDuration(seconds: number): string {
    if (seconds < 60) {
      return `${Math.round(seconds)} sec`;
    }
    if (seconds < 3600) {
      return `${Math.round(seconds / 60)} min`;
    }
    const hours = Math.floor(seconds / 3600);
    const mins = Math.round((seconds % 3600) / 60);
    return mins > 0 ? `${hours} hr ${mins} min` : `${hours} hr`;
  }

  /**
   * Get maneuver icon
   */
  private getManeuverIcon(type?: string, modifier?: string): string {
    if (!type) return '➡';

    const icons: Record<string, string> = {
      'depart': '⬆️',
      'arrive': '🏁',
      'turn-left': '↰',
      'turn-right': '↱',
      'turn-slight left': '↖',
      'turn-slight right': '↗',
      'turn-sharp left': '⬅',
      'turn-sharp right': '➡',
      'continue-straight': '⬆️',
      'continue': '⬆️',
      'roundabout': '🔄',
      'rotary': '🔄',
      'merge': '⤵️',
      'fork-left': '↖',
      'fork-right': '↗',
      'new name': '⬆️',
    };

    const key = modifier ? `${type}-${modifier}` : type;
    return icons[key] || icons[type] || '➡';
  }

  /**
   * Format step instruction
   */
  private formatInstruction(step: IOSRMStep): string {
    const { type, modifier } = step.maneuver;
    const name = step.name || 'unnamed road';

    switch (type) {
      case 'depart':
        return `Head ${modifier || 'forward'} on ${name}`;
      case 'arrive':
        return 'Arrive at your destination';
      case 'turn':
        return `Turn ${modifier || ''} onto ${name}`;
      case 'continue':
        return `Continue on ${name}`;
      case 'merge':
        return `Merge ${modifier || ''} onto ${name}`;
      case 'roundabout':
      case 'rotary':
        return `At the roundabout, take the exit onto ${name}`;
      case 'fork':
        return `Take the ${modifier || ''} fork onto ${name}`;
      case 'end of road':
        return `At the end of the road, turn ${modifier || ''} onto ${name}`;
      case 'new name':
        return `Continue on ${name}`;
      default:
        return `${type} on ${name}`;
    }
  }

  // ─── Cleanup ────────────────────────────────────────────────────────────────

  /**
   * Clean up resources
   */
  public cleanup(): void {
    this.stopGuidance();
    this.removePositionMarker();
  }
}
