import type { CompanionDatabase } from "./database.js";

const START_SPEED_KPH = 5;
const STOP_TIMEOUT_MS = 3 * 60_000;
const GAP_TIMEOUT_MS = 10 * 60_000;
const MAX_LOCATION_ACCURACY_METERS = 100;
const CAR_SAMPLE_WINDOW_MS = 30_000;

export type LocationSample = {
  deviceId: string;
  capturedAt: string;
  latitude: number;
  longitude: number;
  accuracyMeters: number;
  altitudeMeters?: number | null;
  speedKph?: number | null;
  bearingDegrees?: number | null;
  phoneBatteryPercent?: number | null;
};

type CarSample = {
  speed_kph: number | null;
  battery_percent: number | null;
  total_consumption_kwh: number | null;
};

type ActiveTrip = {
  id: number;
  started_at: number;
  last_moving_at: number;
  end_latitude: number;
  end_longitude: number;
};

type PreviousPoint = {
  captured_at: number;
  latitude: number;
  longitude: number;
  speed_kph: number | null;
};

function finite(value: unknown): number | null {
  return typeof value === "number" && Number.isFinite(value) ? value : null;
}

function haversineKm(a: { latitude: number; longitude: number }, b: { latitude: number; longitude: number }): number {
  const radians = Math.PI / 180;
  const lat1 = a.latitude * radians;
  const lat2 = b.latitude * radians;
  const deltaLat = (b.latitude - a.latitude) * radians;
  const deltaLon = (b.longitude - a.longitude) * radians;
  const value = Math.sin(deltaLat / 2) ** 2
    + Math.cos(lat1) * Math.cos(lat2) * Math.sin(deltaLon / 2) ** 2;
  return 6_371 * 2 * Math.atan2(Math.sqrt(value), Math.sqrt(1 - value));
}

export class TripStore {
  constructor(private readonly database: CompanionDatabase) {}

  record(sample: LocationSample): { tripId: number | null; active: boolean } {
    const capturedAt = Date.parse(sample.capturedAt);
    if (!Number.isFinite(capturedAt) || sample.accuracyMeters > MAX_LOCATION_ACCURACY_METERS) {
      return { tripId: null, active: false };
    }

    const car = this.database.prepare(
      `SELECT speed_kph, battery_percent, total_consumption_kwh
       FROM car_telemetry_samples WHERE recorded_at BETWEEN ? AND ?
       ORDER BY ABS(recorded_at - ?) LIMIT 1`
    ).get(capturedAt - CAR_SAMPLE_WINDOW_MS, capturedAt + CAR_SAMPLE_WINDOW_MS, capturedAt) as CarSample | undefined;
    const speedKph = finite(car?.speed_kph) ?? finite(sample.speedKph);
    const moving = (speedKph ?? 0) >= START_SPEED_KPH;
    let active = this.activeTrip(sample.deviceId);

    if (active && capturedAt <= active.started_at) return { tripId: active.id, active: true };
    if (active && capturedAt - active.last_moving_at >= GAP_TIMEOUT_MS) {
      this.finish(active.id, active.last_moving_at, active.end_latitude, active.end_longitude);
      active = undefined;
    }
    if (!active && !moving) return { tripId: null, active: false };

