import { isError } from "es-toolkit";

import type { CheckResult, RawTargetConfig } from "../../types";
import type { CheckerContext, CheckerDefinition, CheckerValidationInput, ResolveContext } from "../types";
import type { ResolvedWsExpectConfig, ResolvedWsTarget, WsTargetConfig } from "./types";

import { errorFailure } from "../../expect/failure";
import { checkValueExpectation } from "../../expect/value";
import { parseSize } from "../../utils";
import { checkConnected, checkHandshakeHeaders, checkMessage } from "./expect";
import { normalizeTargetExpect } from "./normalize";
import { wsCheckerSchemas } from "./schema";
import { validateWsConfig } from "./validate";

const DEFAULT_MAX_MESSAGE_BYTES = 4096;
const DEFAULT_RECEIVE_TIMEOUT = 5000;

type MessageReceiveResult = { data: string; ok: true; size: number } | { error: string; ok: false };

type WsConnectResult = { error: string; ok: false } | { headers: Record<string, string>; ok: true; ws: WebSocket };

export class WsChecker implements CheckerDefinition<ResolvedWsTarget> {
  readonly configKey = "ws";

  readonly schemas = wsCheckerSchemas;

  readonly type = "ws";

  buildDetail(observation: Record<string, unknown>): null | string {
    const connected = observation["connected"];
    if (connected !== true) {
      const error = observation["error"];
      return typeof error === "string" ? `connection failed: ${error}` : "not connected";
    }
    const connectTimeMs = observation["connectTimeMs"];
    const message = observation["message"];
    const parts: string[] = [`connected in ${typeof connectTimeMs === "number" ? connectTimeMs : "?"}ms`];
    if (typeof message === "string" && message.length > 0) {
      parts.push(`message: ${truncateMessage(message)}`);
    }
    return parts.join(", ");
  }

