import { Injectable } from '@nestjs/common';
import { GoogleDirectionsClient } from './google-directions.client';
import { GoogleRoadsClient } from './google-roads.client';
import {
  downsamplePath,
  haversineMeters,
  pathLengthMeters,
  type LatLng,
} from './map-matching.util';

export type AlignPathInputPoint = {
  lat: number;
  lng: number;
  timestamp?: Date | null;
};

export type AlignPathResult = {
  engine: string;
  points: LatLng[];
  distanceMeters: number;
  distanceKm: number;
  rawPointCount: number;
  cleanedPointCount: number;
  gapsFilled: number;
  /** Why Snap did / did not apply (for Flutter logs). */
  snapStatus: string;
};

/**
 * Merge GPS evidence with Google road geometry:
 * clean spikes → Directions kill-gap fill → Snap-to-Roads (interpolate).
 */
@Injectable()
export class PathAlignService {

  constructor(
    private readonly directions: GoogleDirectionsClient,
    private readonly roads: GoogleRoadsClient,
  ) {}

  async align(points: AlignPathInputPoint[]): Promise<AlignPathResult> {
    const rawCount = points.length;
    if (points.length < 2) {
      const pts = points.map((p) => ({ lat: p.lat, lng: p.lng }));
      return {
        engine: 'passthrough',
        points: pts,
        distanceMeters: pathLengthMeters(pts),
        distanceKm: pathLengthMeters(pts) / 1000,
        rawPointCount: rawCount,
        cleanedPointCount: pts.length,
        gapsFilled: 0,
        snapStatus: 'too_few_input',
      };
    }

    const cleaned = this.cleanGps(points);
    const { path: gapFilled, gapsFilled } =
      await this.fillKillGaps(cleaned);
    const down = downsamplePath(gapFilled, 18);

    let aligned: LatLng[] = down;
    let engine = 'gps_cleaned';
    let snapStatus = 'not_attempted';

    if (!this.roads.hasApiKey()) {
      snapStatus = 'no_api_key';
    } else if (down.length < 2) {
      snapStatus = 'too_few_points';
    } else {
      const snapped = await this.roads.snapToRoads(down, true);
      if (snapped.length < 2) {
        snapStatus =
          'snap_empty_or_failed — enable Roads API on the Google Cloud key';
      } else if (!this.isSaneVsRaw(down, snapped)) {
        const rawM = pathLengthMeters(down);
        const snapM = pathLengthMeters(snapped);
        snapStatus = `snap_rejected_length rawM=${rawM.toFixed(0)} snapM=${snapM.toFixed(0)}`;
      } else {
        aligned = snapped;
        snapStatus = `snap_ok ${down.length}→${snapped.length}`;
        engine =
          gapsFilled > 0
            ? 'google_roads+directions_gaps'
            : 'google_roads_snap';
      }
    }

    if (engine === 'gps_cleaned' && gapsFilled > 0) {
      engine = 'directions_gaps';
    }

    // Roads API often 403 — Directions usually works. Stitch driving legs
    // along GPS waypoints so paint stays on streets without Snap-to-Roads.
    if (
      (engine === 'gps_cleaned' || engine === 'directions_gaps') &&
      this.directions.hasApiKey() &&
      down.length >= 2
    ) {
      const corridor = await this.directionsCorridorAlongGps(down);
      if (
        corridor.points.length >= 2 &&
        this.isSaneVsRaw(down, corridor.points)
      ) {
        aligned = corridor.points;
        engine =
          gapsFilled > 0
            ? 'directions_corridor+gaps'
            : 'directions_corridor';
        snapStatus = `${snapStatus}; corridor_ok segs=${corridor.segments}`;
      } else {
        snapStatus = `${snapStatus}; corridor_failed_or_unsane`;
      }
    }

    const meters = pathLengthMeters(aligned);

    return {
      engine,
      points: aligned,
      distanceMeters: meters,
      distanceKm: meters / 1000,
      rawPointCount: rawCount,
      cleanedPointCount: cleaned.length,
      gapsFilled,
      snapStatus,
    };
  }

  /**
   * Sample GPS every ~spacingM and connect with Directions driving routes.
   * Used when Snap-to-Roads is unavailable (403 / API not enabled).
   */
  private async directionsCorridorAlongGps(
    path: LatLng[],
    spacingM = 350,
    maxLegs = 35,
  ): Promise<{ points: LatLng[]; segments: number }> {
    if (path.length < 2) return { points: path, segments: 0 };

    const waypoints: LatLng[] = [path[0]];
    let last = path[0];
    for (let i = 1; i < path.length - 1; i++) {
      if (haversineMeters(last, path[i]) >= spacingM) {
        waypoints.push(path[i]);
        last = path[i];
        if (waypoints.length >= maxLegs) break;
      }
    }
    const end = path[path.length - 1];
    if (
      haversineMeters(waypoints[waypoints.length - 1], end) > 40 ||
      waypoints.length === 1
    ) {
      waypoints.push(end);
    }

    if (waypoints.length < 2) return { points: path, segments: 0 };

    const out: LatLng[] = [{ ...waypoints[0] }];
    let segments = 0;

    for (let i = 1; i < waypoints.length; i++) {
      const a = waypoints[i - 1];
      const b = waypoints[i];
      const straight = haversineMeters(a, b);
      if (straight < 60) {
        out.push({ ...b });
        continue;
      }

      const routes = await this.directions.fetchDrivingRoutes({
        origin: a,
        destination: b,
        alternatives: true,
      });
      const route = this.pickSaneRoute(routes, straight);
      if (route && route.polylinePoints.length >= 2) {
        for (let j = 1; j < route.polylinePoints.length; j++) {
          out.push(route.polylinePoints[j]);
        }
        segments++;
      } else {
        out.push({ ...b });
      }
    }

    return { points: out, segments };
  }

