import { request } from "undici";
import { URL } from "node:url";

import type { Order, OrderBook, PositionSnapshot } from "../../../domain/src/types";
import { signRequest, signStructuredPayload, SignatureHeader } from "./signing";
import { PacificaApiError, buildPacificaError, PacificaRateLimitError } from "./errors";
import { RateLimiter } from "./rateLimiter";
import { observeRestRequest } from "./metrics";
import { extractEndpoint, logDebug } from "./helper";
import { PacificaWsOrderGateway } from "./wsOrderGateway";

type HttpMethod = "GET" | "POST" | "DELETE";

export interface PacificaConfig {
  baseUrl: string;
  apiKey?: string;
  secret?: string;
  subaccount?: string;
  userAgent?: string;
  signatureExpiryWindowMs?: number;
}

interface PlaceOrderResponse {
  orderId?: string;
  id?: string;
  clientOrderId?: string;
}

interface CancelResponse {
  cancelled?: string[];
  success?: boolean;
}

interface PositionsResponse {
  positions?: Array<{
    symbol: string;
    base: string | number;
    quote: string | number;
    entryPrice?: string | number;
    pnl?: string | number;
  }>;
}

interface BookLevel {
  p?: string | number;
  a?: string | number;
  n?: number;
}

interface BookResponse {
  success?: boolean;
  data?: {
    s?: string;
    l?: [BookLevel[], BookLevel[]];
    t?: number;
  };
}

const DEFAULT_SIGNATURE_EXPIRY_WINDOW_MS = 5_000;

export class PacificaAdapter {
  private readonly base: URL;
  private readonly limiter: RateLimiter;
  private wsGateway?: PacificaWsOrderGateway;
  private accountId?: string;

  constructor(private readonly cfg: PacificaConfig) {
    if (!cfg.baseUrl) {
      throw new Error("Pacifica baseUrl is required");
    }
    const baseUrl = cfg.baseUrl.endsWith("/")
      ? cfg.baseUrl
      : `${cfg.baseUrl}/`;
    this.base = new URL(baseUrl);
    this.limiter = new RateLimiter({
      burst: 60,
      refillPerSec: 1  // 60 tokens per 60 seconds = 1 per second
    });
  }

  name() {
    return "Pacifica";
  }

  attachWsGateway(gateway: PacificaWsOrderGateway) {
    this.wsGateway = gateway;
  }

  setAccountId(id: string): void {
    this.accountId = id;
  }

  async getProduct(symbol: string) {
    return this.rest<any>("GET", `/products/${encodeURIComponent(symbol)}`);
  }

  async getOrderBook(symbol: string): Promise<OrderBook> {
    const json = await this.rest<BookResponse>("GET", "/book", {
      query: { symbol }
    });
    const payload = json.data ?? {};
    const levels = payload.l ?? [];
    const [bidLevels = [], askLevels = []] = levels;

    const mapLevels = (
      entries: Array<{ p?: string | number; a?: string | number }>
    ) =>
      entries
        .map(entry => {
          const px = entry.p !== undefined ? Number(entry.p) : undefined;
          const sz = entry.a !== undefined ? Number(entry.a) : undefined;
          if (px === undefined || Number.isNaN(px) || sz === undefined || Number.isNaN(sz)) {
            return undefined;
          }
          return { px, sz };
        })
        .filter((entry): entry is { px: number; sz: number } => entry !== undefined);

    const bids = mapLevels(bidLevels);
    const asks = mapLevels(askLevels);

    return {
      bids,
      asks,
      ts: payload.t ?? Date.now()
    };
  }

  async listPositions(): Promise<PositionSnapshot[]> {
    if (!this.cfg.apiKey) {
      logDebug("Pacifica account address missing, skipping positions fetch");
      return [];
    }

    let json: PositionsResponse | undefined;
    try {
      const query: Record<string, unknown> = {};
      if (this.cfg.apiKey) {
        query.account = this.cfg.apiKey;
      }
      json = await this.rest<PositionsResponse>("GET", "/account/positions", {
        query
      });
    } catch (error) {
      if (error instanceof PacificaApiError && error.status === 404) {
        logDebug("Pacifica positions endpoint returned 404, treating as empty", {
          account: this.cfg.apiKey
        });
        return [];
      }
      throw error;
    }

    const now = Date.now();
    return (json.positions || []).map(pos => ({
      symbol: pos.symbol,
      base: Number(pos.base ?? 0),
      quote: Number(pos.quote ?? 0),
      entryPx: pos.entryPrice !== undefined ? Number(pos.entryPrice) : undefined,
      ts: now,
      accountId: this.accountId
    }));
  }

  async getPosition(symbol: string): Promise<PositionSnapshot> {
    const positions = await this.listPositions();
    return (
      positions.find(p => p.symbol === symbol) ?? {
        symbol,
        base: 0,
        quote: 0,
        ts: Date.now(),
        accountId: this.accountId
      }
    );
  }

