use runmat_accelerate::fusion::FusionGroup;
use runmat_accelerate::graph::ShapeInfo;

use super::{
    FusionPlanDecision, FusionPlanEdge, FusionPlanNode, FusionPlanShader, FusionPlanSnapshot,
    FusionPlannerMetadata,
};

pub(crate) fn build_fusion_snapshot(
    groups: &[FusionGroup],
    candidate_groups: &[runmat_vm::FusionCandidateGroup],
    instruction_windows: &[runmat_vm::FusionInstructionWindow],
    planner: Option<FusionPlannerMetadata>,
) -> Option<FusionPlanSnapshot> {
    let planner = planner.unwrap_or_default();
    let accel_graph_state = if planner.accel_graph_state.is_empty() {
        "unknown"
    } else {
        planner.accel_graph_state.as_str()
    };
    if groups.is_empty() {
        if planner.mir_fusion_signal_count == 0 && planner.mir_fusion_candidate_group_count == 0 {
            return None;
        }
        if !candidate_groups.is_empty() {
            let mut nodes = Vec::new();
            let mut edges = Vec::new();
            let mut shaders = Vec::new();
            let mut decisions = Vec::new();
            append_semantic_candidate_artifacts(
                candidate_groups,
                "bytecode-groups=0",
                accel_graph_state,
                &mut nodes,
                &mut edges,
                &mut shaders,
                &mut decisions,
            );
            append_semantic_window_artifacts(
                instruction_windows,
                "bytecode-groups=0",
                accel_graph_state,
                &mut nodes,
                &mut edges,
                &mut shaders,
                &mut decisions,
            );

            return Some(FusionPlanSnapshot {
                nodes,
                edges,
                shaders,
                decisions,
                planner,
            });
        }
        return Some(FusionPlanSnapshot {
            nodes: Vec::new(),
            edges: Vec::new(),
            shaders: Vec::new(),
            decisions: vec![FusionPlanDecision {
                node_id: "fusion-candidate-summary".to_string(),
                fused: false,
                reason: Some(format!(
                    "mir-signals={} mir-candidate-groups={} semantic-windows={} bytecode-groups=0 accel-graph={}",
                    planner.mir_fusion_signal_count,
                    planner.mir_fusion_candidate_group_count,
                    planner.mir_semantic_instruction_window_count,
                    accel_graph_state
                )),
                thresholds: None,
            }],
            planner,
        });
    }
    let mut nodes = Vec::with_capacity(groups.len());
    let mut edges = Vec::new();
    let mut shaders = Vec::with_capacity(groups.len());
    let mut decisions = Vec::with_capacity(groups.len());
    let gate_open = planner.mir_fusion_candidate_group_count > 0;

    for (index, group) in groups.iter().enumerate() {
        let node_id = format!("group-{}", group.id);
        nodes.push(FusionPlanNode {
            id: node_id.clone(),
            kind: format!("{:?}", group.kind),
            label: format!(
                "{:?} span=[{}..{}] nodes={}",
                group.kind,
                group.span.start,
                group.span.end,
                group.nodes.len()
            ),
            shape: shape_info(&group.shape),
            residency: Some("gpu".to_string()),
        });
        shaders.push(FusionPlanShader {
            name: node_id.clone(),
            stage: format!("{:?}", group.kind),
            workgroup_size: None,
            source_hash: group.pattern.as_ref().map(|p| format!("{:?}", p)),
        });
        decisions.push(FusionPlanDecision {
            node_id: node_id.clone(),
            fused: gate_open,
            reason: Some(if gate_open {
                format!(
                    "kernel={:?} span=[{}..{}]",
                    group.kind, group.span.start, group.span.end
                )
            } else {
                format!(
                    "kernel={:?} span=[{}..{}] fusion-candidate-groups=0 (bytecode compatibility artifact)",
                    group.kind, group.span.start, group.span.end
                )
            }),
            thresholds: None,
        });
        if let Some(next) = groups.get(index + 1) {
            edges.push(FusionPlanEdge {
                from: node_id,
                to: format!("group-{}", next.id),
                reason: Some("program-order".to_string()),
            });
        }
    }

    append_semantic_candidate_artifacts(
        candidate_groups,
        &format!("bytecode-groups={}", groups.len()),
        accel_graph_state,
        &mut nodes,
        &mut edges,
        &mut shaders,
        &mut decisions,
    );
    append_semantic_window_artifacts(
        instruction_windows,
        &format!("bytecode-groups={}", groups.len()),
        accel_graph_state,
        &mut nodes,
        &mut edges,
        &mut shaders,
        &mut decisions,
    );

    Some(FusionPlanSnapshot {
        nodes,
        edges,
        shaders,
        decisions,
        planner,
    })
}

fn shape_info(shape: &ShapeInfo) -> Vec<usize> {
    match shape {
        ShapeInfo::Unknown => Vec::new(),
        ShapeInfo::Scalar => vec![1, 1],
        ShapeInfo::Tensor(dims) => dims.iter().map(|d| d.unwrap_or(0)).collect::<Vec<usize>>(),
    }
}

