"""Fused analyzer that computes both graph matching AND operation stats in a single pass.

This avoids loading traces twice by computing operation-level statistics
during the graph matching loop.
"""

import bisect
from collections import Counter, defaultdict
from pathlib import Path
from typing import Any

import orjson

from .classifier import classify
from .graph_matcher import match_kernel_sequences


def analyze_and_match_fused(
    trace1_path: str | Path,
    trace2_path: str | Path,
    min_graph_size: int = 300,
) -> tuple[dict[str, Any], dict[str, Any], dict[str, Any]]:
    """Fused analysis that computes all results in one pass.

    This loads each trace file once and computes:
    1. Operation-level comparison (from analyze_traces)
    2. Graph-based kernel matching (from match_traces)
    3. Fusion analysis (from analyze_fusion_differences)

    All three are computed inline during the same iteration over kernel matches,
    avoiding redundant work and I/O.

    Args:
        trace1_path: Path to first trace file
        trace2_path: Path to second trace file
        min_graph_size: Threshold for large CUDA graphs

    Returns:
        Tuple of (analyze_result, match_result, fusion_result)
    """
    trace1_path = Path(trace1_path)
    trace2_path = Path(trace2_path)

    # Load raw trace data once using orjson (much faster than stdlib json)
    with open(trace1_path, "rb") as f:
        trace1_data = orjson.loads(f.read())
    with open(trace2_path, "rb") as f:
        trace2_data = orjson.loads(f.read())

    # Extract platform and device info
    trace1_platform = _extract_platform(trace1_data)
    trace2_platform = _extract_platform(trace2_data)
    trace1_gpu, trace1_device = _extract_device_info(trace1_data)
    trace2_gpu, trace2_device = _extract_device_info(trace2_data)

    # Group kernels by correlation ID and extract python stacks (single pass per trace)
    trace1_graphs, trace1_python_intervals, trace1_python_by_id, trace1_python_starts = _group_by_correlation_and_extract_stacks(trace1_data, trace1_platform)
    trace2_graphs, trace2_python_intervals, trace2_python_by_id, trace2_python_starts = _group_by_correlation_and_extract_stacks(trace2_data, trace2_platform)

    # Initialize operation stats accumulators
    trace1_op_stats = defaultdict(lambda: {"total_us": 0, "count": 0, "kernels": [], "python_stacks": [], "phases": set()})
    trace2_op_stats = defaultdict(lambda: {"total_us": 0, "count": 0, "kernels": [], "python_stacks": [], "phases": set()})

    # Initialize fusion stats accumulators (computed inline during matching)
    from collections import Counter
    amd_only_counts = Counter()
    nv_only_counts = Counter()
    matched_counts = Counter()
    amd_only_times = defaultdict(float)
    nv_only_times = defaultdict(float)
    matched_amd_times = defaultdict(float)
    matched_nv_times = defaultdict(float)

    # Separate large CUDA graphs from small groups
    trace1_large = [(corr, kernels) for corr, kernels in trace1_graphs if len(kernels) >= min_graph_size]
    trace1_small = [(corr, kernels) for corr, kernels in trace1_graphs if len(kernels) < min_graph_size]
    trace2_large = [(corr, kernels) for corr, kernels in trace2_graphs if len(kernels) >= min_graph_size]
    trace2_small = [(corr, kernels) for corr, kernels in trace2_graphs if len(kernels) < min_graph_size]

    all_matches = []
    graph_pairs = []

    # Process large CUDA graphs (Nth-to-Nth pairing)
    min_large = min(len(trace1_large), len(trace2_large))
    for i in range(min_large):
        corr1, kernels1 = trace1_large[i]
        corr2, kernels2 = trace2_large[i]

        # Accumulate operation stats while matching
        _accumulate_op_stats(kernels1, trace1_platform, trace1_op_stats, trace1_python_intervals, trace1_python_by_id, trace1_python_starts)
        _accumulate_op_stats(kernels2, trace2_platform, trace2_op_stats, trace2_python_intervals, trace2_python_by_id, trace2_python_starts)

        # Match kernels within this graph pair
        # match_kernel_sequences expects (amd_kernels, nv_kernels), so pass in correct order
        amd_kernels = kernels1 if trace1_platform == "AMD" else kernels2
        nv_kernels = kernels2 if trace2_platform == "NVIDIA" else kernels1
        matches = match_kernel_sequences(amd_kernels, nv_kernels)
        all_matches.extend(matches)

        # Accumulate fusion stats inline (avoid re-iterating through matches later)
        for match in matches:
            status = match['status']
            if status == 'AMD_ONLY':
                ktype = match['amd_type']
                amd_only_counts[ktype] += 1
                if match.get('amd_kernel'):
                    amd_only_times[ktype] += match['amd_kernel'].get('dur', 0)
            elif status == 'NV_ONLY':
                ktype = match['nv_type']
                nv_only_counts[ktype] += 1
                if match.get('nv_kernel'):
                    nv_only_times[ktype] += match['nv_kernel'].get('dur', 0)
            elif status == 'MATCH':
                ktype = match['amd_type']
                matched_counts[ktype] += 1
                if match.get('amd_kernel'):
                    matched_amd_times[ktype] += match['amd_kernel'].get('dur', 0)
                if match.get('nv_kernel'):
                    matched_nv_times[ktype] += match['nv_kernel'].get('dur', 0)

        graph_pairs.append({
            "amd_correlation": corr1 if trace1_platform == "AMD" else corr2,
            "nv_correlation": corr2 if trace2_platform == "NVIDIA" else corr1,
            "amd_corr_id": corr1 if trace1_platform == "AMD" else corr2,
            "nv_corr_id": corr2 if trace2_platform == "NVIDIA" else corr1,
            "amd_kernels": kernels1 if trace1_platform == "AMD" else kernels2,
            "nv_kernels": kernels2 if trace2_platform == "NVIDIA" else kernels1,
            "matches": matches,
            "graph_type": "large",
        })

    # Process small groups (phase-separated pattern matching)
    # Accumulate stats first
    for corr1, kernels1 in trace1_small:
        _accumulate_op_stats(kernels1, trace1_platform, trace1_op_stats, trace1_python_intervals, trace1_python_by_id, trace1_python_starts)

    for corr2, kernels2 in trace2_small:
        _accumulate_op_stats(kernels2, trace2_platform, trace2_op_stats, trace2_python_intervals, trace2_python_by_id, trace2_python_starts)

    # Group small kernels by phase for better matching accuracy
    # This prevents cross-phase matching drift (decode vs prefill)
    amd_small_kernels = []
    nv_small_kernels = []

    for _, kernels in trace1_small:
        if trace1_platform == "AMD":
            amd_small_kernels.extend(kernels)
        else:
            nv_small_kernels.extend(kernels)

    for _, kernels in trace2_small:
        if trace2_platform == "NVIDIA":
            nv_small_kernels.extend(kernels)
        else:
            amd_small_kernels.extend(kernels)

    # Group by phase
    amd_by_phase = defaultdict(list)
    nv_by_phase = defaultdict(list)

    for k in amd_small_kernels:
        phase = k.get('phase', 'unknown')
        amd_by_phase[phase].append(k)

    for k in nv_small_kernels:
        phase = k.get('phase', 'unknown')
        nv_by_phase[phase].append(k)

    # Match each phase separately using the optimized DP algorithm
    all_phases = set(amd_by_phase.keys()) | set(nv_by_phase.keys())

    for phase in sorted(all_phases):
        amd_phase_kernels = amd_by_phase.get(phase, [])
        nv_phase_kernels = nv_by_phase.get(phase, [])

        if amd_phase_kernels or nv_phase_kernels:
            # Use same DP algorithm as large graphs
            phase_matches = match_kernel_sequences(amd_phase_kernels, nv_phase_kernels)
            all_matches.extend(phase_matches)

            # Accumulate fusion stats for small groups
            for match in phase_matches:
                status = match['status']
                if status == 'AMD_ONLY':
                    ktype = match['amd_type']
                    amd_only_counts[ktype] += 1
                    if match.get('amd_kernel'):
                        amd_only_times[ktype] += match['amd_kernel'].get('dur', 0)
                elif status == 'NV_ONLY':
                    ktype = match['nv_type']
                    nv_only_counts[ktype] += 1
                    if match.get('nv_kernel'):
                        nv_only_times[ktype] += match['nv_kernel'].get('dur', 0)
                elif status == 'MATCH':
                    ktype = match['amd_type']
                    matched_counts[ktype] += 1
                    if match.get('amd_kernel'):
                        matched_amd_times[ktype] += match['amd_kernel'].get('dur', 0)
                    if match.get('nv_kernel'):
                        matched_nv_times[ktype] += match['nv_kernel'].get('dur', 0)

            # Add all phase matches as a single graph pair
            # UI will handle grouping/chunking via groupIntoBlocks
            graph_pairs.append({
                "amd_correlation": -1,  # No specific correlation for small groups
                "nv_correlation": -1,
                "amd_corr_id": -1,
                "nv_corr_id": -1,
                "amd_kernels": amd_phase_kernels,
                "nv_kernels": nv_phase_kernels,
                "matches": phase_matches,
                "graph_type": "small",
            })

    # Build operation comparison result
    analyze_result = _build_analyze_result(
        trace1_path, trace2_path,
        trace1_platform, trace2_platform,
        trace1_gpu, trace1_device,
        trace2_gpu, trace2_device,
        trace1_op_stats, trace2_op_stats,
        trace1_graphs, trace2_graphs
    )

    # Build graph matching result
    match_result = {
        "amd_platform": trace1_platform if trace1_platform == "AMD" else trace2_platform,
        "nv_platform": trace2_platform if trace2_platform == "NVIDIA" else trace1_platform,
        "amd_graphs": [],
        "nv_graphs": [],
        "graph_pairs": graph_pairs,
        "summary": {
            "num_graph_pairs": len(graph_pairs),
            "total_kernel_pairs": len(all_matches),
            "matched": sum(1 for m in all_matches if m.get("status") == "MATCH"),
            "amd_only": sum(1 for m in all_matches if m.get("status") == "AMD_ONLY"),
            "nv_only": sum(1 for m in all_matches if m.get("status") == "NV_ONLY"),
            "mismatch": sum(1 for m in all_matches if m.get("status") == "MISMATCH"),
            "match_rate": sum(1 for m in all_matches if m.get("status") == "MATCH") / len(all_matches) if all_matches else 0,
        }
    }

    # Build fusion result (computed inline, no re-iteration needed)
    amd_platform = trace1_platform if trace1_platform == "AMD" else trace2_platform
    nv_platform = trace2_platform if trace2_platform == "NVIDIA" else trace1_platform

    # Get total kernel counts
    trace1_kernel_count = sum(len(kernels) for _, kernels in trace1_graphs)
    trace2_kernel_count = sum(len(kernels) for _, kernels in trace2_graphs)

    # Build global_counts dictionary
    all_ktypes = set(amd_only_counts.keys()) | set(nv_only_counts.keys()) | set(matched_counts.keys())
    global_counts = {}
    for ktype in all_ktypes:
        amd_count = amd_only_counts.get(ktype, 0) + matched_counts.get(ktype, 0)
        nv_count = nv_only_counts.get(ktype, 0) + matched_counts.get(ktype, 0)
        global_counts[ktype] = {
            "trace1_count": amd_count if amd_platform == "AMD" else nv_count,
            "trace2_count": nv_count if nv_platform == "NVIDIA" else amd_count,
        }

    # Detect fusion opportunities (count-based detection)
    fusion_opportunities = []
    for ktype in all_ktypes:
        amd_total = amd_only_counts.get(ktype, 0) + matched_counts.get(ktype, 0)
        nv_total = nv_only_counts.get(ktype, 0) + matched_counts.get(ktype, 0)

        # Calculate timings (convert from microseconds to milliseconds)
        amd_time_ms = (amd_only_times.get(ktype, 0) + matched_amd_times.get(ktype, 0)) / 1000
        nv_time_ms = (nv_only_times.get(ktype, 0) + matched_nv_times.get(ktype, 0)) / 1000

        # Skip if counts are trivial
        if amd_total + nv_total < 20:
            continue

        # Calculate significance
        diff_ratio = amd_total / nv_total if nv_total > 0 else float('inf')
        reverse_ratio = nv_total / amd_total if amd_total > 0 else float('inf')

        # Report significant differences (ratio > 2.0 or one platform doesn't have it)
        is_significant = (
            (diff_ratio > 10.0 or reverse_ratio > 10.0) or
            ((diff_ratio > 2.0 or reverse_ratio > 2.0) and (amd_total + nv_total > 20))
        )

        if is_significant:
            # Determine who fuses (who has FEWER calls = more fusion)
            if diff_ratio > 1.5:
                fused_by = "Trace 2"  # Trace 2 has fewer calls
                ratio = diff_ratio
            else:
                fused_by = "Trace 1"
                ratio = reverse_ratio

            time_ratio = amd_time_ms / nv_time_ms if nv_time_ms > 0 else float('inf')

            fusion_opportunities.append({
                'kernel_type': ktype,
                'trace1_total': amd_total,
                'trace2_total': nv_total,
                'trace1_avg_per_group': amd_total / len(graph_pairs) if len(graph_pairs) > 0 else 0,
                'trace2_avg_per_group': nv_total / len(graph_pairs) if len(graph_pairs) > 0 else 0,
                'trace1_time_ms': amd_time_ms,
                'trace2_time_ms': nv_time_ms,
                'time_ratio': time_ratio,
                'ratio': ratio,
                'fused_by': fused_by,
                'groups_affected': len(graph_pairs),
                'total_groups': len(graph_pairs),
            })

    # Sort by impact
    fusion_opportunities.sort(
        key=lambda x: x['ratio'] * (x['trace1_total'] + x['trace2_total']),
        reverse=True
    )

    # Detect fusion mappings (pattern-based detection)
    fusion_mappings = _detect_fusion_mappings_inline(
        all_matches, amd_platform, nv_platform,
        amd_only_counts, nv_only_counts, matched_counts
    )

    fusion_result = {
        "metadata": {
            "trace1_gpu": trace1_gpu,
            "trace2_gpu": trace2_gpu,
            "trace1_platform": trace1_platform,
            "trace2_platform": trace2_platform,
            "trace1_total_kernels": trace1_kernel_count,
            "trace2_total_kernels": trace2_kernel_count,
            "trace1_correlation_groups": len(trace1_graphs),
            "trace2_correlation_groups": len(trace2_graphs),
            "matched_groups": len(graph_pairs),
            "fusion_mappings_total": len(fusion_mappings),
            "fusion_mappings_verified": len(fusion_mappings),
            "fusion_mappings_filtered": 0,
        },
        "global_counts": global_counts,
        "fusion_opportunities": fusion_opportunities,
        "fusion_mappings": fusion_mappings,
    }

    return analyze_result, match_result, fusion_result


