import * as fs from "node:fs";

import type { BaseClient } from "../src/base_client.js";
import {
  addListenerTyped,
  parserFromSchema,
  sendRequestTyped,
} from "../src/communicator.js";
import { create, PacketBaseSchema, TestDataSchema, toBinary, type TestData } from "../src/packets/message_common_pb.js";
import { connectTcp, connectTcpTls } from "../src/tcp_client.js";
import { connectNodeWs, connectNodeWss } from "../src/node_ws_client.js";

const HOST = "127.0.0.1";
const WS_PATH = "/";
const CONNECT_WINDOW_MS = 3000;
const REQUEST_WINDOW_MS = 10000;

type Mode = "tcp" | "ws" | "tls" | "wss";
type Phase = "send-push" | "requests" | "legacy-receive";

async function main(): Promise<void> {
  const args = parseArgs(process.argv.slice(2));
  console.log(`INFO typeName ts=${TestDataSchema.typeName}`);

  let client: BaseClient;
  try {
    client = await dialWithRetry(args.mode, args.port, args.cert);
  } catch (err) {
    fail("setup", errorMessage(err));
    process.exitCode = 1;
    return;
  }

  let ok = false;
  try {
    if (args.phase === "send-push") {
      ok = await runSendPush(client);
    } else if (args.phase === "legacy-receive") {
      await runLegacyReceive(client);
      ok = true;
    } else {
      ok = await runRequests(client);
    }
  } finally {
    await client.close();
  }

  if (!ok) {
    process.exitCode = 1;
  }
}

function parseArgs(argv: string[]): { mode: Mode; port: number; phase: Phase; cert?: string } {
  let mode: Mode = "tcp";
  let port: number | null = null;
  let phase: Phase = "send-push";
  let cert: string | undefined;

  for (const arg of argv) {
    if (arg.startsWith("--mode=")) {
      const value = arg.slice("--mode=".length);
      if (value === "tcp" || value === "ws" || value === "tls" || value === "wss") {
        mode = value;
      }
      continue;
    }
    if (arg.startsWith("--port=")) {
      port = Number(arg.slice("--port=".length));
      continue;
    }
    if (arg.startsWith("--phase=")) {
      const value = arg.slice("--phase=".length);
      if (value === "send-push" || value === "requests" || value === "legacy-receive") {
        phase = value;
      }
      continue;
    }
    if (arg.startsWith("--cert=")) {
      cert = arg.slice("--cert=".length);
    }
  }

  if (port === null || Number.isNaN(port)) {
    throw new Error("missing required --port");
  }
  return { mode, port, phase, cert };
}

async function dialWithRetry(mode: Mode, port: number, cert?: string): Promise<BaseClient> {
  const deadline = Date.now() + CONNECT_WINDOW_MS;
  let lastError: unknown;
  while (Date.now() < deadline) {
    try {
      return await dial(mode, port, cert);
    } catch (err) {
      lastError = err;
      await sleep(100);
    }
  }
  throw new Error(`connect ${mode}:${port} timed out: ${errorMessage(lastError)}`);
}

async function dial(mode: Mode, port: number, cert?: string): Promise<BaseClient> {
  const parserMap = new Map([[TestDataSchema.typeName, parserFromSchema(TestDataSchema)]]);
  if (mode === "tcp") {
    return connectTcp(HOST, port, 0, 0, parserMap);
  }
  if (mode === "ws") {
    return connectNodeWs(HOST, port, WS_PATH, 0, 0, parserMap);
  }
  if (cert === undefined || cert.length === 0) {
    throw new Error(`--cert is required for ${mode} mode`);
  }
  const ca = fs.readFileSync(cert);
  if (mode === "tls") {
    return connectTcpTls(HOST, port, { ca }, 0, 0, parserMap);
  }
  return connectNodeWss(HOST, port, WS_PATH, { ca }, 0, 0, parserMap);
}

async function runLegacyReceive(client: BaseClient): Promise<void> {
  const data = toBinary(TestDataSchema, create(TestDataSchema, { index: 101, message: "legacy fire from typescript" }));
  await client.communicator.queuePacket(create(PacketBaseSchema, { typeName: "TestData", nonce: 1, data }));
  await sleep(150);
}

async function runSendPush(client: BaseClient): Promise<boolean> {
  const pushed = new Promise<TestData>((resolve) => {
    addListenerTyped(client.communicator, TestDataSchema, (message) => {
      resolve(message);
    });
  });

  try {
    await client.communicator.send(
      create(TestDataSchema, {
        index: 101,
        message: "fire from typescript client",
      }),
    );
    passed("1", "fire-and-forget sent");

    const message = await withTimeout(pushed, REQUEST_WINDOW_MS, "push from dart server timed out");
    if (message.index !== 200 || message.message !== "push from dart server") {
      fail("2", `unexpected push index=${message.index} message=${JSON.stringify(message.message)}`);
      return false;
    }
    passed("2", "received push from dart server");
    return true;
  } catch (err) {
    fail("2", errorMessage(err));
    return false;
  }
}

async function runRequests(client: BaseClient): Promise<boolean> {
  try {
    const single = await sendRequestTyped(
      client.communicator,
      create(TestDataSchema, {
        index: 21,
        message: "single request from typescript",
      }),
      TestDataSchema,
      REQUEST_WINDOW_MS,
    );
    if (single.index !== 42 || single.message !== "echo: single request from typescript") {
      fail("3", `unexpected response index=${single.index} message=${JSON.stringify(single.message)}`);
      return false;
    }
    passed("3", "single request response matched");
  } catch (err) {
    fail("3", errorMessage(err));
    return false;
  }

  try {
    await Promise.all(
      Array.from({ length: concurrencyCount() }, async (_, idx) => {
        const index = 30 + idx;
        const message = `multi request ${idx} from typescript`;
        const response = await sendRequestTyped(
          client.communicator,
          create(TestDataSchema, { index, message }),
          TestDataSchema,
          REQUEST_WINDOW_MS,
        );
        if (response.index !== index * 2 || response.message !== `echo: ${message}`) {
          throw new Error(
            `request ${idx} got index=${response.index} message=${JSON.stringify(response.message)}`,
          );
        }
      }),
    );
    passed("4", "concurrent request responses matched");
    return true;
  } catch (err) {
    fail("4", errorMessage(err));
    return false;
  }
}

function concurrencyCount(): number {
  const value = Number.parseInt(process.env.PROTO_SOCKET_CROSSTEST_CONCURRENCY ?? "5", 10);
  return Number.isFinite(value) && value > 0 ? value : 5;
}

async function withTimeout<T>(promise: Promise<T>, timeoutMs: number, label: string): Promise<T> {
  let timer: ReturnType<typeof setTimeout> | undefined;
  try {
    return await Promise.race([
      promise,
      new Promise<T>((_, reject) => {
        timer = setTimeout(() => reject(new Error(label)), timeoutMs);
      }),
    ]);
  } finally {
    if (timer !== undefined) {
      clearTimeout(timer);
    }
  }
}

function passed(scenario: string, detail: string): void {
  console.log(`PASS scenario=${scenario} detail=${detail}`);
}

function fail(scenario: string, error: string): void {
  console.log(`FAIL scenario=${scenario} error=${error}`);
}

function errorMessage(err: unknown): string {
  return err instanceof Error ? err.message : String(err);
}

function sleep(ms: number): Promise<void> {
  return new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
}

void main().catch((err) => {
  fail("setup", errorMessage(err));
  process.exitCode = 1;
});
