import type { Server as SocketIOServer } from "socket.io";

import type { DeviceStateSnapshot, NormalizedTelemetry, VehicleAlertSummary } from "@tracker/shared";

import { getVehicleStatus } from "@tracker/shared";

import { env } from "../../config/env.js";
import { query, queryOne } from "../../lib/db.js";
import { getRedis } from "../../lib/redis.js";
import { sendInspectionDueSoonEmailIfNeeded } from "../settings/mail.service.js";

type PersistedTelemetry = {
  deviceId: string;
  vehicleId: string | null;
  status: "online" | "delayed" | "offline";
};

type UpsertedDevice = {
  id: string;
  vehicle_id: string | null;
  ignition: boolean | null;
};

export class TelemetryService {
  constructor(private readonly io: SocketIOServer) {}

  async ingestTelemetry(telemetry: NormalizedTelemetry): Promise<PersistedTelemetry> {
    const serverTime = telemetry.serverTime || new Date().toISOString();
    const device = await this.upsertDevice({
      imei: telemetry.imei,
      protocolName: telemetry.protocolName,
      rawPayload: telemetry.rawPayload,
      serverTime,
      deviceTime: telemetry.deviceTime ?? null,
      battery: telemetry.battery ?? null,
      gsmSignal: telemetry.gsmSignal ?? null,
      ignition: telemetry.ignition ?? null,
      charging: telemetry.charging ?? null,
      defense: telemetry.defense ?? null,
      gpsValid: telemetry.gpsValid ?? null,
      satelliteCount: telemetry.satelliteCount ?? null,
      positionType: telemetry.positionType ?? null,
    });

    const resolvedTelemetry: NormalizedTelemetry = {
      ...telemetry,
      ignition: telemetry.ignition ?? device.ignition ?? undefined,
    };

    await this.persistPosition(device, resolvedTelemetry, serverTime);
    await this.handleInspectionDueSoonNotification(device.vehicle_id);
    const alert = await this.createOverspeedAlert(device, resolvedTelemetry, serverTime);

    const status = getVehicleStatus(
      serverTime,
      env.DEVICE_ONLINE_SECONDS,
      env.DEVICE_DELAYED_SECONDS,
    );

    const redis = await getRedis();
    if (redis) {
      await redis.set(`last_position:${telemetry.imei}`, JSON.stringify(resolvedTelemetry));
      await redis.set(`device_seen:${telemetry.imei}`, serverTime);
    }

    if (alert) {
      await this.broadcastVehicleAlert(alert);
    }

    await this.broadcastTelemetry(resolvedTelemetry, device.vehicle_id, status);

    return {
      deviceId: device.id,
      vehicleId: device.vehicle_id,
      status,
    };
  }

  private async handleInspectionDueSoonNotification(vehicleId: string | null) {
    if (!vehicleId) {
      return;
    }

    const vehicle = await queryOne<{
      id: string;
      name: string;
      plate_number: string | null;
      inspection_due_date: string | null;
    }>(
      `
        select id, name, plate_number, inspection_due_date
        from vehicles
        where id = $1
      `,
      [vehicleId],
    );

    if (!vehicle) {
      return;
    }

    try {
      await sendInspectionDueSoonEmailIfNeeded({
        vehicleId: vehicle.id,
        vehicleName: vehicle.name,
        plateNumber: vehicle.plate_number,
        inspectionDueDate: vehicle.inspection_due_date,
      });
    } catch (error) {
      console.error("inspection due soon email failed", {
        vehicleId: vehicle.id,
        error,
      });
    }
  }

  async ingestDeviceState(input: {
    imei: string;
    protocolName: string;
    rawPayload: string;
    serverTime?: string;
    deviceState?: DeviceStateSnapshot;
  }): Promise<PersistedTelemetry> {
    const serverTime = input.serverTime || new Date().toISOString();
    const device = await this.upsertDevice({
      imei: input.imei,
      protocolName: input.protocolName,
      rawPayload: input.rawPayload,
      serverTime,
      deviceTime: input.deviceState?.deviceTime ?? null,
      battery: input.deviceState?.battery ?? null,
      gsmSignal: input.deviceState?.gsmSignal ?? null,
      ignition: input.deviceState?.ignition ?? null,
      charging: input.deviceState?.charging ?? null,
      defense: input.deviceState?.defense ?? null,
      gpsValid: input.deviceState?.gpsValid ?? null,
      satelliteCount: input.deviceState?.satelliteCount ?? null,
      positionType: input.deviceState?.positionType ?? null,
    });

    const status = getVehicleStatus(
      serverTime,
      env.DEVICE_ONLINE_SECONDS,
      env.DEVICE_DELAYED_SECONDS,
    );

    return {
      deviceId: device.id,
      vehicleId: device.vehicle_id,
      status,
    };
  }