def _extract_platform(trace_data: dict) -> str:
    """Extract platform name from trace data."""
    props = trace_data.get("deviceProperties", [{}])[0]
    is_amd = trace_data.get("roctracer_version") or props.get("warpSize") == 64
    return "AMD" if is_amd else "NVIDIA"


def _extract_device_info(trace_data: dict) -> tuple[str, dict]:
    """Extract GPU name and device properties from trace data."""
    if "deviceProperties" in trace_data and len(trace_data["deviceProperties"]) > 0:
        dev = trace_data["deviceProperties"][0]
        gpu_name = dev.get("name", "Unknown GPU")

        # Build device properties dict matching expected format
        device_props = {
            "name": gpu_name,
            "compute_capability": dev.get("computeCapability", "Unknown"),
            "total_memory_gb": dev.get("totalGlobalMem", 0) / (1024**3) if "totalGlobalMem" in dev else 0,
            "sm_count": dev.get("multiProcessorCount", 0),
            "warp_size": dev.get("warpSize", 32),
            "max_threads_per_block": dev.get("maxThreadsPerBlock", 1024),
            "shared_mem_per_block_kb": dev.get("sharedMemPerBlock", 0) / 1024 if "sharedMemPerBlock" in dev else 0,
        }
        return gpu_name, device_props

    return "Unknown GPU", {
        "name": "Unknown GPU",
        "compute_capability": "Unknown",
        "total_memory_gb": 0,
        "sm_count": 0,
        "warp_size": 32,
        "max_threads_per_block": 1024,
        "shared_mem_per_block_kb": 0,
    }


