import net from "net";
import fs from "fs";
import path from "path";

import { env } from "../../config/env.js";
import { detectProtocol } from "../index.js";
import type { TelemetryService } from "../../modules/telemetry/telemetry.service.js";

const summarizeChunk = (chunk: Buffer) => ({
  utf8: chunk
    .toString("utf8")
    .replace(/[^\x20-\x7E\r\n\t]/g, ".")
    .slice(0, 160),
  hex: chunk.toString("hex").slice(0, 160),
});

let lastTcpLogPruneAt = 0;

const getTcpLogFileForDate = (date: Date) => {
  const parsedPath = path.parse(env.LOG_TCP_FRAMES_FILE);
  const datePart = date.toISOString().slice(0, 10);
  const extension = parsedPath.ext || ".log";
  const baseName = parsedPath.ext ? parsedPath.name : parsedPath.base;

  return path.join(parsedPath.dir, `${baseName}-${datePart}${extension}`);
};

const pruneOldTcpLogs = (now: Date) => {
  if (now.getTime() - lastTcpLogPruneAt < 60 * 60 * 1000) {
    return;
  }

  lastTcpLogPruneAt = now.getTime();

  const parsedPath = path.parse(env.LOG_TCP_FRAMES_FILE);
  const extension = parsedPath.ext || ".log";
  const baseName = parsedPath.ext ? parsedPath.name : parsedPath.base;
  const rotatedLogPattern = new RegExp(
    `^${baseName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}-(\\d{4}-\\d{2}-\\d{2})${extension.replace(
      /[.*+?^${}()|[\]\\]/g,
      "\\$&",
    )}$`,
  );
  const cutoff = new Date(now);
  cutoff.setUTCDate(cutoff.getUTCDate() - env.LOG_TCP_FRAMES_RETENTION_DAYS);
  const cutoffDate = cutoff.toISOString().slice(0, 10);

  for (const entry of fs.readdirSync(parsedPath.dir)) {
    const match = rotatedLogPattern.exec(entry);
    if (!match || match[1] >= cutoffDate) {
      continue;
    }

    fs.unlinkSync(path.join(parsedPath.dir, entry));
  }
};

const appendTcpLog = (event: string, payload: Record<string, unknown>, options: { verbose?: boolean } = {}) => {
  if (!env.LOG_TCP_FRAMES) {
    return;
  }

  if (options.verbose && !env.LOG_TCP_FRAMES_VERBOSE) {
    return;
  }

  try {
    const now = new Date();
    const logFile = getTcpLogFileForDate(now);
    fs.mkdirSync(path.dirname(logFile), { recursive: true });
    pruneOldTcpLogs(now);
    fs.appendFileSync(
      logFile,
      `${JSON.stringify({ time: now.toISOString(), event, ...payload })}\n`,
      "utf8",
    );
  } catch (error) {
    console.error("tcp log file write error", error);
  }
};

const findAsciiBoundary = (buffer: Buffer) => {
  const boundaries = [buffer.indexOf(0x0a), buffer.indexOf(0x0d), buffer.indexOf(0x3b)].filter((value) => value >= 0);
  return boundaries.length > 0 ? Math.min(...boundaries) : -1;
};

const extractFrames = (buffer: Buffer) => {
  const frames: Buffer[] = [];
  let rest = buffer;

  while (rest.length > 0) {
    const isShortGt06 = rest.length >= 3 && rest[0] === 0x78 && rest[1] === 0x78;
    const isLongGt06 = rest.length >= 4 && rest[0] === 0x79 && rest[1] === 0x79;

    if (isShortGt06) {
      const totalLength = (rest[2] ?? 0) + 5;
      if (rest.length < totalLength) {
        break;
      }

      frames.push(rest.subarray(0, totalLength));
      rest = rest.subarray(totalLength);
      continue;
    }

    if (isLongGt06) {
      const totalLength = rest.readUInt16BE(2) + 6;
      if (rest.length < totalLength) {
        break;
      }

      frames.push(rest.subarray(0, totalLength));
      rest = rest.subarray(totalLength);
      continue;
    }

    const boundary = findAsciiBoundary(rest);
    if (boundary < 0) {
      break;
    }

    const frame = rest.subarray(0, boundary + 1);
    rest = rest.subarray(boundary + 1);

    if (frame.length > 0) {
      frames.push(frame);
    }
  }

  return { frames, rest };
};

export const createTrackerTcpServer = (telemetryService: TelemetryService) =>
  net.createServer((socket) => {
    let buffer = Buffer.alloc(0);
    let imei: string | undefined;
    const remote = `${socket.remoteAddress ?? "unknown"}:${socket.remotePort ?? "?"}`;

    socket.setKeepAlive(true, 30_000);
    socket.setTimeout(env.TCP_IDLE_TIMEOUT_MS);

    socket.on("data", async (chunk) => {
      const summary = summarizeChunk(chunk);
      appendTcpLog("chunk", { remote, bytes: chunk.length, utf8: summary.utf8, hex: summary.hex }, { verbose: true });

      buffer = Buffer.concat([buffer, chunk]);
      const { frames, rest } = extractFrames(buffer);
      buffer = Buffer.from(rest);

      for (const frame of frames) {
        try {
          appendTcpLog("frame", {
            remote,
            utf8: summarizeChunk(frame).utf8,
            hex: frame.toString("hex").slice(0, 160),
          }, { verbose: true });

          const packet = detectProtocol(frame, { imei });
          if (!packet) {
            appendTcpLog("no_protocol_match", { remote, bytes: frame.length, hex: frame.toString("hex").slice(0, 160) });
            continue;
          }

          if (packet.imei) {
            imei = packet.imei;
          }

          if (packet.telemetry) {
            await telemetryService.ingestTelemetry(packet.telemetry);
          } else if (packet.imei && (packet.deviceState || packet.eventType === "login" || packet.eventType === "heartbeat")) {
            await telemetryService.ingestDeviceState({
              imei: packet.imei,
              protocolName: packet.protocolName,
              rawPayload: packet.rawPayload,
              deviceState: packet.deviceState,
            });
          }

          if (packet.requiresAck && packet.ackPayload) {
            appendTcpLog("ack", { remote, ackHex: packet.ackPayload.toString("hex") }, { verbose: true });
            socket.write(packet.ackPayload);
          }
        } catch (error) {
          appendTcpLog("frame_processing_error", { remote, message: error instanceof Error ? error.message : String(error) });
          console.error("tcp frame processing error", error);
        }
      }
    });

    socket.on("timeout", () => {
      appendTcpLog("timeout", { remote });
      socket.destroy();
    });

    socket.on("error", (error) => {
      console.error("tcp socket error", error);
    });

    socket.on("close", () => {
      appendTcpLog("close", { remote });
    });
  });
