import { Injectable, Logger } from '@nestjs/common';
import { plainToInstance } from 'class-transformer';
import { validate } from 'class-validator';
import { DevicePositionSnapshot } from '../devices/device-position';
import { toPointDto } from '../points/points.service';
import { PrismaService } from '../prisma/prisma.service';
import { RealtimeService } from '../realtime/realtime.service';
import { MqttLogMessageDto } from './dto/mqtt-messages.dto';
import { LocatorRegistryService } from './locator-registry.service';

@Injectable()
export class LogIngestService {
  private readonly logger = new Logger(LogIngestService.name);

  constructor(
    private readonly prisma: PrismaService,
    private readonly realtime: RealtimeService,
    private readonly locatorRegistry: LocatorRegistryService,
  ) {}

  // devices/<serial>/log carries the locator's identity in the topic itself
  // (no publisher device needs to be pre-registered); the job named in the
  // payload supplies the org, since the topic alone doesn't identify one.
  // `type` decides what happens to the reading: "log" persists a LocatePoint;
  // "status" is an ephemeral current-position update, broadcast live but
  // never written to locate_points.
  async handle(serial: string, rawPayload: string) {
    const msg = plainToInstance(MqttLogMessageDto, JSON.parse(rawPayload) as object);
    const errors = await validate(msg, { whitelist: true });
    if (errors.length > 0) {
      this.logger.warn(`Invalid log message from serial ${serial}: ${errors}`);
      return;
    }

    const job = await this.prisma.job.findUnique({ where: { id: msg.jobId } });
    if (!job) {
      this.logger.warn(`Unknown jobId ${msg.jobId} in log message from serial ${serial}`);
      return;
    }

    const locator = await this.locatorRegistry.resolve(job.orgId, serial);
    if (!locator.isActive) {
      this.logger.warn(`Message from disabled device "${serial}" ignored`);
      return;
    }

    const snapshot: DevicePositionSnapshot = {
      lat: msg.lat,
      lng: msg.lng,
      altitude: msg.alt ?? null,
      fixType: msg.fix ?? 'NONE',
      utilityType: msg.utility ?? 'UNKNOWN',
      hAccuracy: msg.hAcc ?? null,
      vAccuracy: msg.vAcc ?? null,
      satellites: msg.sats ?? null,
      hdop: msg.hdop ?? null,
      depth: msg.depth ?? null,
      frequencyHz: msg.freqHz ?? null,
      currentMa: msg.currentMa ?? null,
      signalDb: msg.signalDb ?? null,
      gainDb: msg.gainDb ?? null,
      locateMode: msg.mode ?? null,
      phaseDeg: msg.phaseDeg ?? null,
      compassDeg: msg.compassDeg ?? null,
      distortionPct: msg.distortionPct ?? null,
      recordedAt: msg.ts,
      jobId: job.id,
      jobTicketNumber: job.ticketNumber,
      jobTitle: job.title,
    };
    await this.locatorRegistry.recordPosition(job.orgId, locator.id, snapshot);

    if (msg.type === 'status') {
      this.realtime.publish(`job:${job.id}`, {
        type: 'status',
        jobId: job.id,
        deviceId: locator.id,
        serial,
        lat: msg.lat,
        lng: msg.lng,
        altitude: msg.alt ?? null,
        fixType: msg.fix ?? 'NONE',
        utilityType: msg.utility ?? 'UNKNOWN',
        hAccuracy: msg.hAcc ?? null,
        vAccuracy: msg.vAcc ?? null,
        satellites: msg.sats ?? null,
        hdop: msg.hdop ?? null,
        depth: msg.depth ?? null,
        frequencyHz: msg.freqHz ?? null,
        currentMa: msg.currentMa ?? null,
        signalDb: msg.signalDb ?? null,
        gainDb: msg.gainDb ?? null,
        locateMode: msg.mode ?? null,
        phaseDeg: msg.phaseDeg ?? null,
        compassDeg: msg.compassDeg ?? null,
        distortionPct: msg.distortionPct ?? null,
        recordedAt: msg.ts,
      });
      return;
    }

    const point = await this.prisma.locatePoint.create({
      data: {
        jobId: job.id,
        deviceId: locator.id,
        lat: msg.lat,
        lng: msg.lng,
        altitude: msg.alt,
        utilityType: msg.utility,
        sequence: msg.seq,
        fixType: msg.fix,
        hAccuracy: msg.hAcc,
        vAccuracy: msg.vAcc,
        satellites: msg.sats,
        hdop: msg.hdop,
        depth: msg.depth,
        frequencyHz: msg.freqHz,
        currentMa: msg.currentMa,
        signalDb: msg.signalDb,
        gainDb: msg.gainDb,
        locateMode: msg.mode,
        phaseDeg: msg.phaseDeg,
        compassDeg: msg.compassDeg,
        distortionPct: msg.distortionPct,
        recordedAt: new Date(msg.ts),
        raw: msg as object,
      },
    });

    this.realtime.publish(`job:${job.id}`, {
      type: 'points',
      jobId: job.id,
      points: [toPointDto(point)],
    });
    this.logger.debug(`Stored 1 point from serial ${serial} for job ${job.ticketNumber}`);
  }
}