def _extract_phases(trace_data: dict) -> tuple[list[int], list[str], list[int]]:
    """Extract phase information from user_annotation events.

    Returns:
        Tuple of (phase_starts, phase_types, phase_ends) for binary search lookup
    """
    events = trace_data.get("traceEvents", [])
    phases = []

    for ev in events:
        if ev.get("cat") == "user_annotation":
            name = ev.get("name", "")
            if name.startswith("execute_context"):
                # Parse execute_context_N(K)_generation_M(T)
                num_ctx_tokens = 0
                num_gen_tokens = 0

                try:
                    if "_context_" in name:
                        ctx_part = name.split("_context_")[1].split("_")[0]
                        if "(" in ctx_part and ")" in ctx_part:
                            num_ctx_tokens = int(ctx_part.split("(")[1].split(")")[0])

                    if "_generation_" in name:
                        gen_part = name.split("_generation_")[1]
                        if "(" in gen_part and ")" in gen_part:
                            num_gen_tokens = int(gen_part.split("(")[1].split(")")[0])
                except (ValueError, IndexError):
                    pass

                # Determine phase based on vLLM scheduler semantics
                if num_ctx_tokens > 0 and num_gen_tokens > 0:
                    phase_type = "mixed"
                elif num_ctx_tokens > 0:
                    phase_type = "prefill"
                elif num_gen_tokens > 0:
                    phase_type = "decode"
                else:
                    phase_type = "idle"

                phases.append({
                    "type": phase_type,
                    "ts_start": ev["ts"],
                    "ts_end": ev["ts"] + ev.get("dur", 0),
                })

    # Pre-sort phases for binary search
    phases.sort(key=lambda p: p["ts_start"])
    phase_starts = [p["ts_start"] for p in phases]
    phase_types = [p["type"] for p in phases]
    phase_ends = [p["ts_end"] for p in phases]

    return phase_starts, phase_types, phase_ends


