import type { CompanionDatabase } from "./database.js";
import { collectCarTelemetry, type AdbConfig, type CarTelemetry } from "./adb.js";

const SAMPLE_INTERVAL_MS = 10_000;
const RAW_RETENTION_MS = 90 * 24 * 60 * 60 * 1_000;
const ROLLUP_RETENTION_MS = 10 * 365 * 24 * 60 * 60 * 1_000;
const MAX_RAW_SAMPLES = 800_000;
const MAX_HOURLY_ROLLUPS = 88_000;
const BATTERY_USABLE_KWH = 61.7;

type StoredSample = {
  recordedAt: number;
  speedKph: number | null;
  wheelAngleDegrees: number | null;
  batteryPercent: number | null;
  rangeKm: number | null;
  batteryVoltage: number | null;
  totalConsumptionKwh: number | null;
  chargingStatus: number | null;
  chargingPowerKw: number | null;
};

export type TelemetryPeriod = "24h" | "7d" | "30d" | "1y" | "all";
export type ClearTelemetryMode = "history" | "all";

export type TelemetryHistoryPoint = {
  timestamp: number;
  speedKph: number | null;
  peakSpeedKph: number | null;
  batteryPercent: number | null;
  rangeKm: number | null;
  chargingPowerKw: number | null;
};

function value<T>(field: { available: true; value: T } | { available: false; reason: string }): T | null {
  return field.available ? field.value : null;
}

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

function rowToStored(row: Record<string, unknown> | undefined): StoredSample | null {
  if (!row) return null;
  return {
    recordedAt: Number(row.recorded_at),
    speedKph: finite(row.speed_kph),
    wheelAngleDegrees: finite(row.wheel_angle_degrees),
    batteryPercent: finite(row.battery_percent),
    rangeKm: finite(row.range_km),
    batteryVoltage: finite(row.battery_voltage),
    totalConsumptionKwh: finite(row.total_consumption_kwh),
    chargingStatus: finite(row.charging_status),
    chargingPowerKw: finite(row.charging_power_kw)
  };
}

export class CarTelemetryStore {
  #samplesSincePrune = 0;

  constructor(private readonly database: CompanionDatabase) {}

  record(telemetry: CarTelemetry): StoredSample {
    const recordedAt = Date.parse(telemetry.collectedAt);
    const timestamp = Number.isFinite(recordedAt) ? recordedAt : Date.now();
    const previous = rowToStored(this.database
      .prepare("SELECT * FROM car_telemetry_samples ORDER BY recorded_at DESC LIMIT 1")
      .get() as Record<string, unknown> | undefined);
    const speedKph = value(telemetry.speedKph);
    const batteryPercent = value(telemetry.batteryPercent);
    const chargingStatus = value(telemetry.chargingStatus);
    const chargingPowerKw = this.#estimateChargingPower(timestamp, batteryPercent, chargingStatus);
    const sample: StoredSample = {
      recordedAt: timestamp,
      speedKph,
      wheelAngleDegrees: value(telemetry.wheelAngleDegrees),
      batteryPercent,
      rangeKm: value(telemetry.rangeKm),
      batteryVoltage: value(telemetry.batteryVoltage),
      totalConsumptionKwh: value(telemetry.totalConsumptionKwh),
      chargingStatus,
      chargingPowerKw
    };

    const deltaSeconds = previous ? Math.max(0, (timestamp - previous.recordedAt) / 1_000) : 0;
    const contiguous = deltaSeconds > 0 && deltaSeconds <= 60;
    const averageSpeed = contiguous && speedKph !== null && previous !== null && previous.speedKph !== null
      ? (speedKph + previous.speedKph) / 2
      : speedKph ?? 0;
    const distanceKm = contiguous && averageSpeed > 1 ? averageSpeed * deltaSeconds / 3_600 : 0;
    const drivingSeconds = distanceKm > 0 ? deltaSeconds : 0;
    const charging = chargingStatus !== null && chargingStatus > 0;
    const wasCharging = previous !== null && previous.chargingStatus !== null && previous.chargingStatus > 0;
    const chargingSeconds = contiguous && charging ? deltaSeconds : 0;
    const chargeSession = charging && !wasCharging ? 1 : 0;
    const socDrop = contiguous && !charging && batteryPercent !== null && previous !== null && previous.batteryPercent !== null
      ? Math.min(Math.max(previous.batteryPercent - batteryPercent, 0), 2)
      : 0;

