import { randomUUID } from "node:crypto";
import type { Order, OrderBook } from "../../domain/src/types";

export interface OrderRouterConfig {
  maxBps: number;
  minIntervalMs?: number;
  bulkInitIntervalMs?: number;
  forbidPostOnlyCross?: boolean;
  clientIdCacheSize?: number;
}

export interface DispatchResult {
  id: string;
}

export type DispatchFn = (order: Order) => Promise<DispatchResult>;
export type CancelFn = (id: string) => Promise<void>;
export type CancelClientFn = (clientId: string) => Promise<void>;

export class OrderRouter {
  private readonly minIntervalMs: number;
  private readonly bulkInitIntervalMs: number;
  private readonly forbidPostOnlyCross: boolean;
  private readonly maxBps: number;
  private readonly clientIdCacheSize: number;
  private lastSendAt = 0;
  private bulkInitMode = false;
  private readonly recentClientIds: string[] = [];
  private readonly clientIdSet = new Set<string>();
  private readonly clientIdAlias = new Map<string, string>();
  private cancelFn?: CancelFn;
  private cancelClientFn?: CancelClientFn;

  constructor(
    private readonly sendLimitFn: DispatchFn,
    private readonly getBook: (symbol: string) => OrderBook | undefined,
    config: OrderRouterConfig
  ) {
    this.maxBps = config.maxBps;
    this.minIntervalMs = config.minIntervalMs ?? 0;
    this.bulkInitIntervalMs = config.bulkInitIntervalMs ?? Math.min(20, this.minIntervalMs);
    this.forbidPostOnlyCross = config.forbidPostOnlyCross ?? true;
    this.clientIdCacheSize = config.clientIdCacheSize ?? 512;
  }

  enableBulkInitMode(): void {
    this.bulkInitMode = true;
  }

  disableBulkInitMode(): void {
    this.bulkInitMode = false;
  }

  async sendLimit(order: Order): Promise<string> {
    order.clientId = this.normalizeClientId(order.clientId);
    await this.ensureThrottle();
    this.ensureUniqueClientId(order.clientId);
    this.applySlipGuard(order);
    this.ensurePostOnly(order);

    const { id } = await this.sendLimitFn(order);
    return id;
  }

  async sendIOC(order: Order): Promise<string> {
    order.clientId = this.normalizeClientId(order.clientId);
    await this.ensureThrottle();
    this.ensureUniqueClientId(order.clientId);
    const { id } = await this.sendLimitFn(order);
    return id;
  }

  async sendLimitChild(order: Order): Promise<string> {
    return this.sendLimit(order);
  }

  attachCancelHandlers(cancelFn?: CancelFn, cancelClientFn?: CancelClientFn): void {
    this.cancelFn = cancelFn;
    this.cancelClientFn = cancelClientFn;
  }

  async cancel(orderId: string): Promise<void> {
    if (!this.cancelFn) {
      throw new Error("OrderRouter cancel handler not configured");
    }
    await this.cancelFn(orderId);
    this.clientIdSet.delete(orderId);
    this.cleanupAlias(orderId);
  }

  async cancelByClientId(clientId: string): Promise<void> {
    const resolved = this.resolveClientId(clientId);
    const handler = this.cancelClientFn ?? this.cancelFn;
    if (!handler) {
      throw new Error("OrderRouter cancel handler not configured");
    }
    await handler(resolved);
    this.clientIdSet.delete(resolved);
    this.cleanupAlias(resolved);
  }

  private applySlipGuard(order: Order) {
    if (order.postOnly) return;
    const book = this.getBook(order.symbol);
    if (!book?.bids?.length || !book?.asks?.length) return;
    const reference = order.side === "buy" ? book.asks[0].px : book.bids[0].px;
    if (!reference) return;
    const diff = Math.abs((order.px - reference) / reference) * 10_000;
    if (diff > this.maxBps) {
      throw new Error(`slippage>${this.maxBps}bps`);
    }
  }

  private ensurePostOnly(order: Order) {
    if (!this.forbidPostOnlyCross) return;
    if (!order.postOnly) return;
    const book = this.getBook(order.symbol);
    if (!book?.bids?.length || !book?.asks?.length) return;
    const bestBid = book.bids[0].px;
    const bestAsk = book.asks[0].px;
    if (order.side === "buy" && order.px >= bestAsk) {
      throw new Error("post-only order would cross best ask");
    }
    if (order.side === "sell" && order.px <= bestBid) {
      throw new Error("post-only order would cross best bid");
    }
  }

  private async ensureThrottle() {
    const intervalMs = this.bulkInitMode ? this.bulkInitIntervalMs : this.minIntervalMs;
    if (intervalMs <= 0) return;
    const now = Date.now();
    const elapsed = now - this.lastSendAt;
    if (elapsed < intervalMs) {
      await new Promise(resolve => setTimeout(resolve, intervalMs - elapsed));
    }
    this.lastSendAt = Date.now();
  }

  private ensureUniqueClientId(clientId: string) {
    if (this.clientIdSet.has(clientId)) {
      throw new Error(`duplicate clientId ${clientId}`);
    }
    this.clientIdSet.add(clientId);
    this.recentClientIds.push(clientId);
    if (this.recentClientIds.length > this.clientIdCacheSize) {
      const removed = this.recentClientIds.shift();
      if (removed) {
        this.clientIdSet.delete(removed);
      }
    }
  }

  private normalizeClientId(clientId: string): string {
    const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
    if (uuidPattern.test(clientId)) {
      return clientId;
    }
    const existing = this.clientIdAlias.get(clientId);
    if (existing) return existing;
    const generated = randomUUID();
    this.clientIdAlias.set(clientId, generated);
    return generated;
  }

  private resolveClientId(clientId: string): string {
    return this.clientIdAlias.get(clientId) ?? clientId;
  }

  private cleanupAlias(resolved: string): void {
    for (const [original, alias] of this.clientIdAlias.entries()) {
      if (alias === resolved) {
        this.clientIdAlias.delete(original);
      }
    }
  }
}