  async execute(t: ResolvedWsTarget, ctx: CheckerContext): Promise<CheckResult> {
    const timestamp = new Date().toISOString();
    const start = performance.now();
    const expect = t.expect;

    try {
      const connectResult = await wsConnect(t.ws, ctx.signal);

      if (!connectResult.ok) {
        const durationMs = Math.round(performance.now() - start);
        const observation: Record<string, unknown> = {
          connected: false,
          connectTimeMs: null,
          error: connectResult.error,
          message: null,
          messageSize: null,
        };
        if (expect?.connected === false) {
          return {
            detail: null,
            durationMs,
            failure: null,
            matched: true,
            observation,
            targetId: t.id,
            timestamp,
          };
        }
        return {
          detail: null,
          durationMs,
          failure: errorFailure("connect", "connect", connectResult.error),
          matched: false,
          observation,
          targetId: t.id,
          timestamp,
        };
      }

      const ws = connectResult.ws;
      const connectTimeMs = Math.round(performance.now() - start);
      const handshakeHeaders = connectResult.headers;

      if (ctx.signal.aborted) {
        closeWs(ws);
        const durationMs = Math.round(performance.now() - start);
        return {
          detail: null,
          durationMs,
          failure: errorFailure("connect", "connect", `连接超时 (${t.timeoutMs}ms)`),
          matched: false,
          observation: null,
          targetId: t.id,
          timestamp,
        };
      }

      const expectedConnected = expect?.connected ?? true;
      const connectedResult = checkConnected(true, expectedConnected);
      if (!connectedResult.matched) {
        closeWs(ws);
        const durationMs = Math.round(performance.now() - start);
        return {
          detail: null,
          durationMs,
          failure: connectedResult.failure,
          matched: false,
          observation: {
            connected: true,
            connectTimeMs,
            error: null,
            handshakeHeaders,
            message: null,
            messageSize: null,
          },
          targetId: t.id,
          timestamp,
        };
      }

      if (expect?.handshakeHeaders) {
        const headersResult = checkHandshakeHeaders(handshakeHeaders, expect.handshakeHeaders);
        if (!headersResult.matched) {
          closeWs(ws);
          const durationMs = Math.round(performance.now() - start);
          return {
            detail: null,
            durationMs,
            failure: headersResult.failure,
            matched: false,
            observation: {
              connected: true,
              connectTimeMs,
              error: null,
              handshakeHeaders,
              message: null,
              messageSize: null,
            },
            targetId: t.id,
            timestamp,
          };
        }
      }

      let messageText: null | string = null;
      let messageSize: null | number = null;

      if (t.ws.send) {
        const messageResult: MessageReceiveResult = await wsSendAndReceive(
          ws,
          t.ws.send,
          t.ws.receiveTimeout,
          t.ws.maxMessageBytes,
          ctx.signal,
        );
        if (!messageResult.ok) {
          closeWs(ws);
          const durationMs = Math.round(performance.now() - start);
          const observation: Record<string, unknown> = {
            connected: true,
            connectTimeMs,
            error: messageResult.error,
            handshakeHeaders,
            message: null,
            messageSize: null,
          };
          return {
            detail: null,
            durationMs,
            failure: errorFailure("message", "message", messageResult.error),
            matched: false,
            observation,
            targetId: t.id,
            timestamp,
          };
        }

        messageText = truncateMessageForObservation(messageResult.data);
        messageSize = messageResult.size;

        if (expect?.message) {
          const msgCheck = checkMessage(messageResult.data, expect.message);
          if (!msgCheck.matched) {
            closeWs(ws);
            const durationMs = Math.round(performance.now() - start);
            return {
              detail: null,
              durationMs,
              failure: msgCheck.failure,
              matched: false,
              observation: {
                connected: true,
                connectTimeMs,
                error: null,
                handshakeHeaders,
                message: messageText,
                messageSize,
              },
              targetId: t.id,
              timestamp,
            };
          }
        }
      }

      closeWs(ws);

      const observation: Record<string, unknown> = {
        connected: true,
        connectTimeMs,
        error: null,
        handshakeHeaders,
        message: messageText,
        messageSize,
      };

      if (expect?.connectTimeMs) {
        const ctResult = checkValueExpectation(connectTimeMs, expect.connectTimeMs, {
          message: "connectTimeMs mismatch",
          path: "connectTimeMs",
          phase: "connect",
        });
        if (!ctResult.matched) {
          const durationMs = Math.round(performance.now() - start);
          return {
            detail: null,
            durationMs,
            failure: ctResult.failure,
            matched: false,
            observation,
            targetId: t.id,
            timestamp,
          };
        }
      }

      const durationMs = Math.round(performance.now() - start);
      const durationResult = checkValueExpectation(durationMs, expect?.durationMs, {
        message: "durationMs mismatch",
        path: "durationMs",
        phase: "duration",
      });
      if (!durationResult.matched) {
        return {
          detail: null,
          durationMs,
          failure: durationResult.failure,
          matched: false,
          observation,
          targetId: t.id,
          timestamp,
        };
      }

      return {
        detail: null,
        durationMs,
        failure: null,
        matched: true,
        observation,
        targetId: t.id,
        timestamp,
      };
    } catch (error) {
      const durationMs = Math.round(performance.now() - start);
      return {
        detail: null,
        durationMs,
        failure: errorFailure(
          "connect",
          "connect",
          ctx.signal.aborted ? `连接超时 (${t.timeoutMs}ms)` : isError(error) ? error.message : String(error),
        ),
        matched: false,
        observation: null,
        targetId: t.id,
        timestamp,
      };
    }
  }

  normalize(target: RawTargetConfig): RawTargetConfig {
    return normalizeTargetExpect(target);
  }

