//! Cross-function invariant verification — fails at `build_ir()` time, which
//! runs at the codegen subprocess (`cargo run --bin export-ir`). All
//! graph-level inconsistencies surface before any client artifact is emitted.

use crate::ir::{AffectTarget, NamedType, StructField, TypeShape};
use crate::registry::{lookup_context, CONTEXTS, FUNCTIONS, TYPES};

/// Walk the registered types and find the named type's shape. Used by both
/// graph-check and runtime merge resolution.
pub(crate) fn resolve_type_shape(name: &str) -> Option<NamedType> {
    for entry in TYPES {
        if entry.name == name {
            return Some((entry.shape_fn)());
        }
    }
    None
}

/// Merge-compatibility on named types. A mutation return `value` can
/// splice into a context slot `slot` when any of three shapes hold —
/// matches Python's `types_match_for_merge`:
///   * direct: `slot` shape equals `value` shape → replace
///   * upsert: `slot` is `list[T]`, `value` is `T` → upsert by id
///   * list-replace: `slot` is `list[T]`, `value` is `list[T]`
///
/// The first argument is the slot (context member's output type); the
/// second is the value (mutation's output type).
pub(crate) fn types_match(slot: &NamedType, value: &NamedType) -> bool {
    if named_shapes_equal(slot, value) {
        return true;
    }
    // Upsert: slot is `Alias(List(T))`, value is `T`-shaped.
    if let NamedType::Alias(TypeShape::List(elem)) = slot {
        if shape_matches_named(elem, value) {
            return true;
        }
    }
    false
}

fn named_shapes_equal(a: &NamedType, b: &NamedType) -> bool {
    match (a, b) {
        (NamedType::Struct(fa), NamedType::Struct(fb)) => fields_match(fa, fb),
        (NamedType::Alias(sa), NamedType::Alias(sb)) => shapes_match(sa, sb),
        (NamedType::Enum(va), NamedType::Enum(vb)) => va == vb,
        _ => false,
    }
}

/// True when a `TypeShape` (the slot's list-element) describes the same
/// shape as a `NamedType` (the mutation's full output).
fn shape_matches_named(shape: &TypeShape, named: &NamedType) -> bool {
    match shape {
        TypeShape::Ref(name) => {
            if let Some(referenced) = resolve_type_shape(name) {
                named_shapes_equal(&referenced, named)
            } else {
                false
            }
        }
        _ => false,
    }
}

fn fields_match(a: &[StructField], b: &[StructField]) -> bool {
    if a.len() != b.len() {
        return false;
    }
    a.iter().zip(b.iter()).all(|(fa, fb)| {
        fa.name == fb.name && fa.required == fb.required && shapes_match(&fa.shape, &fb.shape)
    })
}

fn shapes_match(a: &TypeShape, b: &TypeShape) -> bool {
    match (a, b) {
        (TypeShape::Primitive(pa), TypeShape::Primitive(pb)) => {
            std::mem::discriminant(pa) == std::mem::discriminant(pb)
        }
        (TypeShape::Ref(na), TypeShape::Ref(nb)) => {
            // Refs match iff the named types they reference match.
            match (resolve_type_shape(na), resolve_type_shape(nb)) {
                (Some(ta), Some(tb)) => types_match(&ta, &tb),
                _ => na == nb,
            }
        }
        (TypeShape::List(ia), TypeShape::List(ib)) => shapes_match(ia, ib),
        (TypeShape::Optional(ia), TypeShape::Optional(ib)) => shapes_match(ia, ib),
        (TypeShape::Enum(va), TypeShape::Enum(vb)) => va == vb,
        (TypeShape::Union(ba), TypeShape::Union(bb)) => {
            ba.len() == bb.len() && ba.iter().zip(bb.iter()).all(|(x, y)| shapes_match(x, y))
        }
        _ => false,
    }
}

/// Panic with a structured message if the registered function graph is
/// inconsistent. Called from `build_ir()`.
pub fn verify_invariants() {
    check_affects_targets();
    check_merge_targets();
    check_shared_param_types();
}

fn check_affects_targets() {
    for fn_spec in FUNCTIONS {
        for affect in fn_spec.affects() {
            if let AffectTarget::Context(name) = affect {
                if lookup_context(name).is_none() {
                    panic!(
                        "Mizan graph-check: function `{}` declares `affects = \"{}\"` but no context with that name is registered. \
                         Either register a context with that name (via `#[mizan::context(\"{}\")]`) or remove the affects target.",
                        fn_spec.name(),
                        name,
                        name,
                    );
                }
            }
        }
    }
}

fn check_merge_targets() {
    for fn_spec in FUNCTIONS {
        for merge_target in fn_spec.merge() {
            let ctx_entry = match lookup_context(merge_target) {
                Some(c) => c,
                None => panic!(
                    "Mizan graph-check: function `{}` declares `merge = \"{}\"` but no context with that name is registered.",
                    fn_spec.name(),
                    merge_target,
                ),
            };

            let mutation_output = fn_spec.output_type();
            let mutation_shape = match resolve_type_shape(mutation_output) {
                Some(s) => s,
                None => panic!(
                    "Mizan graph-check: function `{}` has output type `{}` but no such named type is registered.",
                    fn_spec.name(), mutation_output,
                ),
            };
            let mut matches: Vec<&'static str> = Vec::new();
            for candidate in FUNCTIONS {
                if candidate.context() != Some(ctx_entry.name) {
                    continue;
                }
                if let Some(candidate_shape) = resolve_type_shape(candidate.output_type()) {
                    if types_match(&candidate_shape, &mutation_shape) {
                        matches.push(candidate.name());
                    }
                }
            }

            if matches.is_empty() {
                panic!(
                    "Mizan graph-check: function `{}` declares `merge = \"{}\"` but no member of that context has output type `{}`. \
                     Add a context member returning `{}`, or remove the merge declaration in favor of `affects` for plain refetch.",
                    fn_spec.name(), merge_target, mutation_output, mutation_output,
                );
            }
            if matches.len() > 1 {
                panic!(
                    "Mizan graph-check: function `{}` declares `merge = \"{}\"` but multiple members ({}) share output type `{}`. \
                     Merge resolution requires exactly one match. Distinguish the outputs or use `affects` for refetch.",
                    fn_spec.name(), merge_target, matches.join(", "), mutation_output,
                );
            }
        }
    }
}

fn check_shared_param_types() {
    for ctx in CONTEXTS {
        let mut by_name: std::collections::HashMap<&'static str, (crate::ir::Primitive, &'static str)>
            = std::collections::HashMap::new();
        for fn_spec in FUNCTIONS {
            if fn_spec.context() != Some(ctx.name) {
                continue;
            }
            for p in fn_spec.input_params() {
                if let Some((prev_primitive, prev_fn)) = by_name.get(p.name) {
                    if std::mem::discriminant(prev_primitive)
                        != std::mem::discriminant(&p.primitive)
                    {
                        panic!(
                            "Mizan graph-check: context `{}` has a parameter `{}` whose type diverges across members. \
                             Function `{}` declares it as `{}`, function `{}` declares it as `{}`. \
                             Shared params must have one type across the whole context.",
                            ctx.name, p.name,
                            prev_fn, prev_primitive.name(),
                            fn_spec.name(), p.primitive.name(),
                        );
                    }
                } else {
                    by_name.insert(p.name, (p.primitive, fn_spec.name()));
                }
            }
        }
    }
}