fn append_semantic_candidate_artifacts(
    candidate_groups: &[runmat_vm::FusionCandidateGroup],
    bytecode_group_state: &str,
    accel_graph_state: &str,
    nodes: &mut Vec<FusionPlanNode>,
    edges: &mut Vec<FusionPlanEdge>,
    shaders: &mut Vec<FusionPlanShader>,
    decisions: &mut Vec<FusionPlanDecision>,
) {
    for (index, group) in candidate_groups.iter().enumerate() {
        let node_id = format!("fusion-candidate-{}", group.id);
        nodes.push(FusionPlanNode {
            id: node_id.clone(),
            kind: "FusionCandidate".to_string(),
            label: format!(
                "semantic-run f={:?} b={:?} stmts=[{}..{}] span=[{}..{}] signals={}",
                group.function,
                group.block,
                group.stmt_start,
                group.stmt_end,
                group.source_span.start,
                group.source_span.end,
                group.signal_count
            ),
            shape: Vec::new(),
            residency: None,
        });
        shaders.push(FusionPlanShader {
            name: node_id.clone(),
            stage: "semantic-candidate".to_string(),
            workgroup_size: None,
            source_hash: None,
        });
        decisions.push(FusionPlanDecision {
            node_id: node_id.clone(),
            fused: false,
            reason: Some(format!(
                "semantic-candidate signals={} span=[{}..{}] {} accel-graph={}",
                group.signal_count,
                group.source_span.start,
                group.source_span.end,
                bytecode_group_state,
                accel_graph_state
            )),
            thresholds: None,
        });
        if let Some(next) = candidate_groups.get(index + 1) {
            edges.push(FusionPlanEdge {
                from: node_id,
                to: format!("fusion-candidate-{}", next.id),
                reason: Some("semantic-program-order".to_string()),
            });
        }
    }
}

fn append_semantic_window_artifacts(
    instruction_windows: &[runmat_vm::FusionInstructionWindow],
    bytecode_group_state: &str,
    accel_graph_state: &str,
    nodes: &mut Vec<FusionPlanNode>,
    edges: &mut Vec<FusionPlanEdge>,
    shaders: &mut Vec<FusionPlanShader>,
    decisions: &mut Vec<FusionPlanDecision>,
) {
    for (index, window) in instruction_windows.iter().enumerate() {
        let node_id = format!("semantic-window-{index}");
        nodes.push(FusionPlanNode {
            id: node_id.clone(),
            kind: "FusionWindow".to_string(),
            label: format!(
                "semantic-window kind={:?} span=[{}..{}]",
                window.kind, window.span.start, window.span.end
            ),
            shape: Vec::new(),
            residency: None,
        });
        shaders.push(FusionPlanShader {
            name: node_id.clone(),
            stage: "semantic-window".to_string(),
            workgroup_size: None,
            source_hash: None,
        });
        decisions.push(FusionPlanDecision {
            node_id: node_id.clone(),
            fused: false,
            reason: Some(format!(
                "semantic-window kind={:?} span=[{}..{}] {} accel-graph={}",
                window.kind,
                window.span.start,
                window.span.end,
                bytecode_group_state,
                accel_graph_state
            )),
            thresholds: None,
        });
        if index + 1 < instruction_windows.len() {
            edges.push(FusionPlanEdge {
                from: node_id,
                to: format!("semantic-window-{}", index + 1),
                reason: Some("semantic-window-order".to_string()),
            });
        }
    }
}

#[cfg(test)]
mod tests {
    use super::build_fusion_snapshot;
    use crate::fusion::FusionPlannerMetadata;
    use runmat_accelerate::fusion::{FusionGroup, FusionKind};
    use runmat_accelerate::graph::{InstrSpan, ShapeInfo};

    #[test]
    fn candidate_summary_emits_without_accel_graph() {
        let snapshot = build_fusion_snapshot(
            &[],
            &[],
            &[],
            Some(FusionPlannerMetadata {
                source: "semantic".to_string(),
                accel_graph_state: "missing".to_string(),
                accel_graph_source: "not_materialized".to_string(),
                mir_local_fact_count: 0,
                mir_diagnostic_count: 0,
                mir_fusion_signal_count: 2,
                mir_fusion_candidate_group_count: 1,
                mir_semantic_instruction_window_count: 1,
            }),
        )
        .expect("semantic candidate summary snapshot");

        assert!(snapshot.nodes.is_empty(), "expected no bytecode nodes");
        assert!(
            snapshot
                .decisions
                .iter()
                .any(|decision| decision.node_id == "fusion-candidate-summary"),
            "expected semantic candidate summary decision"
        );
        assert_eq!(
            snapshot.planner.accel_graph_state, "missing",
            "expected missing accel graph planner metadata"
        );
    }