  resolve(target: RawTargetConfig, context: ResolveContext): ResolvedWsTarget {
    const t = target as RawTargetConfig & { type: "ws"; ws: WsTargetConfig };

    const maxMessageBytes = parseSize(t.ws.maxMessageBytes ?? DEFAULT_MAX_MESSAGE_BYTES);
    const receiveTimeout = t.ws.receiveTimeout ?? DEFAULT_RECEIVE_TIMEOUT;

    const expect = target.expect as ResolvedWsExpectConfig | undefined;
    const resolvedExpect: ResolvedWsExpectConfig = expect
      ? { ...expect, connected: expect.connected ?? true }
      : { connected: true };

    return {
      description: null,
      expect: resolvedExpect,
      group: target.group ?? "default",
      id: t.id,
      intervalMs: context.defaultIntervalMs,
      name: t.name ?? null,
      timeoutMs: context.defaultTimeoutMs,
      type: "ws",
      ws: {
        headers: { ...(t.ws.headers ?? {}) },
        ignoreSSL: t.ws.ignoreSSL ?? false,
        maxMessageBytes,
        receiveTimeout,
        send: t.ws.send,
        subprotocols: t.ws.subprotocols ?? [],
        url: t.ws.url,
      },
    } satisfies ResolvedWsTarget;
  }

  serialize(t: ResolvedWsTarget): { config: string; target: string } {
    return {
      config: JSON.stringify({
        headers: t.ws.headers,
        ignoreSSL: t.ws.ignoreSSL,
        maxMessageBytes: t.ws.maxMessageBytes,
        receiveTimeout: t.ws.receiveTimeout,
        send: t.ws.send,
        subprotocols: t.ws.subprotocols,
        url: t.ws.url,
      }),
      target: t.ws.url,
    };
  }

  validate(input: CheckerValidationInput) {
    return validateWsConfig(input);
  }
}

function closeWs(ws: WebSocket) {
  try {
    if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
      ws.close();
    }
  } catch {
    /* best-effort close */
  }
}

function simplifyConnectError(message: string): string {
  const lower = message.toLowerCase();
  if (lower.includes("econnrefused") || lower.includes("connection refused")) return "connection refused";
  if (lower.includes("enoent") || lower.includes("not found")) return "host not found";
  if (lower.includes("etimedout") || lower.includes("timed out")) return "connection timed out";
  if (lower.includes("econnreset") || lower.includes("reset")) return "connection reset";
  if (lower.includes("enetwork") || lower.includes("network")) return "network error";
  if (lower.includes("certificate") || lower.includes("cert") || lower.includes("ssl") || lower.includes("tls")) {
    return "tls error: certificate verification failed";
  }
  if (lower.includes("401") || lower.includes("unauthorized")) return "handshake failed: unauthorized (401)";
  if (lower.includes("403") || lower.includes("forbidden")) return "handshake failed: forbidden (403)";
  if (lower.includes("404") || lower.includes("not found")) return "handshake failed: not found (404)";
  if (lower.includes("handshake") || lower.includes("upgrade")) return `handshake failed: ${message}`;
  return message;
}

function truncateMessage(message: string, maxLen = 80): string {
  if (message.length <= maxLen) return message;
  return `${message.slice(0, maxLen)}…`;
}

function truncateMessageForObservation(message: string, maxLen = 256): string {
  if (message.length <= maxLen) return message;
  return message.slice(0, maxLen);
}