  /** Drop near-duplicates and urban V-spikes before Google calls. */
  private cleanGps(points: AlignPathInputPoint[]): AlignPathInputPoint[] {
    const spaced: AlignPathInputPoint[] = [points[0]];
    for (let i = 1; i < points.length; i++) {
      const prev = spaced[spaced.length - 1];
      const next = points[i];
      const d = haversineMeters(
        { lat: prev.lat, lng: prev.lng },
        { lat: next.lat, lng: next.lng },
      );
      if (d < 15) continue;
      spaced.push(next);
    }

    if (spaced.length < 3) return spaced;

    const out: AlignPathInputPoint[] = [spaced[0]];
    for (let i = 1; i < spaced.length - 1; i++) {
      const prev = out[out.length - 1];
      const mid = spaced[i];
      const next = spaced[i + 1];
      const dPrev = haversineMeters(
        { lat: prev.lat, lng: prev.lng },
        { lat: mid.lat, lng: mid.lng },
      );
      const dNext = haversineMeters(
        { lat: mid.lat, lng: mid.lng },
        { lat: next.lat, lng: next.lng },
      );
      const dSkip = haversineMeters(
        { lat: prev.lat, lng: prev.lng },
        { lat: next.lat, lng: next.lng },
      );
      // Only drop clear V-spikes (not gentle road curves).
      if (
        dPrev > 55 &&
        dNext > 55 &&
        dSkip < 70 &&
        dSkip < dPrev * 0.4 &&
        dSkip < dNext * 0.4
      ) {
        continue;
      }
      out.push(mid);
    }
    out.push(spaced[spaced.length - 1]);
    return out;
  }

  /** Directions only for real kill / long silence gaps — not every GPS hop. */
  private async fillKillGaps(
    points: AlignPathInputPoint[],
  ): Promise<{ path: LatLng[]; gapsFilled: number }> {
    const path: LatLng[] = [{ lat: points[0].lat, lng: points[0].lng }];
    let gapsFilled = 0;
    if (!this.directions.hasApiKey()) {
      for (let i = 1; i < points.length; i++) {
        path.push({ lat: points[i].lat, lng: points[i].lng });
      }
      return { path, gapsFilled };
    }

    for (let i = 1; i < points.length; i++) {
      const prev = points[i - 1];
      const next = points[i];
      const prevLl = { lat: prev.lat, lng: prev.lng };
      const nextLl = { lat: next.lat, lng: next.lng };
      const distM = haversineMeters(prevLl, nextLl);
      const dtSec = this.dtSeconds(prev, next);

      if (this.shouldFillKillGap(distM, dtSec)) {
        const routes = await this.directions.fetchDrivingRoutes({
          origin: prevLl,
          destination: nextLl,
          alternatives: true,
        });
        const route = this.pickSaneRoute(routes, distM);
        if (route && route.polylinePoints.length >= 2) {
          for (let j = 1; j < route.polylinePoints.length; j++) {
            path.push(route.polylinePoints[j]);
          }
          gapsFilled++;
          continue;
        }
      }
      path.push(nextLl);
    }
    return { path, gapsFilled };
  }

  private shouldFillKillGap(distM: number, dtSec: number): boolean {
    if (distM <= 250 || distM > 15000) return false;
    if (dtSec >= 90) return true;
    return dtSec >= 60 && distM >= 400;
  }

  private pickSaneRoute(
    routes: Array<{ polylinePoints: LatLng[]; distanceMeters: number }>,
    straightM: number,
  ): { polylinePoints: LatLng[]; distanceMeters: number } | null {
    let best: { polylinePoints: LatLng[]; distanceMeters: number } | null =
      null;
    for (const r of routes) {
      const roadM = r.distanceMeters;
      if (roadM < 1 || straightM < 1) continue;
      if (roadM < straightM * 0.5) continue;
      // Tight cap on short hops — avoid 61m → 267m detours.
      const maxExtra = straightM < 150 ? 80 : straightM < 400 ? 250 : 600;
      const maxRatio = straightM < 150 ? 1.6 : 2.0;
      if (roadM > straightM * maxRatio && roadM > straightM + maxExtra) {
        continue;
      }
      if (!best || roadM < best.distanceMeters) best = r;
    }
    return best;
  }

  private isSaneVsRaw(raw: LatLng[], snapped: LatLng[]): boolean {
    const rawM = pathLengthMeters(raw);
    const snapM = pathLengthMeters(snapped);
    if (rawM < 50) return true;
    return snapM <= rawM * 2.8 && snapM >= rawM * 0.35;
  }

  private dtSeconds(a: AlignPathInputPoint, b: AlignPathInputPoint): number {
    if (!a.timestamp || !b.timestamp) return 0;
    return Math.max(
      0,
      (b.timestamp.getTime() - a.timestamp.getTime()) / 1000,
    );
  }
}