    if (!active) {
      const result = this.database.prepare(
        `INSERT INTO trips
          (device_id, started_at, last_moving_at, start_latitude, start_longitude,
           end_latitude, end_longitude, peak_speed_kph, peak_speed_at,
           peak_speed_latitude, peak_speed_longitude, vehicle_battery_start,
           vehicle_battery_end, vehicle_consumption_start_kwh, vehicle_consumption_end_kwh,
           phone_battery_start, phone_battery_end)
         VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
      ).run(
        sample.deviceId, capturedAt, capturedAt, sample.latitude, sample.longitude,
        sample.latitude, sample.longitude, speedKph, capturedAt, sample.latitude, sample.longitude,
        finite(car?.battery_percent), finite(car?.battery_percent), finite(car?.total_consumption_kwh),
        finite(car?.total_consumption_kwh), finite(sample.phoneBatteryPercent), finite(sample.phoneBatteryPercent)
      );
      active = {
        id: Number(result.lastInsertRowid),
        started_at: capturedAt,
        last_moving_at: capturedAt,
        end_latitude: sample.latitude,
        end_longitude: sample.longitude
      };
    }

    const previous = this.database.prepare(
      `SELECT captured_at, latitude, longitude, speed_kph FROM trip_points
       WHERE trip_id = ? ORDER BY captured_at DESC LIMIT 1`
    ).get(active.id) as PreviousPoint | undefined;
    const elapsedSeconds = previous ? Math.max(0, (capturedAt - previous.captured_at) / 1_000) : 0;
    const distanceKm = previous && elapsedSeconds <= GAP_TIMEOUT_MS / 1_000
      ? haversineKm(previous, sample)
      : 0;
    const plausibleDistanceKm = Math.max(0.25, elapsedSeconds / 3_600 * 250);
    const segmentMoving = moving || (previous?.speed_kph ?? 0) >= START_SPEED_KPH;
    const acceptedDistanceKm = segmentMoving && distanceKm <= plausibleDistanceKm ? distanceKm : 0;
    const movingSeconds = segmentMoving && previous ? Math.min(elapsedSeconds, 30) : 0;

    const transaction = this.database.transaction(() => {
      const inserted = this.database.prepare(
        `INSERT OR IGNORE INTO trip_points
          (trip_id, captured_at, latitude, longitude, accuracy_meters, altitude_meters,
           speed_kph, bearing_degrees, vehicle_battery_percent)
         VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
      ).run(
        active!.id, capturedAt, sample.latitude, sample.longitude, sample.accuracyMeters,
        sample.altitudeMeters ?? null, speedKph, sample.bearingDegrees ?? null,
        finite(car?.battery_percent)
      );
      if (inserted.changes === 0) return;
      this.database.prepare(
        `UPDATE trips SET
          last_moving_at = CASE WHEN ? THEN ? ELSE last_moving_at END,
          end_latitude = CASE WHEN ? THEN ? ELSE end_latitude END,
          end_longitude = CASE WHEN ? THEN ? ELSE end_longitude END,
          distance_km = distance_km + ?, moving_seconds = moving_seconds + ?,
          point_count = point_count + 1,
          speed_sum = speed_sum + ?, speed_count = speed_count + ?,
          peak_speed_at = CASE WHEN ? IS NOT NULL AND (peak_speed_kph IS NULL OR ? > peak_speed_kph) THEN ? ELSE peak_speed_at END,
          peak_speed_latitude = CASE WHEN ? IS NOT NULL AND (peak_speed_kph IS NULL OR ? > peak_speed_kph) THEN ? ELSE peak_speed_latitude END,
          peak_speed_longitude = CASE WHEN ? IS NOT NULL AND (peak_speed_kph IS NULL OR ? > peak_speed_kph) THEN ? ELSE peak_speed_longitude END,
          peak_speed_kph = CASE WHEN ? IS NOT NULL AND (peak_speed_kph IS NULL OR ? > peak_speed_kph) THEN ? ELSE peak_speed_kph END,
          vehicle_battery_end = COALESCE(?, vehicle_battery_end),
          vehicle_consumption_end_kwh = COALESCE(?, vehicle_consumption_end_kwh),
          phone_battery_end = COALESCE(?, phone_battery_end)
         WHERE id = ?`
      ).run(
        moving ? 1 : 0, capturedAt, moving ? 1 : 0, sample.latitude, moving ? 1 : 0, sample.longitude,
        acceptedDistanceKm, movingSeconds, moving && speedKph !== null ? speedKph : 0, moving && speedKph !== null ? 1 : 0,
        speedKph, speedKph, capturedAt,
        speedKph, speedKph, sample.latitude,
        speedKph, speedKph, sample.longitude,
        speedKph, speedKph, speedKph,
        finite(car?.battery_percent), finite(car?.total_consumption_kwh), finite(sample.phoneBatteryPercent), active!.id
      );
    });
    transaction();