async function wsConnect(config: ResolvedWsTarget["ws"], signal: AbortSignal): Promise<WsConnectResult> {
  if (signal.aborted) {
    return { error: "连接已取消", ok: false };
  }

  let settled = false;
  let resolveFn: ((result: WsConnectResult) => void) | undefined;
  const connectPromise = new Promise<WsConnectResult>((resolve) => {
    resolveFn = resolve;
  });

  const settle = (result: WsConnectResult) => {
    if (settled) return;
    settled = true;
    resolveFn!(result);
  };

  try {
    const wsOptions: Bun.WebSocketOptions = {};
    if (Object.keys(config.headers).length > 0) {
      (wsOptions as Record<string, unknown>)["headers"] = config.headers;
    }
    if (config.ignoreSSL) {
      (wsOptions as Record<string, unknown>)["tls"] = { rejectUnauthorized: false };
    }
    if (config.subprotocols.length > 0) {
      (wsOptions as Record<string, unknown>)["protocols"] = config.subprotocols;
    }

    const ws = new WebSocket(config.url, wsOptions as never);

    const onAbort = () => {
      settle({ error: "连接超时", ok: false });
      closeWs(ws);
    };
    signal.addEventListener("abort", onAbort, { once: true });

    ws.addEventListener("open", () => {
      signal.removeEventListener("abort", onAbort);
      const headers: Record<string, string> = {};
      if (ws.protocol) {
        headers["sec-websocket-protocol"] = ws.protocol;
      }
      settle({ headers, ok: true, ws });
    });

    ws.addEventListener("error", () => {
      signal.removeEventListener("abort", onAbort);
      settle({ error: "连接失败", ok: false });
      try {
        ws.close();
      } catch {
        /* best-effort */
      }
    });

    ws.addEventListener("close", (event) => {
      signal.removeEventListener("abort", onAbort);
      if (!settled) {
        const code = event.code;
        const reason = event.reason || "";
        if (code >= 1000 && code < 2000) {
          settle({
            error: `handshake failed: server closed with code ${code}${reason ? `: ${reason}` : ""}`,
            ok: false,
          });
        } else {
          settle({ error: "连接关闭", ok: false });
        }
      }
    });

    return await connectPromise;
  } catch (error) {
    if (signal.aborted) {
      return { error: "连接超时", ok: false };
    }
    const message = isError(error) ? error.message : String(error);
    return { error: simplifyConnectError(message), ok: false };
  }
}

async function wsSendAndReceive(
  ws: WebSocket,
  sendText: string,
  receiveTimeout: number,
  maxMessageBytes: number,
  signal: AbortSignal,
): Promise<MessageReceiveResult> {
  let settled = false;
  let resolveFn: ((result: MessageReceiveResult) => void) | undefined;
  const messagePromise = new Promise<MessageReceiveResult>((resolve) => {
    resolveFn = resolve;
  });

  const settle = (result: MessageReceiveResult) => {
    if (settled) return;
    settled = true;
    resolveFn!(result);
  };

  const timer = setTimeout(() => {
    settle({ error: `等待响应超时 (${receiveTimeout}ms)`, ok: false });
  }, receiveTimeout);

  const onAbort = () => {
    settle({ error: "探测已取消", ok: false });
  };
  signal.addEventListener("abort", onAbort, { once: true });

  const cleanup = () => {
    clearTimeout(timer);
    signal.removeEventListener("abort", onAbort);
    ws.removeEventListener("message", onMessage);
    ws.removeEventListener("close", onClose);
    ws.removeEventListener("error", onError);
  };

  const onMessage = (event: MessageEvent) => {
    if (settled) return;
    const data = typeof event.data === "string" ? event.data : new TextDecoder().decode(event.data as Uint8Array);
    const size = new TextEncoder().encode(data).byteLength;
    if (size > maxMessageBytes) {
      settle({ error: `消息超过 ${maxMessageBytes} 字节限制 (${size} bytes)`, ok: false });
      return;
    }
    settle({ data, ok: true, size });
  };

  const onClose = (event: CloseEvent) => {
    if (settled) return;
    const code = event.code;
    const reason = event.reason || "";
    settle({ error: `服务端关闭连接: code=${code}${reason ? ` reason=${reason}` : ""}`, ok: false });
  };

  const onError = () => {
    if (settled) return;
    settle({ error: "连接错误", ok: false });
  };

  ws.addEventListener("message", onMessage);
  ws.addEventListener("close", onClose);
  ws.addEventListener("error", onError);

  try {
    ws.send(sendText);
  } catch (error) {
    cleanup();
    return { error: isError(error) ? error.message : "发送消息失败", ok: false };
  }

  const result = await messagePromise;
  cleanup();
  return result;
}
