/**
 * Order Book Depth Map — Data Utilities
 * 
 * Transforms raw Hyperliquid L2 snapshots into surface matrices
 * for 3D visualization.
 * 
 * Architecture:
 *   Ring buffer stores last N snapshots.
 *   Each snapshot: { bids: [px, sz][], asks: [px, sz][], mid: number, ts: number }
 *   Output: { x: bps[], y: snapshot_index[], z: size[][] }
 * 
 * Ring-buffer design:
 *   - Fixed capacity (default 60 = ~1 minute at 1s updates)
 *   - O(1) append via write pointer
 *   - No allocations on append → suitable for 60fps streaming
 */

export interface L2Level {
  px: number;
  sz: number;
}

export interface L2Snapshot {
  bids: L2Level[];   // sorted descending by price
  asks: L2Level[];   // sorted ascending by price
  mid: number;
  ts: number;
}

export interface SurfaceData {
  /** Distance from mid in basis points (X-axis) */
  x: number[];
  /** Snapshot index or cumulative bid count (Y-axis) */
  y: number[];
  /** Resting size matrix: z[row][col] — rows = snapshots, cols = bps */
  z: number[][];
}

export interface ImbalanceMetrics {
  /** Current imbalance: (V_bid - V_ask) / (V_bid + V_ask) */
  imbalance: number;
  bidVolume: number;
  askVolume: number;
  wallSide: "bid" | "ask" | "none";
  wallStrength: number;
  snapshots: number;
}

/**
 * Ring buffer for L2 snapshots.
 * Fixed capacity, overwrite oldest on overflow.
 */
export class L2RingBuffer {
  private buffer: L2Snapshot[];
  private capacity: number;
  private writeIdx: number;
  private count: number;

  constructor(capacity: number = 60) {
    this.capacity = capacity;
    this.buffer = new Array(capacity);
    this.writeIdx = 0;
    this.count = 0;
  }

  push(snapshot: L2Snapshot): void {
    this.buffer[this.writeIdx] = snapshot;
    this.writeIdx = (this.writeIdx + 1) % this.capacity;
    if (this.count < this.capacity) this.count++;
  }

  /** Returns snapshots oldest-first */
  snapshot(): L2Snapshot[] {
    if (this.count === 0) return [];
    const start = this.count < this.capacity ? 0 : this.writeIdx;
    const result: L2Snapshot[] = [];
    for (let i = 0; i < this.count; i++) {
      result.push(this.buffer[(start + i) % this.capacity]);
    }
    return result;
  }

  get size(): number {
    return this.count;
  }

  clear(): void {
    this.writeIdx = 0;
    this.count = 0;
  }
}

/**
 * Convert L2 snapshots → surface matrix.
 * 
 * X-axis: distance from mid in basis points
 * Y-axis: snapshot index (0 = oldest, N = newest)
 * Z-axis: resting size at that bps level
 *
 * @param snapshots  Ring buffer contents (oldest first)
 * @param bpsRange   ±bps from mid to cover (default: 50)
 * @param resolution Number of bps steps (default: 100)
 */
export function l2SnapshotsToSurface(
  snapshots: L2Snapshot[],
  bpsRange: number = 50,
  resolution: number = 100,
): SurfaceData {
  const bpsStep = (bpsRange * 2) / resolution;
  const x: number[] = [];
  for (let i = 0; i < resolution; i++) {
    x.push(-bpsRange + i * bpsStep);
  }

  const y = snapshots.map((_, i) => i);
  const z: number[][] = [];

  for (const snap of snapshots) {
    const row = new Array(resolution).fill(0);
    const mid = snap.mid;

    // Fill bid side (negative bps)
    for (const bid of snap.bids) {
      const bps = ((bid.px - mid) / mid) * 10000;
      const idx = Math.round((bps + bpsRange) / bpsStep);
      if (idx >= 0 && idx < resolution) {
        row[idx] += bid.sz;
      }
    }

    // Fill ask side (positive bps)
    for (const ask of snap.asks) {
      const bps = ((ask.px - mid) / mid) * 10000;
      const idx = Math.round((bps + bpsRange) / bpsStep);
      if (idx >= 0 && idx < resolution) {
        row[idx] += ask.sz;
      }
    }

    z.push(row);
  }

  return { x, y, z };
}

/**
 * Compute imbalance metrics from latest snapshot.
 */