  async place(order: Order): Promise<{ id: string }> {
    const timeInForce = (order.tif ?? "GTC").toUpperCase();

    if (!this.cfg.apiKey) {
      throw new Error("Pacifica account address (apiKey) is required to place orders");
    }

    if (this.wsGateway) {
      const result = await this.wsGateway.createOrder({
        symbol: order.symbol,
        side: order.side === "buy" ? "bid" : "ask",
        price: formatNumeric(order.px),
        amount: formatNumeric(order.sz),
        tif: timeInForce as "GTC" | "IOC" | "FOK",
        reduceOnly: false,
        clientOrderId: order.clientId
      });
      return { id: result.orderId };
    }

    const expiryWindow = this.cfg.signatureExpiryWindowMs ?? DEFAULT_SIGNATURE_EXPIRY_WINDOW_MS;

    const signatureHeader: SignatureHeader = {
      timestamp: Date.now(),
      expiry_window: expiryWindow,
      type: "create_order"
    };

    const signaturePayload: Record<string, unknown> = {
      symbol: order.symbol,
      side: order.side === "buy" ? "bid" : "ask",
      price: formatNumeric(order.px),
      amount: formatNumeric(order.sz),
      tif: timeInForce,
      reduce_only: false,
      client_order_id: order.clientId
    };

    const { signature, message } = signStructuredPayload(this.cfg, signatureHeader, signaturePayload);
    logDebug("Pacifica order signature", { signaturePayload, signatureMessage: message, signature });

    const json = await this.rest<PlaceOrderResponse>("POST", "/orders/create", {
      body: {
        account: this.cfg.apiKey,
        signature,
        timestamp: signatureHeader.timestamp,
        expiry_window: signatureHeader.expiry_window,
        ...signaturePayload
      }
    });

    const id =
      json.orderId ?? json.id ?? json.clientOrderId ?? order.clientId;

    return { id };
  }

  async cancel(orderId: string, symbol?: string): Promise<void> {
    if (!this.cfg.apiKey) {
      throw new Error("Pacifica account address (apiKey) is required to cancel orders");
    }

    if (this.wsGateway) {
      try {
        await this.wsGateway.cancelOrder({ orderId });
        return;
      } catch (error) {
        logDebug("Pacifica WS cancel failed, falling back to REST", {
          orderId,
          error: formatError(error)
        });
      }
    }

    const expiryWindow = this.cfg.signatureExpiryWindowMs ?? DEFAULT_SIGNATURE_EXPIRY_WINDOW_MS;

    const signatureHeader: SignatureHeader = {
      timestamp: Date.now(),
      expiry_window: expiryWindow,
      type: "cancel_order"
    };

    const signaturePayload: Record<string, unknown> = {
      order_id: orderId
    };
    if (symbol) {
      signaturePayload.symbol = symbol;
    }

    const { signature, message } = signStructuredPayload(this.cfg, signatureHeader, signaturePayload);
    logDebug("Pacifica cancel signature", { signaturePayload, signatureMessage: message, signature });

    const json = await this.rest<CancelResponse>("POST", "/orders/cancel", {
      body: {
        account: this.cfg.apiKey,
        signature,
        timestamp: signatureHeader.timestamp,
        expiry_window: signatureHeader.expiry_window,
        ...signaturePayload
      }
    });

    if (json.cancelled && !json.cancelled.map(entry => String(entry)).includes(String(orderId))) {
      throw new PacificaApiError("Order cancel not acknowledged", {
        status: 200,
        payload: json
      });
    }
  }

  async cancelByClientId(clientOrderId: string, symbol?: string): Promise<void> {
    if (!this.cfg.apiKey) {
      throw new Error("Pacifica account address (apiKey) is required to cancel orders by client id");
    }

    if (this.wsGateway) {
      try {
        await this.wsGateway.cancelOrder({ clientOrderId });
        return;
      } catch (error) {
        logDebug("Pacifica WS cancel by client id failed, falling back to REST", {
          clientOrderId,
          error: formatError(error)
        });
      }
    }

    const expiryWindow = this.cfg.signatureExpiryWindowMs ?? DEFAULT_SIGNATURE_EXPIRY_WINDOW_MS;

    const signatureHeader: SignatureHeader = {
      timestamp: Date.now(),
      expiry_window: expiryWindow,
      type: "cancel_order"
    };

    const signaturePayload: Record<string, unknown> = {
      client_order_id: clientOrderId
    };
    if (symbol) {
      signaturePayload.symbol = symbol;
    }

    const { signature, message } = signStructuredPayload(this.cfg, signatureHeader, signaturePayload);
    logDebug("Pacifica cancel signature", { signaturePayload, signatureMessage: message, signature });

    await this.rest<CancelResponse>("POST", "/orders/cancel", {
      body: {
        account: this.cfg.apiKey,
        signature,
        timestamp: signatureHeader.timestamp,
        expiry_window: signatureHeader.expiry_window,
        ...signaturePayload
      }
    });
  }