def _group_by_correlation_and_extract_stacks(
    trace_data: dict, platform: str
) -> tuple[list[tuple[int, list[dict]]], list[tuple[int, int, int, int | None, str]], dict[int, dict[str, Any]], list[int]]:
    """Group kernel events by correlation ID, assign phases, and extract python stacks in single pass."""
    events = trace_data.get("traceEvents", [])

    # Extract phase information
    phase_starts, phase_types, phase_ends = _extract_phases(trace_data)

    def _get_phase_for_timestamp(ts: int) -> str:
        """Get phase for a timestamp using binary search."""
        if not phase_starts:
            return "decode"
        idx = bisect.bisect_right(phase_starts, ts) - 1
        if idx >= 0 and phase_starts[idx] <= ts <= phase_ends[idx]:
            return phase_types[idx]
        return "decode"

    # Single pass: extract kernels AND python functions
    kernel_events = []
    python_intervals = []
    python_by_id = {}

    for ev in events:
        cat = ev.get("cat")

        if cat == "kernel":
            # Assign phase and collect kernel
            ev["phase"] = _get_phase_for_timestamp(ev.get("ts", 0))
            kernel_events.append(ev)

        elif cat == "python_function":
            # Collect python function for stack traces
            args = ev.get("args", {})
            py_id = args.get("Python id")
            name = ev["name"]
            ts_start = ev["ts"]
            ts_end = ts_start + ev.get("dur", 0)
            duration = ev.get("dur", 0)
            parent_id = args.get("Python parent id")

            python_intervals.append((ts_start, ts_end, duration, py_id, name))

            if py_id is not None:
                python_by_id[py_id] = {
                    "name": name,
                    "parent_id": parent_id,
                }

    # Sort python intervals by start time for binary search
    python_intervals.sort(key=lambda x: x[0])
    # Pre-build start times list for efficient binary search
    python_starts = [x[0] for x in python_intervals]

    # Group kernels by correlation ID
    by_corr = defaultdict(list)
    for event in kernel_events:
        corr_id = event.get("args", {}).get("correlation", 0)
        by_corr[corr_id].append(event)

    # Sort by first kernel timestamp within each group
    result = []
    for corr_id, kernels in by_corr.items():
        kernels_sorted = sorted(kernels, key=lambda k: k.get("ts", 0))
        result.append((corr_id, kernels_sorted))

    # Sort groups by first kernel timestamp
    result.sort(key=lambda x: x[1][0].get("ts", 0) if x[1] else 0)

    return result, python_intervals, python_by_id, python_starts