export function computeImbalance(snapshot: L2Snapshot): ImbalanceMetrics {
  const bidVolume = snapshot.bids.reduce((sum, b) => sum + b.sz * b.px, 0);
  const askVolume = snapshot.asks.reduce((sum, a) => sum + a.sz * a.px, 0);
  const total = bidVolume + askVolume;
  const imbalance = total > 0 ? (bidVolume - askVolume) / total : 0;

  // Wall detection: find side with largest concentration
  const maxBidSz = Math.max(...snapshot.bids.map(b => b.sz), 0);
  const maxAskSz = Math.max(...snapshot.asks.map(a => a.sz), 0);
  const wallSide: "bid" | "ask" | "none" =
    maxBidSz > maxAskSz * 1.3 ? "bid" :
    maxAskSz > maxBidSz * 1.3 ? "ask" : "none";
  const wallStrength = Math.max(maxBidSz, maxAskSz);

  return {
    imbalance: Math.round(imbalance * 10000) / 10000,
    bidVolume: Math.round(bidVolume * 100) / 100,
    askVolume: Math.round(askVolume * 100) / 100,
    wallSide,
    wallStrength: Math.round(wallStrength * 10000) / 10000,
    snapshots: 1,
  };
}

/**
 * Generate synthetic L2 data for testing/development.
 * Produces realistic order-book shapes with price movement.
 */
export function generateSyntheticSnapshots(
  count: number = 60,
  basePrice: number = 97800,
): L2Snapshot[] {
  const snapshots: L2Snapshot[] = [];
  let price = basePrice;
  let trend = 0;

  for (let i = 0; i < count; i++) {
    // Random walk with mean reversion
    trend += (Math.random() - 0.5) * 2;
    trend *= 0.95; // decay
    price += trend * 50;
    price += (basePrice - price) * 0.01; // mean reversion

    const mid = price;
    const bids: L2Level[] = [];
    const asks: L2Level[] = [];

    // Generate 20 levels on each side
    for (let j = 0; j < 20; j++) {
      const bps = (j + 1) * 2.5;
      const bidPx = mid * (1 - bps / 10000);
      const askPx = mid * (1 + bps / 10000);

      // Realistic size distribution: thicker near mid, thinner further out
      // Add wall at certain levels
      const baseSize = Math.exp(-j * 0.15) * 5;
      const bidWall = j === 3 ? Math.random() * 15 : 0; // occasional wall at 10bps
      const askWall = j === 5 ? Math.random() * 12 : 0;
      const noise = (Math.random() - 0.5) * 2;

      bids.push({ px: Math.round(bidPx * 10) / 10, sz: Math.max(0.01, baseSize + bidWall + noise) });
      asks.push({ px: Math.round(askPx * 10) / 10, sz: Math.max(0.01, baseSize + askWall + noise) });
    }

    snapshots.push({ bids, asks, mid, ts: Date.now() + i * 1000 });
  }

  return snapshots;
}

// ═══════════ 3D Subplots: Split Bid/Ask Surfaces ═══════════

export interface DualSurfaceData {
  bid: SurfaceData;
  ask: SurfaceData;
  y: number[];
}

/** Split L2 → dual bid/ask surface matrices for 3D subplots */
export function l2SnapshotsToDualSurface(
  snapshots: L2Snapshot[],
  bpsRange: number = 50,
  resolution: number = 50,
): DualSurfaceData {
  const bpsStep = bpsRange / resolution;
  const bidX: number[] = [], askX: number[] = [];
  for (let i = 0; i < resolution; i++) {
    bidX.push(-bpsRange + i * bpsStep);
    askX.push(i * bpsStep);
  }
  const y = snapshots.map((_, i) => i);
  const bidZ: number[][] = [], askZ: number[][] = [];
  for (const snap of snapshots) {
    const mid = snap.mid;
    const bRow = new Array(resolution).fill(0);
    const aRow = new Array(resolution).fill(0);
    for (const bid of snap.bids) {
      const bps = ((bid.px - mid) / mid) * 10000;
      const idx = Math.round((bps + bpsRange) / bpsStep);
      if (idx >= 0 && idx < resolution) bRow[idx] += bid.sz;
    }
    for (const ask of snap.asks) {
      const bps = ((ask.px - mid) / mid) * 10000;
      const idx = Math.round(bps / bpsStep);
      if (idx >= 0 && idx < resolution) aRow[idx] += ask.sz;
    }
    bidZ.push(bRow);
    askZ.push(aRow);
  }
  return { bid: { x: bidX, y, z: bidZ }, ask: { x: askX, y, z: askZ }, y };
}
