import type { OrderBook, OrderBookDelta, OrderBookUpdate } from "../../domain/src/types";

export interface ShadowBookOptions {
  /**
   * Maximum depth levels to store per side. Defaults to storing all levels.
   */
  maxDepth?: number;
  /**
   * Consider data stale if last update is older than this value (ms).
   */
  stalenessMs?: number;
}

interface BookState {
  bids: OrderBook["bids"];
  asks: OrderBook["asks"];
  ts: number;
  seq?: number;
  lastUpdate: number;
}

export class ShadowBook {
  private readonly books = new Map<string, BookState>();
  private readonly maxDepth?: number;
  private readonly stalenessMs: number;

  constructor(options: ShadowBookOptions = {}) {
    this.maxDepth = options.maxDepth;
    this.stalenessMs = options.stalenessMs ?? 5_000;
  }

  updateFromSnapshot(symbol: string, snapshot: OrderBook, seq?: number): void {
    const bids = this.normalizeSide(snapshot.bids);
    const asks = this.normalizeSide(snapshot.asks);
    this.books.set(symbol, {
      bids,
      asks,
      ts: snapshot.ts,
      seq,
      lastUpdate: Date.now()
    });
  }

  applyIncrement(symbol: string, delta: OrderBookDelta): void {
    const state = this.books.get(symbol);
    if (!state) {
      const bids = this.normalizeSide(delta.bids?.map(convertUpdateToLevel) ?? []);
      const asks = this.normalizeSide(delta.asks?.map(convertUpdateToLevel) ?? []);
      this.books.set(symbol, {
        bids,
        asks,
        ts: delta.ts ?? Date.now(),
        seq: delta.seq,
        lastUpdate: Date.now()
      });
      return;
    }

    if (delta.bids) {
      state.bids = this.applyUpdates(state.bids, delta.bids, "bid");
    }
    if (delta.asks) {
      state.asks = this.applyUpdates(state.asks, delta.asks, "ask");
    }
    if (delta.ts) {
      state.ts = delta.ts;
    }
    if (delta.seq !== undefined) {
      state.seq = delta.seq;
    }
    state.lastUpdate = Date.now();
  }

  getState(symbol: string): BookState | undefined {
    return this.books.get(symbol);
  }

  snapshot(symbol: string): OrderBook | undefined {
    const state = this.books.get(symbol);
    if (!state) return undefined;
    return { bids: state.bids, asks: state.asks, ts: state.ts };
  }

  getTop(symbol: string) {
    const state = this.books.get(symbol);
    if (!state?.bids.length || !state?.asks.length) {
      return undefined;
    }
    return {
      bid: state.bids[0],
      ask: state.asks[0]
    };
  }

  mid(symbol: string): number | undefined {
    const top = this.getTop(symbol);
    if (!top) return undefined;
    return (top.bid.px + top.ask.px) / 2;
  }

  spread(symbol: string): number | undefined {
    const top = this.getTop(symbol);
    if (!top) return undefined;
    return top.ask.px - top.bid.px;
  }

  spreadBps(symbol: string): number | undefined {
    const mid = this.mid(symbol);
    const spread = this.spread(symbol);
    if (!mid || !spread) return undefined;
    return (spread / mid) * 10_000;
  }

  computeObi(symbol: string, depth = 1): number | undefined {
    const state = this.books.get(symbol);
    if (!state) return undefined;
    const maxDepth = Math.min(depth, state.bids.length, state.asks.length);
    if (maxDepth === 0) return undefined;

    const bidVolume = state.bids.slice(0, maxDepth).reduce((sum, level) => sum + level.sz, 0);
    const askVolume = state.asks.slice(0, maxDepth).reduce((sum, level) => sum + level.sz, 0);
    const denom = bidVolume + askVolume;
    if (denom === 0) return 0;
    return (bidVolume - askVolume) / denom;
  }

  topDepthUsd(symbol: string, depth = 5): number | undefined {
    const state = this.books.get(symbol);
    if (!state) return undefined;
    const top = this.getTop(symbol);
    if (!top) return undefined;

    const bidsUsd = state.bids
      .slice(0, depth)
      .reduce((sum, level) => sum + level.sz * level.px, 0);
    const asksUsd = state.asks
      .slice(0, depth)
      .reduce((sum, level) => sum + level.sz * level.px, 0);
    return bidsUsd + asksUsd;
  }

  detectDataGap(symbol: string, now = Date.now()): boolean {
    const state = this.books.get(symbol);
    if (!state) return true;
    return now - state.lastUpdate > this.stalenessMs;
  }

  clear(symbol: string): void {
    this.books.delete(symbol);
  }

  reset(): void {
    this.books.clear();
  }

  private normalizeSide(levels: OrderBook["bids"]): OrderBook["bids"] {
    const sorted = [...levels].sort((a, b) => b.px - a.px);
    if (this.maxDepth !== undefined) {
      return sorted.slice(0, this.maxDepth);
    }
    return sorted;
  }

  private applyUpdates(
    current: OrderBook["bids"],
    updates: OrderBookUpdate[],
    side: "bid" | "ask"
  ): OrderBook["bids"] {
    const map = new Map<number, number>();
    for (const level of current) {
      map.set(level.px, level.sz);
    }
    for (const update of updates) {
      const price = update.price;
      const size = update.size;
      if (size <= 0) {
        map.delete(price);
      } else {
        map.set(price, size);
      }
    }
    const merged = Array.from(map.entries()).map(([px, sz]) => ({ px, sz }));
    merged.sort((a, b) => {
      if (side === "bid") {
        return b.px - a.px;
      }
      return a.px - b.px;
    });
    if (this.maxDepth !== undefined) {
      return merged.slice(0, this.maxDepth);
    }
    return merged;
  }
}

function convertUpdateToLevel(update: OrderBookUpdate) {
  return { px: update.price, sz: update.size };
}