    const transaction = this.database.transaction(() => {
      this.database.prepare(
        `INSERT OR REPLACE INTO car_telemetry_samples
          (recorded_at, speed_kph, wheel_angle_degrees, battery_percent, range_km,
           battery_voltage, total_consumption_kwh, charging_status, charging_power_kw)
         VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
      ).run(
        sample.recordedAt,
        sample.speedKph,
        sample.wheelAngleDegrees,
        sample.batteryPercent,
        sample.rangeKm,
        sample.batteryVoltage,
        sample.totalConsumptionKwh,
        sample.chargingStatus,
        sample.chargingPowerKw
      );
      this.#updateRollup(sample, distanceKm, drivingSeconds, chargingSeconds);
      this.#updateStats(sample, distanceKm, drivingSeconds, chargingSeconds, chargeSession, socDrop);
    });
    transaction();

    this.#samplesSincePrune += 1;
    if (this.#samplesSincePrune >= 360) {
      this.prune(timestamp);
      this.#samplesSincePrune = 0;
    }
    return sample;
  }

  #estimateChargingPower(recordedAt: number, batteryPercent: number | null, chargingStatus: number | null): number | null {
    if (batteryPercent === null || chargingStatus === null || chargingStatus <= 0) return null;
    const baseline = this.database.prepare(
      `SELECT recorded_at, battery_percent FROM car_telemetry_samples
       WHERE recorded_at BETWEEN ? AND ? AND battery_percent IS NOT NULL
       ORDER BY recorded_at DESC LIMIT 1`
    ).get(recordedAt - 10 * 60_000, recordedAt - 2 * 60_000) as { recorded_at: number; battery_percent: number } | undefined;
    if (!baseline) return null;
    const elapsedHours = (recordedAt - baseline.recorded_at) / 3_600_000;
    const socGain = batteryPercent - baseline.battery_percent;
    if (elapsedHours <= 0 || socGain < 0.2) return null;
    const estimate = (socGain / 100) * BATTERY_USABLE_KWH / elapsedHours;
    return estimate > 0 && estimate <= 350 ? Math.round(estimate * 10) / 10 : null;
  }

  #updateRollup(sample: StoredSample, distanceKm: number, drivingSeconds: number, chargingSeconds: number): void {
    const bucketStart = Math.floor(sample.recordedAt / 3_600_000) * 3_600_000;
    const speedCount = sample.speedKph === null ? 0 : 1;
    const socCount = sample.batteryPercent === null ? 0 : 1;
    const rangeCount = sample.rangeKm === null ? 0 : 1;
    const powerCount = sample.chargingPowerKw === null ? 0 : 1;
    this.database.prepare(
      `INSERT INTO car_telemetry_hourly
        (bucket_start, sample_count, speed_sum, speed_count, speed_max, soc_sum, soc_count, soc_min, soc_max,
         range_sum, range_count, range_max, charge_power_sum, charge_power_count, charge_power_max,
         distance_km, driving_seconds, charging_seconds)
       VALUES (?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
       ON CONFLICT(bucket_start) DO UPDATE SET
         sample_count = sample_count + 1,
         speed_sum = speed_sum + excluded.speed_sum,
         speed_count = speed_count + excluded.speed_count,
         speed_max = CASE WHEN excluded.speed_max IS NULL THEN speed_max WHEN speed_max IS NULL OR excluded.speed_max > speed_max THEN excluded.speed_max ELSE speed_max END,
         soc_sum = soc_sum + excluded.soc_sum,
         soc_count = soc_count + excluded.soc_count,
         soc_min = CASE WHEN excluded.soc_min IS NULL THEN soc_min WHEN soc_min IS NULL OR excluded.soc_min < soc_min THEN excluded.soc_min ELSE soc_min END,
         soc_max = CASE WHEN excluded.soc_max IS NULL THEN soc_max WHEN soc_max IS NULL OR excluded.soc_max > soc_max THEN excluded.soc_max ELSE soc_max END,
         range_sum = range_sum + excluded.range_sum,
         range_count = range_count + excluded.range_count,
         range_max = CASE WHEN excluded.range_max IS NULL THEN range_max WHEN range_max IS NULL OR excluded.range_max > range_max THEN excluded.range_max ELSE range_max END,
         charge_power_sum = charge_power_sum + excluded.charge_power_sum,
         charge_power_count = charge_power_count + excluded.charge_power_count,
         charge_power_max = CASE WHEN excluded.charge_power_max IS NULL THEN charge_power_max WHEN charge_power_max IS NULL OR excluded.charge_power_max > charge_power_max THEN excluded.charge_power_max ELSE charge_power_max END,
         distance_km = distance_km + excluded.distance_km,
         driving_seconds = driving_seconds + excluded.driving_seconds,
         charging_seconds = charging_seconds + excluded.charging_seconds`
    ).run(
      bucketStart,
      sample.speedKph ?? 0,
      speedCount,
      sample.speedKph,
      sample.batteryPercent ?? 0,
      socCount,
      sample.batteryPercent,
      sample.batteryPercent,
      sample.rangeKm ?? 0,
      rangeCount,
      sample.rangeKm,
      sample.chargingPowerKw ?? 0,
      powerCount,
      sample.chargingPowerKw,
      distanceKm,
      drivingSeconds,
      chargingSeconds
    );
  }

  #updateStats(
    sample: StoredSample,
    distanceKm: number,
    drivingSeconds: number,
    chargingSeconds: number,
    chargeSession: number,
    socDrop: number
  ): void {
    this.database.prepare(
      `UPDATE car_telemetry_stats SET
        first_sample_at = COALESCE(first_sample_at, ?),
        last_sample_at = ?,
        sample_count = sample_count + 1,
        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_kph = CASE WHEN ? IS NOT NULL AND (peak_speed_kph IS NULL OR ? > peak_speed_kph) THEN ? ELSE peak_speed_kph END,
        peak_charging_power_at = CASE WHEN ? IS NOT NULL AND (peak_charging_power_kw IS NULL OR ? > peak_charging_power_kw) THEN ? ELSE peak_charging_power_at END,
        peak_charging_power_kw = CASE WHEN ? IS NOT NULL AND (peak_charging_power_kw IS NULL OR ? > peak_charging_power_kw) THEN ? ELSE peak_charging_power_kw END,
        max_observed_range_at = CASE WHEN ? IS NOT NULL AND (max_observed_range_km IS NULL OR ? > max_observed_range_km) THEN ? ELSE max_observed_range_at END,
        max_observed_range_km = CASE WHEN ? IS NOT NULL AND (max_observed_range_km IS NULL OR ? > max_observed_range_km) THEN ? ELSE max_observed_range_km END,
        tracked_distance_km = tracked_distance_km + ?,
        driving_seconds = driving_seconds + ?,
        charging_seconds = charging_seconds + ?,
        charge_sessions = charge_sessions + ?,
        soc_used_percent = soc_used_percent + ?
       WHERE id = 1`
    ).run(
      sample.recordedAt,
      sample.recordedAt,
      sample.speedKph, sample.speedKph, sample.recordedAt,
      sample.speedKph, sample.speedKph, sample.speedKph,
      sample.chargingPowerKw, sample.chargingPowerKw, sample.recordedAt,
      sample.chargingPowerKw, sample.chargingPowerKw, sample.chargingPowerKw,
      sample.rangeKm, sample.rangeKm, sample.recordedAt,
      sample.rangeKm, sample.rangeKm, sample.rangeKm,
      distanceKm,
      drivingSeconds,
      chargingSeconds,
      chargeSession,
      socDrop
    );
  }

  prune(now = Date.now()): void {
    const transaction = this.database.transaction(() => {
      this.database.prepare("DELETE FROM car_telemetry_samples WHERE recorded_at < ?").run(now - RAW_RETENTION_MS);
      this.database.prepare(
        `DELETE FROM car_telemetry_samples WHERE recorded_at IN (
          SELECT recorded_at FROM car_telemetry_samples ORDER BY recorded_at DESC LIMIT -1 OFFSET ?
        )`
      ).run(MAX_RAW_SAMPLES);
      this.database.prepare("DELETE FROM car_telemetry_hourly WHERE bucket_start < ?").run(now - ROLLUP_RETENTION_MS);
      this.database.prepare(
        `DELETE FROM car_telemetry_hourly WHERE bucket_start IN (
          SELECT bucket_start FROM car_telemetry_hourly ORDER BY bucket_start DESC LIMIT -1 OFFSET ?
        )`
      ).run(MAX_HOURLY_ROLLUPS);
    });
    transaction();
  }

  clear(mode: ClearTelemetryMode): { mode: ClearTelemetryMode; deletedSamples: number; deletedRollups: number } {
    const transaction = this.database.transaction(() => {
      const deletedSamples = this.database.prepare("DELETE FROM car_telemetry_samples").run().changes;
      const deletedRollups = this.database.prepare("DELETE FROM car_telemetry_hourly").run().changes;

      if (mode === "all") {
        this.database.prepare(
          `UPDATE car_telemetry_stats SET
            first_sample_at = NULL,
            last_sample_at = NULL,
            sample_count = 0,
            peak_speed_kph = NULL,
            peak_speed_at = NULL,
            peak_charging_power_kw = NULL,
            peak_charging_power_at = NULL,
            max_observed_range_km = NULL,
            max_observed_range_at = NULL,
            tracked_distance_km = 0,
            driving_seconds = 0,
            charging_seconds = 0,
            charge_sessions = 0,
            soc_used_percent = 0
           WHERE id = 1`
        ).run();
      }

      return { mode, deletedSamples, deletedRollups };
    });

    this.#samplesSincePrune = 0;
    return transaction();
  }

  overview() {
    const stats = this.database.prepare("SELECT * FROM car_telemetry_stats WHERE id = 1").get() as Record<string, number | null>;
    const latest = rowToStored(this.database.prepare("SELECT * FROM car_telemetry_samples ORDER BY recorded_at DESC LIMIT 1").get() as Record<string, unknown> | undefined);
    const storage = this.database.prepare(
      `SELECT
        (SELECT COUNT(*) FROM car_telemetry_samples) AS raw_samples,
        (SELECT MIN(recorded_at) FROM car_telemetry_samples) AS oldest_raw_at,
        (SELECT COUNT(*) FROM car_telemetry_hourly) AS hourly_rollups,
        (SELECT MIN(bucket_start) FROM car_telemetry_hourly) AS oldest_rollup_at`
    ).get() as Record<string, number | null>;
    const trackedDistance = Number(stats.tracked_distance_km ?? 0);
    const drivingSeconds = Number(stats.driving_seconds ?? 0);
    const socUsed = Number(stats.soc_used_percent ?? 0);
    const ownRangeEstimate = trackedDistance >= 10 && socUsed >= 5 ? trackedDistance / socUsed * 100 : null;
    return {
      latest,
      records: {
        peakSpeedKph: finite(stats.peak_speed_kph),
        peakSpeedAt: finite(stats.peak_speed_at),
        peakChargingPowerKw: finite(stats.peak_charging_power_kw),
        peakChargingPowerAt: finite(stats.peak_charging_power_at),
        maxObservedRangeKm: finite(stats.max_observed_range_km),
        maxObservedRangeAt: finite(stats.max_observed_range_at)
      },
      lifetime: {
        firstSampleAt: finite(stats.first_sample_at),
        lastSampleAt: finite(stats.last_sample_at),
        sampleCount: Number(stats.sample_count ?? 0),
        trackedDistanceKm: trackedDistance,
        drivingSeconds,
        chargingSeconds: Number(stats.charging_seconds ?? 0),
        chargeSessions: Number(stats.charge_sessions ?? 0),
        socUsedPercent: socUsed,
        averageMovingSpeedKph: drivingSeconds > 0 ? trackedDistance / (drivingSeconds / 3_600) : null,
        ownRangeEstimateKm: ownRangeEstimate
      },
      storage: {
        rawSamples: Number(storage.raw_samples ?? 0),
        hourlyRollups: Number(storage.hourly_rollups ?? 0),
        oldestRawAt: finite(storage.oldest_raw_at),
        oldestRollupAt: finite(storage.oldest_rollup_at),
        estimatedBytes: Number(storage.raw_samples ?? 0) * 112 + Number(storage.hourly_rollups ?? 0) * 176,
        rawRetentionDays: 90,
        rollupRetentionYears: 10,
        rawSampleCap: MAX_RAW_SAMPLES
      }
    };
  }

  history(period: TelemetryPeriod): { period: TelemetryPeriod; points: TelemetryHistoryPoint[] } {
    const now = Date.now();
    const settings: Record<TelemetryPeriod, { duration: number; bucket: number; rollup: boolean }> = {
      "24h": { duration: 24 * 60 * 60_000, bucket: 5 * 60_000, rollup: false },
      "7d": { duration: 7 * 24 * 60 * 60_000, bucket: 30 * 60_000, rollup: false },
      "30d": { duration: 30 * 24 * 60 * 60_000, bucket: 2 * 60 * 60_000, rollup: false },
      "1y": { duration: 365 * 24 * 60 * 60_000, bucket: 24 * 60 * 60_000, rollup: true },
      all: { duration: ROLLUP_RETENTION_MS, bucket: 7 * 24 * 60 * 60_000, rollup: true }
    };
    const setting = settings[period];
    const from = now - setting.duration;
    const rows = setting.rollup
      ? this.database.prepare(
        `SELECT CAST(bucket_start / ? AS INTEGER) * ? AS timestamp,
          CASE WHEN SUM(speed_count) > 0 THEN SUM(speed_sum) / SUM(speed_count) END AS speed_kph,
          MAX(speed_max) AS peak_speed_kph,
          CASE WHEN SUM(soc_count) > 0 THEN SUM(soc_sum) / SUM(soc_count) END AS battery_percent,
          CASE WHEN SUM(range_count) > 0 THEN SUM(range_sum) / SUM(range_count) END AS range_km,
          MAX(charge_power_max) AS charging_power_kw
         FROM car_telemetry_hourly WHERE bucket_start >= ? GROUP BY timestamp ORDER BY timestamp`
      ).all(setting.bucket, setting.bucket, from)
      : this.database.prepare(
        `SELECT CAST(recorded_at / ? AS INTEGER) * ? AS timestamp,
          AVG(speed_kph) AS speed_kph, MAX(speed_kph) AS peak_speed_kph,
          AVG(battery_percent) AS battery_percent, AVG(range_km) AS range_km,
          MAX(charging_power_kw) AS charging_power_kw
         FROM car_telemetry_samples WHERE recorded_at >= ? GROUP BY timestamp ORDER BY timestamp`
      ).all(setting.bucket, setting.bucket, from);
    return {
      period,
      points: (rows as Record<string, unknown>[]).map((row) => ({
        timestamp: Number(row.timestamp),
        speedKph: finite(row.speed_kph),
        peakSpeedKph: finite(row.peak_speed_kph),
        batteryPercent: finite(row.battery_percent),
        rangeKm: finite(row.range_km),
        chargingPowerKw: finite(row.charging_power_kw)
      }))
    };
  }
}

export class CarTelemetryRecorder {
  readonly store: CarTelemetryStore;
  #timer: NodeJS.Timeout | null = null;
  #pending: Promise<CarTelemetry> | null = null;
  #latest: CarTelemetry | null = null;
  #lastStoredAt = 0;

  constructor(database: CompanionDatabase, private readonly config: AdbConfig) {
    this.store = new CarTelemetryStore(database);
  }

  start(): void {
    if (!this.config.enabled || this.#timer) return;
    void this.readLive().catch(() => undefined);
    this.#timer = setInterval(() => void this.readLive().catch(() => undefined), SAMPLE_INTERVAL_MS);
    this.#timer.unref();
  }

  stop(): void {
    if (this.#timer) clearInterval(this.#timer);
    this.#timer = null;
  }

  async readLive(): Promise<CarTelemetry> {
    const latestAt = this.#latest ? Date.parse(this.#latest.collectedAt) : 0;
    if (this.#latest && Date.now() - latestAt < 1_500) return this.#latest;
    if (this.#pending) return this.#pending;
    this.#pending = collectCarTelemetry(this.config).then((telemetry) => {
      this.#latest = telemetry;
      const sampleAt = Date.parse(telemetry.collectedAt);
      if (sampleAt - this.#lastStoredAt >= SAMPLE_INTERVAL_MS - 1_000) {
        this.store.record(telemetry);
        this.#lastStoredAt = sampleAt;
      }
      return telemetry;
    }).finally(() => { this.#pending = null; });
    return this.#pending;
  }
}
