import { BadRequestException, ConflictException, Injectable, NotFoundException } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { toPointDto } from '../points/points.service';
import { RealtimeService } from '../realtime/realtime.service';
import { DevicePositionSnapshot } from './device-position';
import { CreateDeviceDto, UpdateDeviceDto } from './dto/devices.dto';

@Injectable()
export class DevicesService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly realtime: RealtimeService,
  ) {}

  list(orgId: string) {
    return this.prisma.device.findMany({
      where: { orgId },
      orderBy: { createdAt: 'asc' },
    });
  }

  async create(orgId: string, dto: CreateDeviceDto) {
    if (!dto.mqttUsername && !dto.serialNumber) {
      throw new BadRequestException('A device needs a serial number, an MQTT username, or both');
    }
    if (dto.mqttUsername) {
      const existing = await this.prisma.device.findUnique({
        where: { mqttUsername: dto.mqttUsername },
      });
      if (existing) {
        throw new ConflictException('A device with this MQTT username already exists');
      }
    }
    if (dto.serialNumber) {
      const existing = await this.prisma.device.findUnique({
        where: { serialNumber: dto.serialNumber },
      });
      if (existing) {
        throw new ConflictException('A device with this serial number already exists');
      }
    }
    const device = await this.prisma.device.create({
      data: { orgId, name: dto.name, mqttUsername: dto.mqttUsername, serialNumber: dto.serialNumber },
    });
    return {
      ...device,
      provisioning: device.mqttUsername
        ? {
            mqttUsername: device.mqttUsername,
            pointsTopic: `devices/${device.mqttUsername}/points`,
            jobsTopic: `devices/${device.mqttUsername}/jobs`,
            note: 'Broker credentials must be created separately (mosquitto_passwd) until dynamic broker auth lands.',
          }
        : {
            logTopic: device.serialNumber ? `devices/${device.serialNumber}/log` : undefined,
            note: `Relayed locator: points published by a gateway should carry "serial": "${device.serialNumber}", or a direct-connect locator can publish single readings to devices/${device.serialNumber}/log.`,
          },
    };
  }

  async update(orgId: string, deviceId: string, dto: UpdateDeviceDto) {
    await this.get(orgId, deviceId);
    const { disabledReason, ...rest } = dto;
    const device = await this.prisma.device.update({
      where: { id: deviceId },
      data: {
        ...rest,
        // A reason only makes sense while disabled; re-enabling always clears it.
        ...(dto.isActive === true && { disabledReason: null }),
        ...(dto.isActive === false && { disabledReason: disabledReason ?? null }),
      },
    });
    this.realtime.publish(`org:${orgId}:devices`, {
      type: 'device',
      orgId,
      deviceId,
      isActive: device.isActive,
      disabledReason: device.disabledReason,
    });
    return device;
  }

  async remove(orgId: string, deviceId: string) {
    await this.get(orgId, deviceId);
    await this.prisma.device.delete({ where: { id: deviceId } });
    return { ok: true };
  }

  async getLocation(orgId: string, deviceId: string) {
    const device = await this.get(orgId, deviceId);
    const point = await this.prisma.locatePoint.findFirst({
      where: { deviceId },
      orderBy: { recordedAt: 'desc' },
      include: { job: { select: { id: true, ticketNumber: true, title: true } } },
    });

    // A live "status" ping is never persisted as a LocatePoint, so the most
    // recent position may only exist on the device's lastPosition snapshot —
    // compare timestamps and use whichever source is actually newer.
    const live = device.lastPosition as unknown as DevicePositionSnapshot | null;
    if (live && device.lastPositionAt && (!point || device.lastPositionAt > point.recordedAt)) {
      return {
        point: {
          id: `live-${deviceId}`,
          lat: live.lat,
          lng: live.lng,
          altitude: live.altitude,
          utilityType: live.utilityType,
          fixType: live.fixType,
          sequence: null,
          recordedAt: live.recordedAt,
          hAccuracy: live.hAccuracy,
          vAccuracy: live.vAccuracy,
          satellites: live.satellites,
          hdop: live.hdop,
          depth: live.depth,
          frequencyHz: live.frequencyHz,
          currentMa: live.currentMa,
          signalDb: live.signalDb,
          gainDb: live.gainDb,
          locateMode: live.locateMode,
          phaseDeg: live.phaseDeg,
          compassDeg: live.compassDeg,
          distortionPct: live.distortionPct,
        },
        job: { id: live.jobId, ticketNumber: live.jobTicketNumber, title: live.jobTitle },
      };
    }

    if (!point) {
      return { point: null, job: null };
    }
    const { job, ...rest } = point;
    return { point: toPointDto(rest), job };
  }

  private async get(orgId: string, deviceId: string) {
    const device = await this.prisma.device.findFirst({ where: { id: deviceId, orgId } });
    if (!device) {
      throw new NotFoundException('Device not found');
    }
    return device;
  }
}
