import { Injectable, NotFoundException } from '@nestjs/common';
import { LocatePoint, Prisma } from '@prisma/client';
import { PrismaService } from '../prisma/prisma.service';
import { CreatePointDto, QueryPointsDto } from './dto/points.dto';

// JSON-safe shape: BigInt id -> string, Decimal -> number
export interface PointDto {
  id: string;
  pointId: string | null;
  jobId: string;
  deviceId: string | null;
  origin: string;
  uploadPath: string | null;
  originClientId: string | null;
  createdAt: Date | null;
  lat: number;
  lng: number;
  altitude: number | null;
  utilityType: string;
  sequence: number | null;
  fixType: string;
  hAccuracy: number | null;
  vAccuracy: number | null;
  satellites: number | null;
  hdop: number | null;
  depth: number | null;
  frequencyHz: number | null;
  currentMa: number | null;
  signalDb: number | null;
  gainDb: number | null;
  locateMode: string | null;
  phaseDeg: number | null;
  compassDeg: number | null;
  distortionPct: number | null;
  recordedAt: Date;
  receivedAt: Date;
}

function num(value: unknown): number | null {
  return value === null || value === undefined ? null : Number(value);
}

export function toPointDto(p: LocatePoint): PointDto {
  return {
    id: p.id.toString(),
    pointId: p.pointId,
    jobId: p.jobId,
    deviceId: p.deviceId,
    origin: p.origin,
    uploadPath: p.uploadPath,
    originClientId: p.originClientId,
    createdAt: p.createdAt,
    lat: Number(p.lat),
    lng: Number(p.lng),
    altitude: num(p.altitude),
    utilityType: p.utilityType,
    sequence: p.sequence,
    fixType: p.fixType,
    hAccuracy: num(p.hAccuracy),
    vAccuracy: num(p.vAccuracy),
    satellites: p.satellites,
    hdop: num(p.hdop),
    depth: num(p.depth),
    frequencyHz: p.frequencyHz,
    currentMa: num(p.currentMa),
    signalDb: num(p.signalDb),
    gainDb: num(p.gainDb),
    locateMode: p.locateMode,
    phaseDeg: num(p.phaseDeg),
    compassDeg: num(p.compassDeg),
    distortionPct: num(p.distortionPct),
    recordedAt: p.recordedAt,
    receivedAt: p.receivedAt,
  };
}

@Injectable()
export class PointsService {
  constructor(private readonly prisma: PrismaService) {}

  async listForJob(orgId: string, jobId: string, query: QueryPointsDto): Promise<{ points: PointDto[] }> {
    const job = await this.prisma.job.findFirst({ where: { id: jobId, orgId }, select: { id: true } });
    if (!job) {
      throw new NotFoundException('Job not found');
    }
    return { points: await this.query({ jobId }, query) };
  }

  async listForOrg(orgId: string, query: QueryPointsDto): Promise<{ points: PointDto[] }> {
    return { points: await this.query({ job: { orgId } }, query) };
  }

  async createForJob(
    orgId: string,
    jobId: string,
    dto: CreatePointDto,
    deviceId: string | null = null,
  ): Promise<PointDto> {
    const job = await this.prisma.job.findFirst({ where: { id: jobId, orgId }, select: { id: true } });
    if (!job) {
      throw new NotFoundException('Job not found');
    }
    const { recordedAt, ...fields } = dto;
    const point = await this.prisma.locatePoint.create({
      data: {
        jobId,
        deviceId,
        ...fields,
        recordedAt: new Date(recordedAt),
      },
    });
    return toPointDto(point);
  }

  private async query(scope: Prisma.LocatePointWhereInput, query: QueryPointsDto): Promise<PointDto[]> {
    const where: Prisma.LocatePointWhereInput = {
      ...scope,
      ...(query.after && { recordedAt: { gt: new Date(query.after) } }),
      ...((query.from || query.to) && {
        recordedAt: {
          ...(query.after && { gt: new Date(query.after) }),
          ...(query.from && { gte: new Date(query.from) }),
          ...(query.to && { lte: new Date(query.to) }),
        },
      }),
    };

    if (query.bbox) {
      const [minLng, minLat, maxLng, maxLat] = query.bbox.split(',').map(Number);
      // Spatial filter runs on the generated geom column (GIST-indexed); the id list
      // is then fed back through Prisma so scope/time filters and typing stay uniform.
      const rows = await this.prisma.$queryRaw<{ id: bigint }[]>`
        SELECT id FROM locate_points
        WHERE geom && ST_MakeEnvelope(${minLng}, ${minLat}, ${maxLng}, ${maxLat}, 4326)
      `;
      where.id = { in: rows.map((r) => r.id) };
    }

    const points = await this.prisma.locatePoint.findMany({
      where,
      orderBy: [{ recordedAt: 'asc' }, { id: 'asc' }],
      take: query.limit ?? 5000,
    });
    return points.map(toPointDto);
  }
}