def _build_python_stack_index(
    events: list[dict[str, Any]],
) -> tuple[list[tuple[int, int, int, int | None, str]], dict[int, dict[str, Any]]]:
    """Build Python call stack index for kernels."""
    python_intervals = []
    python_by_id = {}

    for ev in events:
        if ev.get("cat") != "python_function":
            continue

        args = ev.get("args", {})
        py_id = args.get("Python id")
        name = ev["name"]
        ts_start = ev["ts"]
        ts_end = ts_start + ev.get("dur", 0)
        duration = ev.get("dur", 0)
        parent_id = args.get("Python parent id")

        python_intervals.append((ts_start, ts_end, duration, py_id, name))

        if py_id is not None:
            python_by_id[py_id] = {
                "name": name,
                "parent_id": parent_id,
            }

    # Sort by start time for binary search
    python_intervals.sort(key=lambda x: x[0])

    return python_intervals, python_by_id


def _get_python_stack_full(
    timestamp: int,
    python_intervals: list[tuple[int, int, int, int | None, str]],
    python_by_id: dict[int, dict[str, Any]],
    python_starts: list[int],  # Pre-built list of start times for binary search
) -> tuple[str | None, list[str]]:
    """Get full Python call stack for a kernel launch."""
    # Find innermost Python function at this timestamp using binary search
    idx = bisect.bisect_right(python_starts, timestamp) - 1

    # Walk backwards to find the innermost function containing this timestamp
    while idx >= 0:
        ts_start, ts_end, _dur, py_id, name = python_intervals[idx]
        if ts_start <= timestamp <= ts_end:
            # Found innermost function - walk up parent chain
            stack = []
            cpu_op = name  # Top of stack is the CPU operation
            current_id = py_id

            while current_id is not None and current_id in python_by_id:
                func = python_by_id[current_id]
                stack.append(func["name"])
                current_id = func.get("parent_id")

            return cpu_op, stack

        idx -= 1

    return None, []


