import type { FlowPacket } from "@islandflow/types";
import type { ContractLeg, StructureSummary } from "./structures";

export type NbboPlacementCounts = {
  aa: number;
  a: number;
  b: number;
  bb: number;
  mid: number;
  missing: number;
  stale: number;
};

export type LegEvidence = ContractLeg & {
  members: string[];
  totalSize: number;
  totalPremium: number;
  placements: NbboPlacementCounts;
  source_ts: number;
  ingest_ts: number;
  seq: number;
};

export type StructurePacketPlan = {
  id: string;
  dedupeKey: string;
  bucketStartTs: number;
  root: string;
  pseudoContractId: string;
  expiries: string[];
  strikes: number[];
  roll_from_expiry: string | null;
  roll_to_expiry: string | null;
  roll_from_strike: number | null;
  roll_to_strike: number | null;
  roll_strike_delta: number | null;
  roll_expiry_days_delta: number | null;
  startTs: number;
  endTs: number;
  members: string[];
  totalSize: number;
  totalPremium: number;
  count: number;
  placements: NbboPlacementCounts;
  nbboCoverageRatio: number;
  nbboAggressiveBuyRatio: number;
  nbboAggressiveSellRatio: number;
  nbboAggressiveRatio: number;
  sameSizeLegSymmetry: number;
  source_ts: number;
  ingest_ts: number;
  seq: number;
};

const roundTo = (value: number, digits = 4): number => {
  if (!Number.isFinite(value)) {
    return 0;
  }
  return Number(value.toFixed(digits));
};

const emptyPlacements = (): NbboPlacementCounts => ({
  aa: 0,
  a: 0,
  b: 0,
  bb: 0,
  mid: 0,
  missing: 0,
  stale: 0
});

const mergePlacements = (legs: LegEvidence[]): NbboPlacementCounts => {
  const merged = emptyPlacements();
  for (const leg of legs) {
    merged.aa += leg.placements.aa;
    merged.a += leg.placements.a;
    merged.b += leg.placements.b;
    merged.bb += leg.placements.bb;
    merged.mid += leg.placements.mid;
    merged.missing += leg.placements.missing;
    merged.stale += leg.placements.stale;
  }
  return merged;
};

const buildPseudoContractId = (root: string, expiry: string, structureType: string): string => {
  const normalizedRoot = root.trim().toUpperCase();
  return `${normalizedRoot}-${expiry}-STRUCT-${structureType}`;
};

const bucketTs = (value: number, bucketMs: number): number => {
  if (!Number.isFinite(value) || value <= 0 || !Number.isFinite(bucketMs) || bucketMs <= 0) {
    return 0;
  }
  return Math.floor(value / bucketMs) * bucketMs;
};

const uniqueSorted = (values: string[]): string[] => {
  return Array.from(new Set(values)).sort();
};

const uniqueSortedNumbers = (values: number[]): number[] => {
  return Array.from(new Set(values)).sort((a, b) => a - b);
};

const medianNumber = (values: number[]): number | null => {
  if (values.length === 0) {
    return null;
  }
  const sorted = values.slice().sort((a, b) => a - b);
  const mid = Math.floor(sorted.length / 2);
  if (sorted.length % 2 === 1) {
    return sorted[mid] ?? null;
  }
  const a = sorted[mid - 1];
  const b = sorted[mid];
  if (!Number.isFinite(a) || !Number.isFinite(b)) {
    return null;
  }
  return (a + b) / 2;
};

const dayDiff = (from: string | null, to: string | null): number | null => {
  if (!from || !to) {
    return null;
  }
  const fromTs = Date.parse(`${from}T00:00:00Z`);
  const toTs = Date.parse(`${to}T00:00:00Z`);
  if (!Number.isFinite(fromTs) || !Number.isFinite(toTs)) {
    return null;
  }
  const diffMs = toTs - fromTs;
  return Math.round(diffMs / 86_400_000);
};

const sameSizeLegSymmetry = (legs: LegEvidence[]): number => {
  const sizes = legs
    .map((leg) => leg.totalSize)
    .filter((value) => Number.isFinite(value) && value > 0);
  if (sizes.length < 2) {
    return 0;
  }
  const min = Math.min(...sizes);
  const max = Math.max(...sizes);
  if (!Number.isFinite(min) || !Number.isFinite(max) || max <= 0) {
    return 0;
  }
  return min / max;
};