  async cancelAll(symbol?: string): Promise<void> {
    if (!this.cfg.apiKey) {
      throw new Error("Pacifica account address (apiKey) is required to cancel orders");
    }

    if (this.wsGateway) {
      try {
        await this.wsGateway.cancelAll({ allSymbols: symbol ? false : true, symbol });
        return;
      } catch (error) {
        logDebug("Pacifica WS cancel_all failed, falling back to REST", {
          symbol,
          error: formatError(error)
        });
      }
    }

    const body: Record<string, unknown> = {
      account: this.cfg.apiKey
    };
    if (symbol) {
      body.symbol = symbol;
    }
    await this.rest("POST", "/orders/cancel/all", { body });
  }

  supportsSTP(): boolean {
    return true;
  }

  private async rest<T>(
    method: HttpMethod,
    path: string,
    options?: { body?: unknown; query?: Record<string, unknown> }
  ): Promise<T> {
    await this.limiter.acquire();
    const normalizedPath = this.buildPath(path, options?.query);
    const relativePath = normalizedPath.startsWith("/")
      ? normalizedPath.slice(1)
      : normalizedPath;
    const url = new URL(relativePath, this.base).toString();

    const { headers: signatureHeaders, body: bodyString } = signRequest(
      this.cfg,
      method,
      normalizedPath,
      options?.body
    );

    const headers: Record<string, string> = {
      Accept: "application/json",
      ...signatureHeaders
    };

    if (options?.body !== undefined) {
      headers["Content-Type"] = "application/json";
    }
    if (this.cfg.userAgent) {
      headers["User-Agent"] = this.cfg.userAgent;
    }

    const start = Date.now();
    let attempt = 0;
    let res;
    let completed = false;

    logDebug("Pacifica REST request", {
      method,
      endpoint: extractEndpoint(normalizedPath),
      query: options?.query ? JSON.stringify(options.query) : undefined,
      hasBody: options?.body !== undefined,
      url
    });

    while (!completed) {
      attempt += 1;
      res = await request(url, {
        method,
        headers,
        body: bodyString.length ? bodyString : undefined
      });
      if (res.statusCode === 429 || res.statusCode >= 500) {
        if (attempt >= 5) {
          completed = true;
        } else {
          const retryAfterDelay = parseRetryAfter(res.headers["retry-after"]);
          await sleep(retryAfterDelay ?? attempt * 100);
          continue;
        }
      }
      completed = true;
    }

    logDebug("Pacifica REST response", {
      method,
      endpoint: extractEndpoint(normalizedPath),
      status: res?.statusCode,
      attempt
    });

    if (!res) {
      throw new PacificaApiError('Pacifica request did not return a response', {
        status: 0,
        code: 'no_response'
      });
    }

    const text = await res.body.text();
    let json: any;
    if (text) {
      try {
        json = JSON.parse(text);
      } catch {
        json = undefined;
      }
    }

    if (res.statusCode >= 400) {
      const retryAfter = parseRetryAfter(res.headers["retry-after"]);
      if (res.statusCode === 429) {
        throw new PacificaRateLimitError(text || "rate limited", {
          status: res.statusCode,
          code: json?.code,
          retryAfterMs: retryAfter,
          payload: json ?? text
        });
      }
      throw buildPacificaError(res.statusCode, text, json, retryAfter);
    }

    observeRestRequest({ endpoint: normalizedPath, method, status: res.statusCode }, Date.now() - start);

    return (json as T) ?? (undefined as T);
  }

  private buildPath(
    path: string,
    query?: Record<string, unknown>
  ): string {
    const base = new URL(path, "http://placeholder.local");
    if (query) {
      for (const [key, value] of Object.entries(query)) {
        if (value === undefined || value === null) continue;
        base.searchParams.set(key, String(value));
      }
    }
    const search = base.searchParams.toString();
    return base.pathname + (search ? `?${search}` : "");
  }
}

function parseRetryAfter(value: string | string[] | undefined): number | undefined {
  if (!value) return undefined;
  const raw = Array.isArray(value) ? value[0] : value;
  if (!raw) return undefined;

  const seconds = Number(raw);
  if (!Number.isNaN(seconds)) {
    return seconds * 1000;
  }

  const date = Date.parse(raw);
  if (Number.isNaN(date)) return undefined;
  const diff = date - Date.now();
  return diff > 0 ? diff : undefined;
}

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

function formatNumeric(value: number): string {
  if (!Number.isFinite(value)) {
    throw new Error(`Pacifica numeric field must be finite, received ${value}`);
  }
  return value.toString();
}

function formatError(error: unknown): Record<string, unknown> {
  if (!error) {
    return {};
  }
  if (error instanceof Error) {
    const anyErr = error as any;
    return {
      name: error.name,
      message: error.message,
      code: anyErr?.code,
      status: anyErr?.status,
      stack: error.stack
    };
  }
  if (typeof error === "object") {
    return error as Record<string, unknown>;
  }
  return { message: String(error) };
}