def _accumulate_op_stats(
    kernels: list[dict],
    platform: str,
    op_stats: dict,
    python_intervals: list[tuple[int, int, int, int | None, str]],
    python_by_id: dict[int, dict[str, Any]],
    python_starts: list[int],  # Pre-built list for binary search
    max_stacks: int = 3,
):
    """Accumulate operation statistics for a list of kernels."""
    for kernel in kernels:
        name = kernel.get("name", "")
        dur_us = kernel.get("dur", 0)
        ts = kernel.get("ts", 0)
        phase = kernel.get("phase", "decode")  # Get phase assigned during grouping

        # Classify kernel to operation type
        op_type, _detail = classify(name, platform)
        op_name = op_type.value if hasattr(op_type, "value") else str(op_type)

        # Get Python stack trace
        _cpu_op, python_stack = _get_python_stack_full(ts, python_intervals, python_by_id, python_starts)

        # Accumulate stats
        op_stats[op_name]["total_us"] += dur_us
        op_stats[op_name]["count"] += 1
        op_stats[op_name]["kernels"].append(name)
        op_stats[op_name]["phases"].add(phase)  # Track which phases this operation appears in

        # Collect sample stacks (limit to max_stacks per operation)
        if "python_stacks" not in op_stats[op_name]:
            op_stats[op_name]["python_stacks"] = []
        if python_stack and len(op_stats[op_name]["python_stacks"]) < max_stacks:
            op_stats[op_name]["python_stacks"].append(python_stack)


