import { randomUUID } from "node:crypto";
import { createServer as createHttpServer } from "node:http";
import { createServer as createTcpServer } from "node:net";

import {
  ARUSNAVI_B2_MODEL_PROFILE,
  buildB2HeaderAcknowledgement,
  buildB2PackageAcknowledgement,
  tryParseB2Header2,
  tryParseB2Package,
} from "../../../packages/arusnavi-b2-adapter/src/index.mjs";
import {
  DEVICE_DISCOVERY_SIGNAL_SCHEMA,
} from "../../../packages/device-protocol-contract/src/index.mjs";

export function createDeviceGatewayRuntime(options = {}) {
  const config = normalizeConfig(options);
  const sessions = new Map();
  const sessionsByAddress = new Map();
  const connectionWindows = new Map();
  let totalAccepted = 0;
  let totalRejected = 0;
  let totalDiscoveries = 0;
  let totalPackagesAcknowledged = 0;

  const tcpServer = createTcpServer((socket) => {
    const remoteAddress = normalizeRemoteAddress(socket.remoteAddress);
    if (
      sessions.size >= config.maxConcurrentSessions
      || currentAddressSessions(remoteAddress) >= config.maxSessionsPerAddress
      || !consumeConnectionPermit(remoteAddress)
    ) {
      totalRejected += 1;
      socket.destroy();
      return;
    }

    const sessionRef = `session:${randomUUID()}`;
    const session = {
      sessionRef,
      remoteAddress,
      buffer: Buffer.alloc(0),
      state: "awaiting-header",
      processing: false,
      rejected: false,
      closed: false,
    };
    sessions.set(socket, session);
    incrementAddressSessions(remoteAddress);
    totalAccepted += 1;
    socket.setNoDelay(true);
    socket.setTimeout(config.sessionTimeoutMs);

    socket.on("data", (chunk) => {
      if (session.closed || session.rejected) return;
      socket.pause();
      session.buffer = Buffer.concat(
        [session.buffer, chunk],
        session.buffer.length + chunk.length,
      );
      if (session.buffer.length > config.maxBufferedBytes) {
        rejectSession(socket, session);
        return;
      }
      if (session.processing) return;
      session.processing = true;
      void processSession(socket, session)
        .catch(() => rejectSession(socket, session))
        .finally(() => {
          session.processing = false;
          if (!session.closed && !session.rejected) socket.resume();
        });
    });
    socket.on("timeout", () => rejectSession(socket, session));
    socket.on("close", () => closeSession(socket, session));
    socket.on("error", () => closeSession(socket, session));
  });

  const healthServer = createHttpServer((request, response) => {
    response.setHeader("Content-Type", "application/json; charset=utf-8");
    response.setHeader("Cache-Control", "no-store");
    response.setHeader("X-Content-Type-Options", "nosniff");
    if (request.method !== "GET" || request.url !== "/healthz") {
      response.statusCode = 404;
      return response.end('{"ok":false,"error":"device_gateway_route_not_found"}\n');
    }
    response.statusCode = 200;
    return response.end(`${JSON.stringify({
      ok: true,
      service: "nodedc-device-gateway",
      protocolProfile: ARUSNAVI_B2_MODEL_PROFILE.profileRef,
      framing: ARUSNAVI_B2_MODEL_PROFILE.framing.status,
      tcpListener: config.listenEnabled ? "discovery-only" : "disabled",
      publicIngress: config.publicIngressEnabled
        ? "discovery-only"
        : "disabled",
      commandTransport: "disabled",
      sessions: {
        active: sessions.size,
        accepted: totalAccepted,
        rejected: totalRejected,
        discoveries: totalDiscoveries,
        packagesAcknowledged: totalPackagesAcknowledged,
      },
    })}\n`);
  });

  return {
    async start() {
      await listen(healthServer, config.healthPort, config.healthHost);
      if (config.listenEnabled) {
        await listen(tcpServer, config.tcpPort, config.tcpHost);
      }
      return {
        healthAddress: healthServer.address(),
        tcpAddress: config.listenEnabled ? tcpServer.address() : null,
      };
    },
    async stop() {
      for (const [socket, session] of sessions) {
        session.closed = true;
        socket.destroy();
      }
      await Promise.all([
        closeServer(healthServer),
        config.listenEnabled ? closeServer(tcpServer) : Promise.resolve(),
      ]);
    },
    status() {
      return {
        activeSessions: sessions.size,
        totalAccepted,
        totalRejected,
        totalDiscoveries,
        totalPackagesAcknowledged,
        commandTransport: "disabled",
        publicIngress: config.publicIngressEnabled
          ? "discovery-only"
          : "disabled",
      };
    },
  };

  async function processSession(socket, session) {
    while (!session.closed && !session.rejected) {
      if (session.state === "awaiting-header") {
        const parsed = tryParseB2Header2(session.buffer);
        if (parsed.status === "incomplete") return;

        const observedAt = config.now().toISOString();
        await config.onDiscovery?.({
          schemaVersion: DEVICE_DISCOVERY_SIGNAL_SCHEMA,
          sessionRef: session.sessionRef,
          modelProfileRef: ARUSNAVI_B2_MODEL_PROFILE.profileRef,
          protocol: ARUSNAVI_B2_MODEL_PROFILE.protocol,
          observedAt,
          identifier: {
            kind: parsed.identifier.kind,
            value: parsed.identifier.value,
          },
          evidence: parsed.evidence,
        });
        session.buffer = session.buffer.subarray(parsed.bytesConsumed);
        session.state = "packages";
        totalDiscoveries += 1;
        socket.write(buildB2HeaderAcknowledgement(
          Math.floor(new Date(observedAt).getTime() / 1000),
        ));
        continue;
      }

      const parsed = tryParseB2Package(session.buffer);
      if (parsed.status === "incomplete") return;
      session.buffer = session.buffer.subarray(parsed.bytesConsumed);
      totalPackagesAcknowledged += 1;
      socket.write(buildB2PackageAcknowledgement(parsed.packageNumber));
    }
  }

  function currentAddressSessions(remoteAddress) {
    return sessionsByAddress.get(remoteAddress) || 0;
  }

  function incrementAddressSessions(remoteAddress) {
    sessionsByAddress.set(
      remoteAddress,
      currentAddressSessions(remoteAddress) + 1,
    );
  }

  function decrementAddressSessions(remoteAddress) {
    const current = currentAddressSessions(remoteAddress);
    if (current <= 1) {
      sessionsByAddress.delete(remoteAddress);
    } else {
      sessionsByAddress.set(remoteAddress, current - 1);
    }
  }

  function consumeConnectionPermit(remoteAddress) {
    const nowMs = config.now().getTime();
    const current = connectionWindows.get(remoteAddress);
    if (!current || nowMs - current.startedAt >= 60_000) {
      connectionWindows.set(remoteAddress, {
        startedAt: nowMs,
        count: 1,
      });
      return true;
    }
    if (current.count >= config.maxConnectionsPerMinutePerAddress) {
      return false;
    }
    current.count += 1;
    return true;
  }

  function rejectSession(socket, session) {
    if (!session.rejected) {
      session.rejected = true;
      totalRejected += 1;
    }
    socket.destroy();
  }

  function closeSession(socket, session) {
    if (session.closed) return;
    session.closed = true;
    sessions.delete(socket);
    decrementAddressSessions(session.remoteAddress);
  }
}