  private async upsertDevice(input: {
    imei: string;
    protocolName: string;
    rawPayload: string;
    serverTime: string;
    deviceTime?: string | null;
    battery?: number | null;
    gsmSignal?: number | null;
    ignition?: boolean | null;
    charging?: boolean | null;
    defense?: boolean | null;
    gpsValid?: boolean | null;
    satelliteCount?: number | null;
    positionType?: string | null;
  }) {
    const device = await queryOne<UpsertedDevice>(
      `
        insert into devices (
          imei,
          protocol_name,
          status,
          last_seen,
          last_device_time,
          battery,
          gsm_signal,
          ignition,
          charging,
          defense,
          gps_valid,
          satellite_count,
          position_type
        )
        values ($1, $2, 'online', $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
        on conflict (imei)
        do update set
          protocol_name = excluded.protocol_name,
          status = 'online',
          last_seen = excluded.last_seen,
          last_device_time = coalesce(excluded.last_device_time, devices.last_device_time),
          battery = coalesce(excluded.battery, devices.battery),
          gsm_signal = coalesce(excluded.gsm_signal, devices.gsm_signal),
          ignition = coalesce(excluded.ignition, devices.ignition),
          charging = coalesce(excluded.charging, devices.charging),
          defense = coalesce(excluded.defense, devices.defense),
          gps_valid = coalesce(excluded.gps_valid, devices.gps_valid),
          satellite_count = coalesce(excluded.satellite_count, devices.satellite_count),
          position_type = coalesce(excluded.position_type, devices.position_type),
          updated_at = now()
        returning id, vehicle_id, ignition
      `,
      [
        input.imei,
        input.protocolName,
        input.serverTime,
        input.deviceTime ?? null,
        input.battery ?? null,
        input.gsmSignal ?? null,
        input.ignition ?? null,
        input.charging ?? null,
        input.defense ?? null,
        input.gpsValid ?? null,
        input.satelliteCount ?? null,
        input.positionType ?? null,
      ],
    );

    if (!device) {
      throw new Error("Unable to upsert device");
    }

    if (!device.vehicle_id) {
      await query(
        `
          insert into unassigned_devices (imei, protocol_name, raw_payload, first_seen, last_seen)
          values ($1, $2, $3, $4, $4)
          on conflict (imei)
          do update set protocol_name = excluded.protocol_name, raw_payload = excluded.raw_payload, last_seen = excluded.last_seen
        `,
        [input.imei, input.protocolName, input.rawPayload, input.serverTime],
      );
    }

    return device;
  }

  private async persistPosition(
    device: UpsertedDevice,
    telemetry: NormalizedTelemetry,
    serverTime: string,
  ) {
    await query(
      `
        insert into positions (
          device_id,
          vehicle_id,
          imei,
          protocol_name,
          raw_payload,
          device_time,
          server_time,
          lat,
          lon,
          speed,
          heading,
          ignition,
          battery,
          gsm_signal,
          satellite_count,
          gps_valid,
          charging,
          defense,
          position_type,
          geom
        )
        values (
          $1,
          $2,
          $3,
          $4,
          $5,
          $6,
          $7,
          $8,
          $9,
          $10,
          $11,
          $12,
          $13,
          $14,
          $15,
          $16,
          $17,
          $18,
          $19,
          ST_SetSRID(ST_MakePoint($9, $8), 4326)::geography
        )
      `,
      [
        device.id,
        device.vehicle_id,
        telemetry.imei,
        telemetry.protocolName,
        telemetry.rawPayload,
        telemetry.deviceTime ?? null,
        serverTime,
        telemetry.lat,
        telemetry.lon,
        telemetry.speed ?? null,
        telemetry.heading ?? null,
        telemetry.ignition ?? null,
        telemetry.battery ?? null,
        telemetry.gsmSignal ?? null,
        telemetry.satelliteCount ?? null,
        telemetry.gpsValid ?? null,
        telemetry.charging ?? null,
        telemetry.defense ?? null,
        telemetry.positionType ?? null,
      ],
    );

    await query(
      `
        insert into last_positions (
          device_id,
          vehicle_id,
          imei,
          lat,
          lon,
          speed,
          heading,
          device_time,
          server_time,
          ignition,
          battery,
          gsm_signal,
          satellite_count,
          gps_valid,
          charging,
          defense,
          position_type,
          geom
        )
        values (
          $1,
          $2,
          $3,
          $4,
          $5,
          $6,
          $7,
          $8,
          $9,
          $10,
          $11,
          $12,
          $13,
          $14,
          $15,
          $16,
          $17,
          ST_SetSRID(ST_MakePoint($5, $4), 4326)::geography
        )
        on conflict (device_id)
        do update set
          vehicle_id = excluded.vehicle_id,
          imei = excluded.imei,
          lat = excluded.lat,
          lon = excluded.lon,
          speed = excluded.speed,
          heading = excluded.heading,
          device_time = excluded.device_time,
          server_time = excluded.server_time,
          ignition = excluded.ignition,
          battery = excluded.battery,
          gsm_signal = excluded.gsm_signal,
          satellite_count = excluded.satellite_count,
          gps_valid = excluded.gps_valid,
          charging = excluded.charging,
          defense = excluded.defense,
          position_type = excluded.position_type,
          geom = excluded.geom,
          updated_at = now()
      `,
      [
        device.id,
        device.vehicle_id,
        telemetry.imei,
        telemetry.lat,
        telemetry.lon,
        telemetry.speed ?? null,
        telemetry.heading ?? null,
        telemetry.deviceTime ?? null,
        serverTime,
        telemetry.ignition ?? null,
        telemetry.battery ?? null,
        telemetry.gsmSignal ?? null,
        telemetry.satelliteCount ?? null,
        telemetry.gpsValid ?? null,
        telemetry.charging ?? null,
        telemetry.defense ?? null,
        telemetry.positionType ?? null,
      ],
    );
  }