def _build_analyze_result(
    trace1_path, trace2_path,
    trace1_platform, trace2_platform,
    trace1_gpu, trace1_device,
    trace2_gpu, trace2_device,
    trace1_op_stats, trace2_op_stats,
    trace1_graphs, trace2_graphs
) -> dict[str, Any]:
    """Build the analyze_traces-compatible result from accumulated stats."""

    # Calculate totals
    trace1_total_us = sum(stats["total_us"] for stats in trace1_op_stats.values())
    trace2_total_us = sum(stats["total_us"] for stats in trace2_op_stats.values())
    trace1_kernel_count = sum(stats["count"] for stats in trace1_op_stats.values())
    trace2_kernel_count = sum(stats["count"] for stats in trace2_op_stats.values())

    # Build operations list
    all_ops = set(trace1_op_stats.keys()) | set(trace2_op_stats.keys())
    operations = []

    for op in sorted(all_ops):
        trace1_stats = trace1_op_stats.get(op, {"total_us": 0, "count": 0, "kernels": [], "python_stacks": [], "phases": set()})
        trace2_stats = trace2_op_stats.get(op, {"total_us": 0, "count": 0, "kernels": [], "python_stacks": [], "phases": set()})

        trace1_total = trace1_stats["total_us"]
        trace2_total = trace2_stats["total_us"]
        trace1_count = trace1_stats["count"]
        trace2_count = trace2_stats["count"]

        ratio = trace1_total / trace2_total if trace2_total > 0 else 0
        gap_ms = (trace1_total - trace2_total) / 1000

        # Combine phases from both traces
        all_phases = trace1_stats["phases"] | trace2_stats["phases"]
        phases_list = sorted(list(all_phases)) if all_phases else ["all"]

        operations.append({
            "operation": op,
            "trace1_count": trace1_count,
            "trace2_count": trace2_count,
            "trace1_avg_us": trace1_total / trace1_count if trace1_count > 0 else 0,
            "trace2_avg_us": trace2_total / trace2_count if trace2_count > 0 else 0,
            "trace1_total_ms": trace1_total / 1000,
            "trace2_total_ms": trace2_total / 1000,
            "ratio": ratio,
            "gap_ms": gap_ms,
            "status": "slower" if ratio > 1.1 else ("faster" if ratio < 0.9 else "similar"),
            "trace1_kernel": trace1_stats["kernels"][0] if trace1_stats["kernels"] else "",
            "trace2_kernel": trace2_stats["kernels"][0] if trace2_stats["kernels"] else "",
            "trace1_python_stacks": trace1_stats["python_stacks"],
            "trace2_python_stacks": trace2_stats["python_stacks"],
            "phases": phases_list,
        })

    return {
        "metadata": {
            "trace1_name": str(trace1_path),
            "trace2_name": str(trace2_path),
            "trace1_platform": trace1_platform,
            "trace2_platform": trace2_platform,
            "trace1_gpu": trace1_gpu,
            "trace1_device": trace1_device,
            "trace2_gpu": trace2_gpu,
            "trace2_device": trace2_device,
            "trace1_kernels": trace1_kernel_count,
            "trace2_kernels": trace2_kernel_count,
            "trace1_total_ms": trace1_total_us / 1000,
            "trace2_total_ms": trace2_total_us / 1000,
            "phase": "all",
            "trace1_layers": 0,  # TODO: Extract layer info
            "trace2_layers": 0,
        },
        "operations": operations,
        "layers": [],  # Simplified for now
    }