    #[test]
    fn candidate_groups_emit_nodes_without_bytecode_groups() {
        let snapshot = build_fusion_snapshot(
            &[],
            &[runmat_vm::FusionCandidateGroup {
                id: 0,
                signal_count: 3,
                function: runmat_hir::FunctionId(0),
                block: runmat_mir::BasicBlockId(0),
                stmt_start: 1,
                stmt_end: 4,
                source_span: runmat_hir::Span { start: 10, end: 18 },
            }],
            &[runmat_vm::FusionInstructionWindow {
                span: InstrSpan { start: 1, end: 3 },
                kind: runmat_vm::FusionInstructionKind::Elementwise,
            }],
            Some(FusionPlannerMetadata {
                source: "semantic".to_string(),
                accel_graph_state: "missing".to_string(),
                accel_graph_source: "not_materialized".to_string(),
                mir_local_fact_count: 0,
                mir_diagnostic_count: 0,
                mir_fusion_signal_count: 3,
                mir_fusion_candidate_group_count: 1,
                mir_semantic_instruction_window_count: 1,
            }),
        )
        .expect("semantic candidate snapshot");

        assert_eq!(
            snapshot.nodes.len(),
            2,
            "expected semantic candidate and semantic window nodes"
        );
        assert_eq!(snapshot.nodes[0].kind, "FusionCandidate");
        assert!(
            snapshot
                .nodes
                .iter()
                .any(|node| node.kind == "FusionWindow"),
            "expected semantic window node"
        );
        assert!(
            snapshot
                .decisions
                .iter()
                .any(|decision| decision.node_id == "fusion-candidate-0" && !decision.fused),
            "expected semantic candidate decision for fusion-candidate-0"
        );
    }

    #[test]
    fn candidate_groups_emit_nodes_with_bytecode_groups() {
        let snapshot = build_fusion_snapshot(
            &[FusionGroup {
                id: 7,
                kind: FusionKind::ElementwiseChain,
                nodes: vec![0, 1],
                shape: ShapeInfo::Scalar,
                span: InstrSpan { start: 3, end: 9 },
                pattern: None,
                stack_layout: None,
            }],
            &[runmat_vm::FusionCandidateGroup {
                id: 1,
                signal_count: 2,
                function: runmat_hir::FunctionId(1),
                block: runmat_mir::BasicBlockId(2),
                stmt_start: 0,
                stmt_end: 2,
                source_span: runmat_hir::Span { start: 20, end: 30 },
            }],
            &[runmat_vm::FusionInstructionWindow {
                span: InstrSpan { start: 3, end: 9 },
                kind: runmat_vm::FusionInstructionKind::Elementwise,
            }],
            Some(FusionPlannerMetadata {
                source: "semantic".to_string(),
                accel_graph_state: "missing".to_string(),
                accel_graph_source: "not_materialized".to_string(),
                mir_local_fact_count: 0,
                mir_diagnostic_count: 0,
                mir_fusion_signal_count: 2,
                mir_fusion_candidate_group_count: 1,
                mir_semantic_instruction_window_count: 1,
            }),
        )
        .expect("fusion snapshot");

        assert!(
            snapshot.nodes.iter().any(|node| node.id == "group-7"),
            "expected bytecode fusion node"
        );
        assert!(
            snapshot
                .nodes
                .iter()
                .any(|node| node.id == "fusion-candidate-1"),
            "expected semantic candidate node"
        );
        assert!(
            snapshot
                .nodes
                .iter()
                .any(|node| node.kind == "FusionWindow"),
            "expected semantic window node"
        );
        assert!(
            snapshot
                .nodes
                .iter()
                .any(|node| node.id == "fusion-candidate-1" && node.kind == "FusionCandidate"),
            "expected semantic candidate node kind for fusion-candidate-1"
        );
    }

    #[test]
    fn bytecode_groups_without_semantic_candidates_are_marked_non_fused() {
        let snapshot = build_fusion_snapshot(
            &[FusionGroup {
                id: 3,
                kind: FusionKind::ElementwiseChain,
                nodes: vec![0, 1],
                shape: ShapeInfo::Scalar,
                span: InstrSpan { start: 10, end: 12 },
                pattern: None,
                stack_layout: None,
            }],
            &[],
            &[],
            Some(FusionPlannerMetadata {
                source: "semantic".to_string(),
                accel_graph_state: "missing".to_string(),
                accel_graph_source: "not_materialized".to_string(),
                mir_local_fact_count: 0,
                mir_diagnostic_count: 0,
                mir_fusion_signal_count: 2,
                mir_fusion_candidate_group_count: 0,
                mir_semantic_instruction_window_count: 0,
            }),
        )
        .expect("snapshot");

        let decision = snapshot
            .decisions
            .iter()
            .find(|decision| decision.node_id == "group-3")
            .expect("bytecode group decision");
        assert!(
            !decision.fused,
            "expected bytecode-only group to be marked non-fused when semantic candidates are absent"
        );
        assert_eq!(
            snapshot.planner.mir_fusion_candidate_group_count, 0,
            "expected fusion-candidate-group metadata to reflect semantic gating"
        );
    }
}
