import {
  Injectable,
  Logger,
  OnApplicationBootstrap,
  OnModuleDestroy,
} from "@nestjs/common";
import * as net from "net";

import { GpsIngestService } from "../gps-ingest.service";
import { buildAck, GT06_PROTOCOL, parseStream } from "./gt06.codec";

interface Session {
  buffer: Buffer;
  imei: string | null;
}

const MAX_BUFFER = 64 * 1024;

/**
 * Raw TCP listener for GT06 GPS trackers. Trackers open a socket, send a login
 * (IMEI), then stream location/heartbeat/alarm packets; we decode, persist via
 * {@link GpsIngestService}, and ACK login/heartbeat/alarm so the device keeps
 * the connection alive. Disabled when GT06_TCP_PORT=0.
 */
@Injectable()
export class Gt06Server implements OnApplicationBootstrap, OnModuleDestroy {
  private readonly logger = new Logger(Gt06Server.name);
  private server?: net.Server;
  private readonly sessions = new Map<net.Socket, Session>();

  constructor(private readonly gps: GpsIngestService) {}

  onApplicationBootstrap(): void {
    const port = Number(process.env.GT06_TCP_PORT ?? 5023);
    if (!port) {
      this.logger.log("GT06 TCP listener disabled (GT06_TCP_PORT=0)");
      return;
    }
    const host = process.env.GT06_TCP_HOST ?? "0.0.0.0";
    this.server = net.createServer((socket) => this.onConnection(socket));
    this.server.on("error", (err) => this.logger.error(`GT06 server error: ${String(err)}`));
    this.server.listen(port, host, () =>
      this.logger.log(`GT06 GPS tracker listener on ${host}:${port}`),
    );
  }

  onModuleDestroy(): void {
    for (const socket of this.sessions.keys()) socket.destroy();
    this.sessions.clear();
    this.server?.close();
  }

  private onConnection(socket: net.Socket): void {
    this.sessions.set(socket, { buffer: Buffer.alloc(0), imei: null });
    socket.on("data", (chunk) => void this.onData(socket, chunk));
    socket.on("error", () => this.sessions.delete(socket));
    socket.on("close", () => this.sessions.delete(socket));
  }

  private async onData(socket: net.Socket, chunk: Buffer): Promise<void> {
    const session = this.sessions.get(socket);
    if (!session) return;
    session.buffer = Buffer.concat([session.buffer, chunk]);
    if (session.buffer.length > MAX_BUFFER) session.buffer = Buffer.alloc(0); // drop garbage

    const { packets, rest } = parseStream(session.buffer);
    session.buffer = rest;

    for (const pkt of packets) {
      try {
        await this.handle(socket, session, pkt);
      } catch (err) {
        this.logger.error(`Failed to handle GT06 packet (${pkt.type}): ${String(err)}`);
      }
    }
  }

  private async handle(
    socket: net.Socket,
    session: Session,
    pkt: ReturnType<typeof parseStream>["packets"][number],
  ): Promise<void> {
    switch (pkt.type) {
      case "login":
        session.imei = pkt.imei;
        await this.gps.handleLogin(pkt.imei);
        socket.write(buildAck(GT06_PROTOCOL.LOGIN, pkt.serial));
        break;
      case "heartbeat":
        if (session.imei) await this.gps.handleHeartbeat(session.imei, pkt.status);
        socket.write(buildAck(GT06_PROTOCOL.HEARTBEAT, pkt.serial));
        break;
      case "location":
        if (session.imei) await this.gps.handleFix(session.imei, pkt.gps);
        break;
      case "alarm":
        if (session.imei)
          await this.gps.handleFix(session.imei, pkt.gps, pkt.status.alarm, pkt.status);
        socket.write(buildAck(GT06_PROTOCOL.ALARM, pkt.serial));
        break;
      default:
        break;
    }
  }
}