  private async broadcastTelemetry(
    telemetry: NormalizedTelemetry,
    vehicleId: string | null,
    status: "online" | "delayed" | "offline",
  ) {
    const audience = vehicleId
      ? await query<{ id: string }>(
          `
            select id from users where role = 'admin'
            union
            select user_id as id from user_vehicle_permissions where vehicle_id = $1
          `,
          [vehicleId],
        )
      : await query<{ id: string }>("select id from users where role = 'admin'");

    const payload = {
      vehicleId,
      imei: telemetry.imei,
      status,
      telemetry,
    };

    for (const user of audience) {
      this.io.to(`user:${user.id}`).emit("telemetry:update", payload);
    }
  }

  private async createOverspeedAlert(
    device: UpsertedDevice,
    telemetry: NormalizedTelemetry,
    serverTime: string,
  ): Promise<VehicleAlertSummary | null> {
    const speed = telemetry.speed ?? null;
    if (!device.vehicle_id || speed == null || speed <= 140 || telemetry.ignition !== true) {
      return null;
    }

    const recentSamples = await query<{
      speed: number | null;
      ignition: boolean | null;
      server_time: string;
    }>(
      `
        select speed, ignition, server_time
        from positions
        where device_id = $1
        order by server_time desc
        limit 3
      `,
      [device.id],
    );

    const [latestSample, previousSample] = recentSamples;
    const latestSampleTime = latestSample ? new Date(latestSample.server_time).getTime() : Number.NaN;
    const previousSampleTime = previousSample ? new Date(previousSample.server_time).getTime() : Number.NaN;
    const samplesAreCloseEnough = !Number.isNaN(latestSampleTime)
      && !Number.isNaN(previousSampleTime)
      && latestSampleTime - previousSampleTime <= 30_000;
    const hasSustainedOverspeed = Boolean(
      latestSample
      && previousSample
      && latestSample.ignition === true
      && previousSample.ignition === true
      && (latestSample.speed ?? 0) > 140
      && (previousSample.speed ?? 0) > 110
      && samplesAreCloseEnough,
    );

    if (!hasSustainedOverspeed) {
      return null;
    }

    const inserted = await queryOne<{
      id: string;
      vehicle_id: string;
      vehicle_name: string;
      vehicle_type: "passenger" | "delivery";
      imei: string | null;
      speed: number;
      triggered_at: string;
      acknowledged_at: string | null;
    }>(
      `
        with inserted as (
          insert into vehicle_alerts (
            vehicle_id,
            device_id,
            imei,
            alert_type,
            message,
            speed,
            triggered_at
          )
          select
            v.id,
            $2,
            $3,
            'delivery-overspeed',
            $4,
            $5,
            $6
          from vehicles v
          where v.id = $1
            and v.vehicle_type = 'delivery'
            and not exists (
              select 1
              from vehicle_alerts existing
              where existing.vehicle_id = v.id
                and existing.alert_type = 'delivery-overspeed'
                and existing.acknowledged_at is null
            )
          returning id, vehicle_id, imei, speed, triggered_at, acknowledged_at
        )
        select
          inserted.id,
          inserted.vehicle_id,
          v.name as vehicle_name,
          v.vehicle_type,
          inserted.imei,
          inserted.speed,
          inserted.triggered_at,
          inserted.acknowledged_at
        from inserted
        inner join vehicles v on v.id = inserted.vehicle_id
      `,
      [
        device.vehicle_id,
        device.id,
        telemetry.imei,
        `Przekroczenie 140 km/h przez ${telemetry.imei}`,
        speed,
        serverTime,
      ],
    );

    if (!inserted) {
      return null;
    }

    return {
      id: inserted.id,
      vehicleId: inserted.vehicle_id,
      vehicleName: inserted.vehicle_name,
      vehicleKind: inserted.vehicle_type,
      imei: inserted.imei,
      speed: inserted.speed,
      triggeredAt: inserted.triggered_at,
      acknowledgedAt: inserted.acknowledged_at,
    };
  }

  private async broadcastVehicleAlert(alert: VehicleAlertSummary) {
    const audience = await query<{ id: string }>(
      `
        select id from users where role = 'admin'
        union
        select user_id as id from user_vehicle_permissions where vehicle_id = $1
      `,
      [alert.vehicleId],
    );

    for (const user of audience) {
      this.io.to(`user:${user.id}`).emit("vehicle-alert:update", alert);
    }
  }
}