    if (!moving && capturedAt - active.last_moving_at >= STOP_TIMEOUT_MS) {
      this.finish(active.id, active.last_moving_at, active.end_latitude, active.end_longitude);
      return { tripId: active.id, active: false };
    }
    return { tripId: active.id, active: true };
  }

  list() {
    this.finalizeStale();
    const rows = this.database.prepare(
      `SELECT trips.*, mobile_devices.name AS device_name FROM trips
       JOIN mobile_devices ON mobile_devices.id = trips.device_id
       ORDER BY started_at DESC`
    ).all() as Record<string, unknown>[];
    return { trips: rows.map((row) => this.summary(row)) };
  }

  detail(id: number) {
    this.finalizeStale();
    const row = this.database.prepare(
      `SELECT trips.*, mobile_devices.name AS device_name FROM trips
       JOIN mobile_devices ON mobile_devices.id = trips.device_id WHERE trips.id = ?`
    ).get(id) as Record<string, unknown> | undefined;
    if (!row) return null;
    const endedAt = finite(row.ended_at);
    const points = this.database.prepare(
      `SELECT captured_at AS capturedAt, latitude, longitude, accuracy_meters AS accuracyMeters,
        altitude_meters AS altitudeMeters, speed_kph AS speedKph, bearing_degrees AS bearingDegrees,
        vehicle_battery_percent AS vehicleBatteryPercent
       FROM trip_points WHERE trip_id = ? AND captured_at <= ? ORDER BY captured_at`
    ).all(id, endedAt ?? Date.now());
    return {
      ...this.summary(row),
      points,
      pins: [
        { type: "start", latitude: Number(row.start_latitude), longitude: Number(row.start_longitude), capturedAt: Number(row.started_at) },
        finite(row.peak_speed_latitude) === null ? null : {
          type: "peak-speed", latitude: Number(row.peak_speed_latitude), longitude: Number(row.peak_speed_longitude),
          capturedAt: finite(row.peak_speed_at), speedKph: finite(row.peak_speed_kph)
        },
        { type: "end", latitude: Number(row.end_latitude), longitude: Number(row.end_longitude), capturedAt: endedAt }
      ].filter(Boolean)
    };
  }

  private activeTrip(deviceId: string): ActiveTrip | undefined {
    return this.database.prepare(
      `SELECT id, started_at, last_moving_at, end_latitude, end_longitude
       FROM trips WHERE device_id = ? AND ended_at IS NULL`
    ).get(deviceId) as ActiveTrip | undefined;
  }

  private finish(id: number, endedAt: number, latitude: number, longitude: number): void {
    this.database.transaction(() => {
      this.database.prepare("DELETE FROM trip_points WHERE trip_id = ? AND captured_at > ?").run(id, endedAt);
      this.database.prepare(
        `UPDATE trips SET ended_at = ?, end_latitude = ?, end_longitude = ?,
          point_count = (SELECT COUNT(*) FROM trip_points WHERE trip_id = ?)
         WHERE id = ? AND ended_at IS NULL`
      ).run(endedAt, latitude, longitude, id, id);
    })();
  }

  private finalizeStale(): void {
    const cutoff = Date.now() - STOP_TIMEOUT_MS;
    const stale = this.database.prepare(
      `SELECT id, last_moving_at, end_latitude, end_longitude FROM trips
       WHERE ended_at IS NULL AND last_moving_at < ?`
    ).all(cutoff) as Array<{ id: number; last_moving_at: number; end_latitude: number; end_longitude: number }>;
    for (const trip of stale) this.finish(trip.id, trip.last_moving_at, trip.end_latitude, trip.end_longitude);
  }

  private summary(row: Record<string, unknown>) {
    const startedAt = Number(row.started_at);
    const endedAt = finite(row.ended_at);
    const vehicleStart = finite(row.vehicle_battery_start);
    const vehicleEnd = finite(row.vehicle_battery_end);
    const consumptionStart = finite(row.vehicle_consumption_start_kwh);
    const consumptionEnd = finite(row.vehicle_consumption_end_kwh);
    const speedCount = Number(row.speed_count ?? 0);
    return {
      id: Number(row.id),
      deviceId: String(row.device_id),
      deviceName: String(row.device_name),
      startedAt,
      endedAt,
      active: endedAt === null,
      durationSeconds: Math.max(0, ((endedAt ?? Date.now()) - startedAt) / 1_000),
      movingSeconds: Number(row.moving_seconds ?? 0),
      distanceKm: Number(row.distance_km ?? 0),
      pointCount: Number(row.point_count ?? 0),
      averageSpeedKph: speedCount > 0 ? Number(row.speed_sum) / speedCount : null,
      peakSpeedKph: finite(row.peak_speed_kph),
      vehicleBatteryStartPercent: vehicleStart,
      vehicleBatteryEndPercent: vehicleEnd,
      batteryConsumedPercent: vehicleStart !== null && vehicleEnd !== null ? Math.max(0, vehicleStart - vehicleEnd) : null,
      energyConsumedKwh: consumptionStart !== null && consumptionEnd !== null
        ? Math.max(0, consumptionEnd - consumptionStart)
        : null,
      phoneBatteryStartPercent: finite(row.phone_battery_start),
      phoneBatteryEndPercent: finite(row.phone_battery_end),
      start: { latitude: Number(row.start_latitude), longitude: Number(row.start_longitude) },
      end: { latitude: Number(row.end_latitude), longitude: Number(row.end_longitude) }
    };
  }
}