function normalizeConfig(input) {
  const listenEnabled = input.listenEnabled === true;
  const publicIngressEnabled = input.publicIngressEnabled === true;
  if (publicIngressEnabled && !listenEnabled) {
    throw new TypeError("device_gateway_public_ingress_listener_required");
  }
  if (
    publicIngressEnabled
    && typeof input.onDiscovery !== "function"
  ) {
    throw new TypeError("device_gateway_discovery_sink_required");
  }
  return {
    listenEnabled,
    publicIngressEnabled,
    healthHost: normalizeHealthHost(input.healthHost, "127.0.0.1"),
    healthPort: parseInteger(
      input.healthPort,
      18121,
      0,
      65535,
      "device_gateway_health_port_invalid",
    ),
    tcpHost: normalizeTcpHost(
      input.tcpHost,
      publicIngressEnabled ? "0.0.0.0" : "127.0.0.1",
      publicIngressEnabled,
    ),
    tcpPort: parseInteger(
      input.tcpPort,
      9921,
      0,
      65535,
      "device_gateway_tcp_port_invalid",
    ),
    maxBufferedBytes: parseInteger(
      input.maxBufferedBytes,
      ARUSNAVI_B2_MODEL_PROFILE.framing.maxBufferedBytes,
      1024,
      ARUSNAVI_B2_MODEL_PROFILE.framing.maxBufferedBytes,
      "device_gateway_buffer_limit_invalid",
    ),
    maxConcurrentSessions: parseInteger(
      input.maxConcurrentSessions,
      100,
      1,
      10000,
      "device_gateway_session_limit_invalid",
    ),
    maxSessionsPerAddress: parseInteger(
      input.maxSessionsPerAddress,
      10,
      1,
      1000,
      "device_gateway_address_session_limit_invalid",
    ),
    maxConnectionsPerMinutePerAddress: parseInteger(
      input.maxConnectionsPerMinutePerAddress,
      30,
      1,
      10000,
      "device_gateway_address_rate_limit_invalid",
    ),
    sessionTimeoutMs: parseInteger(
      input.sessionTimeoutMs,
      10000,
      100,
      60000,
      "device_gateway_session_timeout_invalid",
    ),
    onDiscovery: typeof input.onDiscovery === "function"
      ? input.onDiscovery
      : undefined,
    now: typeof input.now === "function" ? input.now : () => new Date(),
  };
}

function normalizeHealthHost(value, fallback) {
  const normalized = String(value || fallback).trim();
  if (!["127.0.0.1", "::1", "0.0.0.0", "::"].includes(normalized)) {
    throw new TypeError("device_gateway_health_host_invalid");
  }
  return normalized;
}

function normalizeTcpHost(value, fallback, publicIngressEnabled) {
  const normalized = String(value || fallback).trim();
  const allowed = publicIngressEnabled
    ? ["0.0.0.0", "::"]
    : ["127.0.0.1", "::1"];
  if (!allowed.includes(normalized)) {
    throw new TypeError(
      publicIngressEnabled
        ? "device_gateway_public_ingress_host_invalid"
        : "device_gateway_baseline_loopback_only",
    );
  }
  return normalized;
}

function normalizeRemoteAddress(value) {
  const normalized = String(value || "unknown").trim();
  return normalized.slice(0, 64) || "unknown";
}

function parseInteger(value, fallback, minimum, maximum, errorCode) {
  const parsed = Number(value ?? fallback);
  if (
    !Number.isSafeInteger(parsed)
    || parsed < minimum
    || parsed > maximum
  ) {
    throw new TypeError(errorCode);
  }
  return parsed;
}

function listen(server, port, host) {
  return new Promise((resolve, reject) => {
    server.once("error", reject);
    server.listen(port, host, resolve);
  });
}

function closeServer(server) {
  if (!server.listening) return Promise.resolve();
  return new Promise((resolve, reject) => {
    server.close((error) => (error ? reject(error) : resolve()));
  });
}
