import crypto from "node:crypto";
import type { Dispatcher } from "undici";

/**
 * Cliente base da Bybit (P2P Open API, v5). Único lugar que conhece a
 * apiKey/secret e monta a assinatura.
 *
 * Assinatura (HMAC-SHA256, doc P2P):
 *   payload = timestamp + apiKey + recvWindow + (queryString | jsonBody)
 *   sign    = HMAC_SHA256(payload, secret) -> hex
 * Headers: X-BAPI-API-KEY, X-BAPI-TIMESTAMP, X-BAPI-SIGN, X-BAPI-RECV-WINDOW,
 *          X-BAPI-SIGN-TYPE: 2 (HMAC).
 *
 * Envelope: { retCode, retMsg, result, retExtInfo, time }. Sucesso = retCode 0.
 */

export interface BybitConfig {
  apiKey: string;
  secretKey: string;
  baseUrl?: string;
  recvWindow?: number;
  /** Dispatcher do undici (ProxyAgent) por conta. */
  dispatcher?: Dispatcher;
}

export interface BybitRet<T> {
  retCode: number;
  retMsg: string;
  result: T;
  retExtInfo?: unknown;
  time?: number;
}

export class BybitError extends Error {
  constructor(
    public status: number,
    public retCode: number | null,
    message: string,
    public raw?: unknown,
  ) {
    super(message);
    this.name = "BybitError";
  }
}

export interface BybitRequest {
  method: "GET" | "POST";
  path: string;
  /** Para GET: vira query string. Para POST: vira JSON body. */
  params?: object;
}

export class BybitClient {
  private readonly apiKey: string;
  private readonly secretKey: string;
  private readonly baseUrl: string;
  private readonly recvWindow: number;
  private readonly dispatcher?: Dispatcher;

  constructor(cfg: BybitConfig) {
    if (!cfg.apiKey || !cfg.secretKey) {
      throw new Error("apiKey e secretKey são obrigatórios (Bybit)");
    }
    this.apiKey = cfg.apiKey;
    this.secretKey = cfg.secretKey;
    this.baseUrl = cfg.baseUrl ?? "https://api.bybit.com";
    this.recvWindow = cfg.recvWindow ?? 5000;
    this.dispatcher = cfg.dispatcher;
  }

  private sign(payload: string): string {
    return crypto.createHmac("sha256", this.secretKey).update(payload).digest("hex");
  }

  async request<T>(req: BybitRequest): Promise<T> {
    const timestamp = String(Date.now());
    const recv = String(this.recvWindow);

    let url = `${this.baseUrl}${req.path}`;
    let body: string | undefined;
    let signPayloadTail = "";

    if (req.method === "GET") {
      const qs = buildQueryString((req.params ?? {}) as Record<string, unknown>);
      if (qs) url += `?${qs}`;
      signPayloadTail = qs;
    } else {
      body = JSON.stringify(req.params ?? {});
      signPayloadTail = body;
    }

    const sign = this.sign(timestamp + this.apiKey + recv + signPayloadTail);

    const headers: Record<string, string> = {
      "X-BAPI-API-KEY": this.apiKey,
      "X-BAPI-TIMESTAMP": timestamp,
      "X-BAPI-RECV-WINDOW": recv,
      "X-BAPI-SIGN": sign,
      "X-BAPI-SIGN-TYPE": "2",
    };
    if (req.method === "POST") headers["Content-Type"] = "application/json";

    const init: RequestInit & { dispatcher?: Dispatcher } = {
      method: req.method,
      headers,
      body,
    };
    if (this.dispatcher) init.dispatcher = this.dispatcher;

    const res = await fetch(url, init);
    const text = await res.text();
    let parsed: unknown;
    try {
      parsed = text ? JSON.parse(text) : null;
    } catch {
      throw new BybitError(res.status, null, `Resposta não-JSON: ${text}`, text);
    }

    if (!res.ok) {
      const e = parsed as { retCode?: number; retMsg?: string };
      throw new BybitError(res.status, e?.retCode ?? null, e?.retMsg ?? `HTTP ${res.status}`, parsed);
    }

    const env = parsed as BybitRet<T>;
    if (env && typeof env === "object" && "retCode" in env) {
      if (env.retCode !== 0) {
        throw new BybitError(res.status, env.retCode, env.retMsg ?? "Falha na API Bybit", env);
      }
      return env.result;
    }
    return parsed as T;
  }
}

/**
 * Monta a query string. A Bybit assina exatamente a string enviada, então
 * usamos a mesma para assinar e para a URL. Ordenamos as chaves para
 * estabilidade.
 */
function buildQueryString(params: Record<string, unknown>): string {
  const sp = new URLSearchParams();
  for (const key of Object.keys(params).sort()) {
    const v = params[key];
    if (v !== undefined && v !== null) sp.append(key, String(v));
  }
  return sp.toString();
}
