import type { CpuSample, RawHang, RawLeak, UiHang, MemoryLeak } from "../types";
import { findDominantFunction, extractAppCallChain } from "./02-aggregate";

// ---------------------------------------------------------------------------
// Hang <-> CPU correlation
// ---------------------------------------------------------------------------

const TOP_N_FUNCTIONS = 5;
const TOP_N_CHAINS = 3;

export interface CorrelationResult {
  uiHangs: UiHang[];
  hangSampleTimestamps: Set<number>;
}

export function correlateHangsWithCpu(
  hangs: RawHang[],
  cpuSamples: CpuSample[]
): CorrelationResult {
  // Sort samples by timestamp for efficient windowing
  const sortedSamples = [...cpuSamples].sort((a, b) => a.timestampNs - b.timestampNs);

  const hangSampleTimestamps = new Set<number>();

  const uiHangs = hangs.map((hang) => {
    const windowStart = hang.startNs;
    const windowEnd = hang.startNs + hang.durationNs;

    // Find CPU samples within the hang window
    const windowSamples = sortedSamples.filter(
      (s) => s.timestampNs >= windowStart && s.timestampNs <= windowEnd
    );

    // Track all sample timestamps that fell in hang windows
    for (const sample of windowSamples) {
      hangSampleTimestamps.add(sample.timestampNs);
    }

    // Count function frequency across all threads in the window
    const funcCounts = new Map<string, number>();
    // Count call chain frequency
    const chainCounts = new Map<string, { chain: string[]; count: number }>();

    for (const sample of windowSamples) {
      const dominant = findDominantFunction(sample.stack);
      if (dominant) {
        funcCounts.set(dominant, (funcCounts.get(dominant) ?? 0) + 1);
      }

      const chain = extractAppCallChain(sample.stack);
      if (chain.length > 0) {
        const chainKey = chain.join(" > ");
        const existing = chainCounts.get(chainKey);
        if (existing) {
          existing.count++;
        } else {
          chainCounts.set(chainKey, { chain, count: 1 });
        }
      }
    }

    // Sort by frequency, take top N
    const suspectedFunctions = [...funcCounts.entries()]
      .sort((a, b) => b[1] - a[1])
      .slice(0, TOP_N_FUNCTIONS)
      .map(([fn]) => fn);

    // Top call chains by frequency
    const appCallChains = [...chainCounts.values()]
      .sort((a, b) => b.count - a.count)
      .slice(0, TOP_N_CHAINS)
      .map(({ chain, count }) => ({ chain, sampleCount: count }));

    const durationMs = Math.round(hang.durationNs / 1_000_000);
    const severity = classifyHangSeverity(hang.hangType);

    // Format start time from nanoseconds
    const totalMs = Math.round(hang.startNs / 1_000_000);
    const minutes = Math.floor(totalMs / 60_000);
    const seconds = Math.floor((totalMs % 60_000) / 1000);
    const ms = totalMs % 1000;
    const startTimeFormatted = `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}.${String(ms).padStart(3, "0")}`;

    return {
      type: "ui_hang" as const,
      platform: "ios" as const,
      hangType: hang.hangType,
      durationMs,
      startTimeFormatted,
      startNs: hang.startNs,
      endNs: hang.startNs + hang.durationNs,
      suspectedFunctions,
      appCallChains,
      severity,
    };
  });

  return { uiHangs, hangSampleTimestamps };
}

function classifyHangSeverity(hangType: string): "RED" | "YELLOW" {
  const lower = hangType.toLowerCase();
  if (lower.includes("severe") || lower === "hang") return "RED";
  return "YELLOW"; // Microhang
}

// ---------------------------------------------------------------------------
// Leak aggregation
// ---------------------------------------------------------------------------

/**
 * xctrace only records malloc-stack history when the target is launched under
 * the Allocations/Leaks instrument. Argent records via `xctrace --attach`, so
 * leaks of pre-existing allocations — in practice most leaks on the simulator —
 * come back with no responsible frame: xctrace emits the sentinel
 * `<Call stack limit reached>` and an empty library. Those leaks are not
 * actionable and are dominated by benign system allocations (`Malloc N Bytes`,
 * `xpc_*`), so they are reported as a low-confidence (YELLOW) signal rather than
 * a confirmed (RED) app leak. Verified empirically against xctrace 16: both an
 * idle system app and an actively-allocating RN app return only unattributed
 * leaks under `--attach`, while CPU/hang stacks symbolicate normally.
 */
const UNATTRIBUTED_LEAK_FRAMES = new Set(["", "Unknown", "<Call stack limit reached>"]);

export function isLeakAttributed(responsibleFrame: string): boolean {
  return !UNATTRIBUTED_LEAK_FRAMES.has(responsibleFrame.trim());
}

export function aggregateLeaks(rawLeaks: RawLeak[]): MemoryLeak[] {
  const groups = new Map<
    string,
    { objectType: string; totalSize: number; count: number; frame: string; library: string }
  >();

  for (const leak of rawLeaks) {
    // Group by object type AND responsible frame: with malloc_stack_logging the
    // same object type can leak from several distinct call sites, and each site
    // is its own finding — merging on type alone would attribute them all to
    // whichever was parsed first and hide the rest. Key on the frame NORMALIZED
    // the way isLeakAttributed() matches it (trimmed, with every unattributed
    // spelling — "", "Unknown", the "<Call stack limit reached>" sentinel —
    // collapsed to one bucket), not verbatim: an export can mix those spellings
    // for one object type (a missing responsible-frame attribute parses as
    // "Unknown"), and keying verbatim would split what is a single unattributed
    // group per type. Under `--attach` (no stacks) everything is unattributed,
    // so this collapses back to per-type.
    // The delimiter is a NUL, written as the `\u0000` escape rather than a
    // raw NUL byte: a raw NUL makes git treat this whole source file as binary
    // and hides the diff. Object types and demangled frames never contain a NUL,
    // so the composite key can't collide the way a printable separator could.
    const frameKey = isLeakAttributed(leak.responsibleFrame) ? leak.responsibleFrame.trim() : "";
    const key = `${leak.objectType}\u0000${frameKey}`;
    const existing = groups.get(key);
    if (existing) {
      existing.totalSize += leak.sizeBytes * leak.count;
      existing.count += leak.count;
    } else {
      groups.set(key, {
        objectType: leak.objectType,
        totalSize: leak.sizeBytes * leak.count,
        count: leak.count,
        frame: leak.responsibleFrame,
        library: leak.responsibleLibrary,
      });
    }
  }

  return [...groups.values()]
    .map((g) => {
      const attributed = isLeakAttributed(g.frame);
      return {
        type: "memory_leak" as const,
        platform: "ios" as const,
        objectType: g.objectType,
        totalSizeBytes: g.totalSize,
        count: g.count,
        responsibleFrame: g.frame,
        responsibleLibrary: g.library,
        attributed,
        severity: (attributed ? "RED" : "YELLOW") as "RED" | "YELLOW",
      };
    })
    .sort((a, b) =>
      a.attributed === b.attributed ? b.totalSizeBytes - a.totalSizeBytes : a.attributed ? -1 : 1
    );
}
