import type { SignalInboundEvent, SignalTransport } from "./adapter.ts";

type JsonRpcResponse = {
  result?: unknown;
  error?: { message?: string; code?: number; data?: unknown };
};

function endpoint(baseUrl: string, path: string): string {
  return new URL(path, baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`).toString();
}

function parseSseChunk(buffer: string): { events: string[]; rest: string } {
  const events: string[] = [];
  let rest = buffer;
  for (;;) {
    const idx = rest.indexOf("\n\n");
    if (idx < 0) break;
    const raw = rest.slice(0, idx);
    rest = rest.slice(idx + 2);
    const data = raw.split(/\r?\n/)
      .filter((line) => line.startsWith("data:"))
      .map((line) => line.slice(5).trimStart())
      .join("\n")
      .trim();
    if (data) events.push(data);
  }
  return { events, rest };
}

export function createSignalHttpTransport(opts: {
  baseUrl: string;
  accountId?: string;
  eventsPath?: string;
  rpcPath?: string;
  reconnectMs?: number;
  reconnectMaxMs?: number;
  rpcTimeoutMs?: number;
  fetchImpl?: typeof fetch;
  sleepMs?: (ms: number) => Promise<void>;
}): SignalTransport {
  let stopped = false;
  let controller: AbortController | null = null;
  const accountId = opts.accountId ?? "default";
  const eventsPath = opts.eventsPath ?? `/api/v1/events?account=${encodeURIComponent(accountId)}`;
  const rpcPath = opts.rpcPath ?? "/api/v1/rpc";
  const reconnectMs = opts.reconnectMs ?? 1000;
  const reconnectMaxMs = opts.reconnectMaxMs ?? 30_000;
  const rpcTimeoutMs = opts.rpcTimeoutMs ?? 20_000;
  const fetcher = opts.fetchImpl ?? fetch;
  const sleep = opts.sleepMs ?? ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms)));

  async function readLoop(handler: (event: SignalInboundEvent) => Promise<void>) {
    let delayMs = reconnectMs;
    while (!stopped) {
      controller = new AbortController();
      try {
        const res = await fetcher(endpoint(opts.baseUrl, eventsPath), {
          headers: { Accept: "text/event-stream" },
          signal: controller.signal,
        });
        if (!res.ok || !res.body) throw new Error(`signal SSE failed: HTTP ${res.status}`);
        const reader = res.body.getReader();
        const decoder = new TextDecoder();
        let buffer = "";
        for (;;) {
          const { done, value } = await reader.read();
          if (done) break;
          buffer += decoder.decode(value, { stream: true }).replace(/\r\n/g, "\n");
          const parsed = parseSseChunk(buffer);
          buffer = parsed.rest;
          for (const data of parsed.events) {
            await handler(JSON.parse(data));
          }
        }
        delayMs = reconnectMs;
      } catch (e: any) {
        if (stopped || e?.name === "AbortError") return;
      }
      if (!stopped) {
        await sleep(delayMs);
        delayMs = Math.min(Math.max(delayMs * 2, reconnectMs), reconnectMaxMs);
      }
    }
  }

  return {
    async start(handler) {
      stopped = false;
      void readLoop(handler);
    },
    stop() {
      stopped = true;
      controller?.abort();
    },
    async sendMessage(recipient: string, text: string) {
      const rpcController = new AbortController();
      const timeout = setTimeout(() => rpcController.abort(), rpcTimeoutMs);
      let res: Response;
      try {
        res = await fetcher(endpoint(opts.baseUrl, rpcPath), {
          method: "POST",
          headers: { "content-type": "application/json" },
          signal: rpcController.signal,
          body: JSON.stringify({
            jsonrpc: "2.0",
            id: `dewlink-${Date.now()}`,
            method: "send",
            params: { account: accountId, recipient, message: text },
          }),
        });
      } catch (e: any) {
        if (e?.name === "AbortError") throw new Error(`signal JSON-RPC timed out after ${rpcTimeoutMs}ms`);
        throw e;
      } finally {
        clearTimeout(timeout);
      }
      if (res.status === 429) {
        const err: any = new Error("signal rate limited");
        err.retryAfter = Number(res.headers.get("retry-after") ?? 1);
        throw err;
      }
      if (!res.ok) throw new Error(`signal JSON-RPC failed: HTTP ${res.status}`);
      const body = await res.json() as JsonRpcResponse;
      if (body.error) throw new Error(body.error.message ?? `signal JSON-RPC error ${body.error.code ?? ""}`.trim());
      return body.result ?? body;
    },
  };
}
