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

/**
 * Cliente base da Binance C2C (SAPI v7.4).
 *
 * Toda chamada à Binance passa por aqui. É o único lugar que conhece
 * a apiKey/secretKey, monta a assinatura HMAC-SHA256 e injeta os
 * headers obrigatórios do C2C (clientType, x-user-id, etc.).
 *
 * Esquema de assinatura (confirmado na doc SAPI v7.4):
 *   1. monta a query string com todos os params + timestamp
 *   2. signature = HMAC_SHA256(queryString, secretKey)  -> hex
 *   3. apiKey vai no header X-MBX-APIKEY
 *   4. signature é anexado como mais um param (&signature=...)
 */

export interface C2CConfig {
  apiKey: string;
  secretKey: string;
  /** ID do usuário Binance, usado no header x-user-id */
  userId: string;
  /** Base da API. Produção: https://api.binance.com */
  baseUrl?: string;
  /** Tolerância de tempo da requisição (ms). Máx Binance: 60000 */
  recvWindow?: number;
  /** clientType obrigatório no header. Ex: "web" */
  clientType?: string;
  /**
   * Dispatcher do undici (ex: ProxyAgent) para rotear esta conta por um
   * proxy. Cada conta tem o seu — montado em `src/accounts/client-factory.ts`
   * a partir de `exchange_accounts`. Quando ausente, usa a saída direta.
   */
  dispatcher?: Dispatcher;
}

export interface RequestOptions {
  method: "GET" | "POST";
  path: string;
  /** Params que entram na query string assinada (GET e POST ambos) */
  query?: Record<string, string | number | undefined>;
  /** Corpo JSON para POST (não entra na assinatura — vai no body) */
  body?: unknown;
}

/** Envelope padrão de resposta do C2C: CommonRet_T_ */
export interface CommonRet<T> {
  code: string;
  message: string | null;
  messageDetail: string | null;
  data: T;
  success: boolean;
}

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

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

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

  /** Gera a assinatura HMAC-SHA256 em hex sobre a query string. */
  private sign(queryString: string): string {
    return crypto
      .createHmac("sha256", this.secretKey)
      .update(queryString)
      .digest("hex");
  }

  /** Monta a query string ordenada e assinada. */
  private buildSignedQuery(query: Record<string, string | number | undefined>): string {
    const params = new URLSearchParams();
    for (const [k, v] of Object.entries(query)) {
      if (v !== undefined && v !== null) params.append(k, String(v));
    }
    params.append("timestamp", String(Date.now()));
    params.append("recvWindow", String(this.recvWindow));
    const qs = params.toString();
    const signature = this.sign(qs);
    return `${qs}&signature=${signature}`;
  }

  /** Executa uma chamada assinada e devolve data já desembrulhado do CommonRet. */
  async request<T>(opts: RequestOptions): Promise<T> {
    const signedQuery = this.buildSignedQuery(opts.query ?? {});
    const url = `${this.baseUrl}${opts.path}?${signedQuery}`;

    const headers: Record<string, string> = {
      "X-MBX-APIKEY": this.apiKey,
      clientType: this.clientType,
      "x-user-id": this.userId,
    };
    if (opts.body !== undefined) {
      headers["Content-Type"] = "application/json";
    }

    // `dispatcher` é uma extensão do undici aceita pelo fetch nativo do Node,
    // mas não tipada em RequestInit — daí o cast.
    const init: RequestInit & { dispatcher?: Dispatcher } = {
      method: opts.method,
      headers,
      body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined,
    };
    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 BinanceC2CError(res.status, null, `Resposta não-JSON: ${text}`, text);
    }

    if (!res.ok) {
      const e = parsed as { code?: string; msg?: string; message?: string };
      throw new BinanceC2CError(
        res.status,
        e?.code ?? null,
        e?.msg ?? e?.message ?? `HTTP ${res.status}`,
        parsed,
      );
    }

    const envelope = parsed as CommonRet<T>;
    if (envelope && typeof envelope === "object" && "success" in envelope) {
      if (!envelope.success) {
        throw new BinanceC2CError(
          res.status,
          envelope.code,
          envelope.message ?? envelope.messageDetail ?? "Falha na API C2C",
          envelope,
        );
      }
      return envelope.data;
    }
    // alguns endpoints (ex: listUserOrderHistory) podem devolver formato distinto
    return parsed as T;
  }
}