def _detect_fusion_mappings_inline(
    matches: list[dict[str, Any]],
    amd_platform: str,
    nv_platform: str,
    amd_only_counts: Counter,
    nv_only_counts: Counter,
    matched_counts: Counter,
) -> list[dict[str, Any]]:
    """Detect fusion patterns from aligned kernel sequences (inline version).

    This implements the same logic as fusion_analyzer_graph.py but inline
    to avoid re-iterating through matches.

    OPTIMIZED: Single pass through matches to collect all pattern data.
    """
    mappings = []

    # Collect all pattern data in a single pass
    # Strategy 1: Track what comes before each AMD_ONLY/NV_ONLY kernel
    amd_only_before = defaultdict(Counter)  # ktype -> Counter of what comes before
    nv_only_before = defaultdict(Counter)

    # Strategy 2: Track consecutive chains
    amd_only_chains = defaultdict(lambda: {'total': 0, 'consecutive': 0})
    nv_only_chains = defaultdict(lambda: {'total': 0, 'consecutive': 0})

    prev_match = None
    for match in matches:
        status = match['status']

        if status == 'AMD_ONLY':
            ktype = match['amd_type']
            # Strategy 1: Track predecessor
            if prev_match and prev_match['status'] == 'MATCH':
                amd_only_before[ktype][prev_match['amd_type']] += 1
            # Strategy 2: Track chains
            amd_only_chains[ktype]['total'] += 1
            if prev_match and prev_match['status'] == 'AMD_ONLY' and prev_match['amd_type'] == ktype:
                amd_only_chains[ktype]['consecutive'] += 1

        elif status == 'NV_ONLY':
            ktype = match['nv_type']
            # Strategy 1: Track predecessor
            if prev_match and prev_match['status'] == 'MATCH':
                nv_only_before[ktype][prev_match['nv_type']] += 1
            # Strategy 2: Track chains
            nv_only_chains[ktype]['total'] += 1
            if prev_match and prev_match['status'] == 'NV_ONLY' and prev_match['nv_type'] == ktype:
                nv_only_chains[ktype]['consecutive'] += 1

        prev_match = match

    # Strategy 1: Detect inter-type fusion (AMD_ONLY kernels and what they follow)
    for ktype, before_counter in amd_only_before.items():
        count = amd_only_chains[ktype]['total']
        if count < 10:  # Skip rare occurrences
            continue

        most_common_before = before_counter.most_common(1)
        if most_common_before and most_common_before[0][1] / count > 0.05:
            fusion_target = most_common_before[0][0]
            confidence = most_common_before[0][1] / count
            evidence = f"{amd_platform} runs {fusion_target}+{ktype} separately, {nv_platform} fuses into {fusion_target}"

            mappings.append({
                'fused_platform': nv_platform,
                'fused_kernel_type': fusion_target,
                'fused_count': 0,
                'unfused_platform': amd_platform,
                'unfused_sequence': [fusion_target, ktype],
                'unfused_count_per_type': {fusion_target: count, ktype: count},
                'pattern_count': count,
                'pattern_confidence': confidence,
                'evidence': evidence,
                'verified': True,
                'verification_details': f"Graph-based alignment shows {ktype} appears after {fusion_target} in {most_common_before[0][1]}/{count} cases",
            })

    for ktype, before_counter in nv_only_before.items():
        count = nv_only_chains[ktype]['total']
        if count < 10:
            continue

        most_common_before = before_counter.most_common(1)
        if most_common_before and most_common_before[0][1] / count > 0.05:
            fusion_target = most_common_before[0][0]
            confidence = most_common_before[0][1] / count
            evidence = f"{nv_platform} runs {fusion_target}+{ktype} separately, {amd_platform} fuses into {fusion_target}"

            mappings.append({
                'fused_platform': amd_platform,
                'fused_kernel_type': fusion_target,
                'fused_count': 0,
                'unfused_platform': nv_platform,
                'unfused_sequence': [fusion_target, ktype],
                'unfused_count_per_type': {fusion_target: count, ktype: count},
                'pattern_count': count,
                'pattern_confidence': confidence,
                'evidence': evidence,
                'verified': True,
                'verification_details': f"Graph-based alignment shows {ktype} appears after {fusion_target} in {most_common_before[0][1]}/{count} cases",
            })

    # Strategy 2: Detect intra-type fusion (chains of same-type kernels)
    for ktype, chain_info in amd_only_chains.items():
        if chain_info['total'] < 100:  # Skip small patterns
            continue

        chain_ratio = chain_info['consecutive'] / chain_info['total']
        if chain_ratio > 0.8:  # >80% appear in chains
            mappings.append({
                'fused_platform': nv_platform,
                'fused_kernel_type': ktype,
                'fused_count': 0,
                'unfused_platform': amd_platform,
                'unfused_sequence': [ktype, ktype],
                'unfused_count_per_type': {ktype: chain_info['total']},
                'pattern_count': chain_info['total'],
                'pattern_confidence': chain_ratio,
                'evidence': f"{amd_platform} runs {ktype} in chains ({chain_info['consecutive']} consecutive out of {chain_info['total']} total, {chain_ratio:.1%}), {nv_platform} fuses them",
                'verified': True,
                'verification_details': f"Graph-based alignment shows {chain_ratio:.1%} of {ktype} kernels appear consecutively",
            })

    for ktype, chain_info in nv_only_chains.items():
        if chain_info['total'] < 100:
            continue

        chain_ratio = chain_info['consecutive'] / chain_info['total']
        if chain_ratio > 0.8:
            mappings.append({
                'fused_platform': amd_platform,
                'fused_kernel_type': ktype,
                'fused_count': 0,
                'unfused_platform': nv_platform,
                'unfused_sequence': [ktype, ktype],
                'unfused_count_per_type': {ktype: chain_info['total']},
                'pattern_count': chain_info['total'],
                'pattern_confidence': chain_ratio,
                'evidence': f"{nv_platform} runs {ktype} in chains ({chain_info['consecutive']} consecutive out of {chain_info['total']} total, {chain_ratio:.1%}), {amd_platform} fuses them",
                'verified': True,
                'verification_details': f"Graph-based alignment shows {chain_ratio:.1%} of {ktype} kernels appear consecutively",
            })

    return mappings