export const shouldEmitStructurePacket = (
  legs: LegEvidence[],
  currentLegContractId: string
): boolean => {
  if (legs.length < 2) {
    return false;
  }

  const current = legs.find((leg) => leg.contractId === currentLegContractId);
  if (!current) {
    return false;
  }

  const maxEnd = legs.reduce((max, leg) => Math.max(max, leg.endTs), 0);
  return current.endTs >= maxEnd;
};

export const planStructurePacket = (
  legs: LegEvidence[],
  summary: StructureSummary,
  clusterWindowMs: number
): StructurePacketPlan | null => {
  if (legs.length < 2) {
    return null;
  }

  const rootRaw = legs[0]?.root;
  if (!rootRaw) {
    return null;
  }

  const expiries = uniqueSorted(legs.map((leg) => leg.expiry));
  const expiry = expiries[0];
  if (!expiry) {
    return null;
  }

  const strikes = uniqueSortedNumbers(legs.map((leg) => leg.strike));

  let rollFromExpiry: string | null = null;
  let rollToExpiry: string | null = null;
  let rollFromStrike: number | null = null;
  let rollToStrike: number | null = null;
  let rollStrikeDelta: number | null = null;
  let rollExpiryDaysDelta: number | null = null;

  if (summary.type === "roll" && expiries.length >= 2) {
    rollFromExpiry = expiries[0] ?? null;
    rollToExpiry = expiries[expiries.length - 1] ?? null;

    const strikesByExpiry = new Map<string, number[]>();
    for (const leg of legs) {
      const bucket = strikesByExpiry.get(leg.expiry);
      if (bucket) {
        bucket.push(leg.strike);
      } else {
        strikesByExpiry.set(leg.expiry, [leg.strike]);
      }
    }

    rollFromStrike = medianNumber(strikesByExpiry.get(rollFromExpiry) ?? []) ?? null;
    rollToStrike = medianNumber(strikesByExpiry.get(rollToExpiry) ?? []) ?? null;

    if (rollFromStrike !== null && rollToStrike !== null) {
      rollStrikeDelta = roundTo(rollToStrike - rollFromStrike, 4);
    }

    rollExpiryDaysDelta = dayDiff(rollFromExpiry, rollToExpiry);
  }

  const contractIds = uniqueSorted(legs.map((leg) => leg.contractId));
  const startTs = legs.reduce((min, leg) => Math.min(min, leg.startTs), Number.POSITIVE_INFINITY);
  const endTs = legs.reduce((max, leg) => Math.max(max, leg.endTs), 0);
  const bucketStartTs = bucketTs(startTs, clusterWindowMs);
  const pseudoContractId = buildPseudoContractId(rootRaw, expiry, summary.type);
  const id = `flowpacket:${pseudoContractId}:${bucketStartTs}:${contractIds.join("|")}`;
  const dedupeKey = `${pseudoContractId}:${bucketStartTs}:${contractIds.join("|")}`;

  const members = uniqueSorted(legs.flatMap((leg) => leg.members));
  const totalPremium = legs.reduce((sum, leg) => sum + leg.totalPremium, 0);
  const totalSize = legs.reduce((sum, leg) => sum + leg.totalSize, 0);
  const count = legs.reduce((sum, leg) => sum + leg.members.length, 0);
  const placements = mergePlacements(legs);
  const placementTotal =
    placements.aa + placements.a + placements.b + placements.bb + placements.mid;
  const aggressiveTotal = placements.aa + placements.a + placements.b + placements.bb;
  const aggressiveBuy = placements.aa + placements.a;
  const aggressiveSell = placements.bb + placements.b;
  const nbboCoverageRatio = count > 0 ? placementTotal / count : 0;
  const nbboAggressiveBuyRatio = aggressiveTotal > 0 ? aggressiveBuy / aggressiveTotal : 0;
  const nbboAggressiveSellRatio = aggressiveTotal > 0 ? aggressiveSell / aggressiveTotal : 0;
  const nbboAggressiveRatio = placementTotal > 0 ? aggressiveTotal / placementTotal : 0;

  const source_ts = legs.reduce(
    (min, leg) => Math.min(min, leg.source_ts),
    Number.POSITIVE_INFINITY
  );
  const ingest_ts = legs.reduce((max, leg) => Math.max(max, leg.ingest_ts), 0);
  const seq = legs.reduce((max, leg) => Math.max(max, leg.seq), 0);

  return {
    id,
    dedupeKey,
    bucketStartTs,
    root: rootRaw.trim().toUpperCase(),
    pseudoContractId,
    expiries,
    strikes,
    roll_from_expiry: rollFromExpiry,
    roll_to_expiry: rollToExpiry,
    roll_from_strike: rollFromStrike,
    roll_to_strike: rollToStrike,
    roll_strike_delta: rollStrikeDelta,
    roll_expiry_days_delta: rollExpiryDaysDelta,
    startTs: Number.isFinite(startTs) ? startTs : 0,
    endTs,
    members,
    totalSize,
    totalPremium,
    count,
    placements,
    nbboCoverageRatio,
    nbboAggressiveBuyRatio,
    nbboAggressiveSellRatio,
    nbboAggressiveRatio,
    sameSizeLegSymmetry: roundTo(sameSizeLegSymmetry(legs)),
    source_ts: Number.isFinite(source_ts) ? source_ts : 0,
    ingest_ts,
    seq
  };
};

