import { Injectable, Logger } from '@nestjs/common';
import { Device, Job } from '@prisma/client';
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 { MqttPointsMessageDto } from './dto/mqtt-messages.dto';
import { LocatorRegistryService } from './locator-registry.service';

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

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

  async handle(device: Device, rawPayload: string) {
    const msg = plainToInstance(MqttPointsMessageDto, JSON.parse(rawPayload) as object);
    const errors = await validate(msg, { whitelist: true });
    if (errors.length > 0) {
      this.logger.warn(`Invalid points message from ${device.mqttUsername}: ${errors}`);
      return;
    }
    if (!msg.jobId && !msg.ticket) {
      this.logger.warn(`Points message from ${device.mqttUsername} has neither jobId nor ticket`);
      return;
    }

    const job = await this.resolveJob(device, msg);
    if (!job) {
      return;
    }

    const locator = !msg.serial || msg.serial === device.serialNumber
      ? device
      : await this.locatorRegistry.resolve(device.orgId, msg.serial);

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

    this.realtime.publish(`job:${job.id}`, {
      type: 'points',
      jobId: job.id,
      points: points.map(toPointDto),
    });

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

    this.logger.debug(`Stored ${points.length} points for job ${job.ticketNumber}`);
  }

  private async resolveJob(device: Device, msg: MqttPointsMessageDto): Promise<Job | null> {
    if (msg.jobId) {
      const job = await this.prisma.job.findFirst({
        where: { id: msg.jobId, orgId: device.orgId },
      });
      if (!job) {
        this.logger.warn(`Unknown jobId ${msg.jobId} from device ${device.mqttUsername}`);
      }
      return job;
    }

    const ticket = msg.ticket!;
    const existing = await this.prisma.job.findUnique({
      where: { orgId_ticketNumber: { orgId: device.orgId, ticketNumber: ticket } },
    });
    if (existing) {
      return existing;
    }

    // Field-first workflow: points arrive before anyone opened the ticket in the web
    // app. Create a stub so the data is never lost; metadata gets filled in later.
    this.logger.log(`Auto-creating stub job for unknown ticket ${ticket} (device ${device.mqttUsername})`);
    return this.prisma.job.create({
      data: {
        orgId: device.orgId,
        ticketNumber: ticket,
        title: `Ticket ${ticket} (device-created)`,
        status: 'IN_PROGRESS',
        source: 'DEVICE',
        createdByDeviceId: device.id,
        startedAt: new Date(),
      },
    });
  }
}