export const buildStructureFlowPacket = (
  plan: StructurePacketPlan,
  summary: StructureSummary
): FlowPacket => {
  const totalPremium = roundTo(plan.totalPremium);
  const totalNotional = roundTo(totalPremium * 100, 2);
  const windowMs = Math.max(0, plan.endTs - plan.startTs);

  const features: Record<string, string | number | boolean> = {
    packet_kind: "structure",
    option_contract_id: plan.pseudoContractId,
    underlying_id: plan.root,
    count: plan.count,
    total_size: plan.totalSize,
    total_premium: totalPremium,
    total_notional: totalNotional,
    start_ts: plan.startTs,
    end_ts: plan.endTs,
    window_ms: windowMs,
    structure_type: summary.type,
    structure_legs: summary.legs,
    structure_strikes: summary.strikes,
    structure_strike_span: roundTo(summary.strikeSpan),
    structure_rights: summary.rights,
    structure_contract_ids: summary.contractIds.join(","),
    structure_expiries_count: plan.expiries.length,
    structure_expiries: plan.expiries.join(","),
    structure_strikes_list: plan.strikes.join(",")
  };

  if (summary.type === "roll") {
    if (plan.roll_from_expiry) {
      features.roll_from_expiry = plan.roll_from_expiry;
    }
    if (plan.roll_to_expiry) {
      features.roll_to_expiry = plan.roll_to_expiry;
    }
    if (plan.roll_from_strike !== null) {
      features.roll_from_strike = plan.roll_from_strike;
    }
    if (plan.roll_to_strike !== null) {
      features.roll_to_strike = plan.roll_to_strike;
    }
    if (plan.roll_strike_delta !== null) {
      features.roll_strike_delta = plan.roll_strike_delta;
    }
    if (plan.roll_expiry_days_delta !== null) {
      features.roll_expiry_days_delta = plan.roll_expiry_days_delta;
    }
  }

  // These are aggregate counts across the legs. We do not attach rolling z-scores
  // (baseline is per-contract), so structure packets default to absolute thresholds.
  features.nbbo_aa_count = plan.placements.aa;
  features.nbbo_a_count = plan.placements.a;
  features.nbbo_b_count = plan.placements.b;
  features.nbbo_bb_count = plan.placements.bb;
  features.nbbo_mid_count = plan.placements.mid;
  features.nbbo_missing_count = plan.placements.missing;
  features.nbbo_stale_count = plan.placements.stale;
  features.nbbo_coverage_ratio = roundTo(plan.nbboCoverageRatio);
  features.nbbo_aggressive_buy_ratio = roundTo(plan.nbboAggressiveBuyRatio);
  features.nbbo_aggressive_sell_ratio = roundTo(plan.nbboAggressiveSellRatio);
  features.nbbo_aggressive_ratio = roundTo(plan.nbboAggressiveRatio);
  features.same_size_leg_symmetry = roundTo(plan.sameSizeLegSymmetry);

  const join_quality: Record<string, number> = {
    nbbo_coverage_ratio: roundTo(plan.nbboCoverageRatio)
  };

  return {
    source_ts: plan.source_ts,
    ingest_ts: plan.ingest_ts,
    seq: plan.seq,
    trace_id: plan.id,
    id: plan.id,
    members: plan.members,
    features,
    join_quality
  };
};
