use crate::diag::{Diag, DiagCode};
use crate::ir::DType;

// ── Helpers ──────────────────────────────────────────────────────────────────

fn promotion_hint(lhs: DType, rhs: DType, out: DType) -> String {
    if lhs != out && rhs == out {
        format!("cast the left operand from {lhs} to {out} using `@{out}`")
    } else if rhs != out && lhs == out {
        format!("cast the right operand from {rhs} to {out} using `@{out}`")
    } else {
        format!("cast one operand to {out} using `@{out}`")
    }
}

// ── Diagnostic constructors ─────────────────────────────────────────────────
//
// Each builds a `Diag` with pre-formatted messages so `diag.rs` stays
// independent of IR types.  Span arguments are plain `(usize, usize)` pairs
// — no wrapper type needed.

pub fn lower_undefined_var(name: &str, span: (usize, usize)) -> Diag {
    Diag::new(
        DiagCode::LowerUndefinedVar,
        format!("undefined variable `{name}`"),
        Some(span),
        None,
    )
}

pub fn lower_type_mismatch(op: &str, lhs: DType, rhs: DType, span: (usize, usize)) -> Diag {
    Diag::new(
        DiagCode::LowerTypeMismatch,
        format!("type mismatch in {op}: cannot promote {lhs} and {rhs}"),
        Some(span),
        None,
    )
}

pub fn lower_int_out_of_range(value: i64, dtype: DType, span: (usize, usize)) -> Diag {
    Diag::new(
        DiagCode::LowerIntOutOfRange,
        format!("integer literal `{value}` is out of range for `{dtype}`"),
        Some(span),
        None,
    )
}

pub fn lower_unknown_function(name: &str, span: (usize, usize)) -> Diag {
    Diag::new(
        DiagCode::LowerUnknownFunction,
        format!("undefined function `{name}`"),
        Some(span),
        None,
    )
}

pub fn lower_arg_count(name: &str, expected: usize, got: usize, span: (usize, usize)) -> Diag {
    Diag::new(
        DiagCode::LowerArgCount,
        format!("wrong argument count for `{name}`: expected {expected}, got {got}"),
        Some(span),
        None,
    )
}

pub fn lower_div_by_zero(op: &str, span: (usize, usize)) -> Diag {
    Diag::new(
        DiagCode::LowerDivByZero,
        format!("division by zero in {op}"),
        Some(span),
        None,
    )
}

pub fn lower_tuple_arity(expected: usize, got: usize, span: (usize, usize)) -> Diag {
    Diag::new(
        DiagCode::LowerTupleArity,
        format!("expected {expected} value, but expression produced {got}"),
        Some(span),
        None,
    )
}

pub fn lower_implicit_promotion(
    op: &str,
    lhs: DType,
    rhs: DType,
    out: DType,
    span: (usize, usize),
) -> Diag {
    Diag::new(
        DiagCode::LowerImplicitPromotion,
        format!("implicit promotion in {op}: {lhs} + {rhs} -> {out}"),
        Some(span),
        Some(promotion_hint(lhs, rhs, out)),
    )
}

/// Like [`lower_implicit_promotion`] but spans the full expression (left through right)
/// with per-operand labels.
pub fn lower_implicit_promotion_with_operands(
    op: &str,
    lhs: DType,
    rhs: DType,
    out: DType,
    left_span: (usize, usize),
    right_span: (usize, usize),
) -> Diag {
    let span = (left_span.0, right_span.1);
    let mut diag = Diag::new(
        DiagCode::LowerImplicitPromotion,
        format!("implicit promotion in {op}: {lhs} + {rhs} -> {out}"),
        Some(span),
        Some(promotion_hint(lhs, rhs, out)),
    );
    diag.labels = Some(vec![
        (left_span.0, left_span.1, format!("left operand ({lhs})")),
        (right_span.0, right_span.1, format!("right operand ({rhs})")),
    ]);
    diag
}

pub fn lower_float_precision_loss(value: f64, dtype: DType, span: (usize, usize)) -> Diag {
    let hint = match dtype.bits {
        16 => Some("consider f32 or f64 for full precision".to_string()),
        32 => Some("consider f64 for full precision".to_string()),
        _ => None,
    };
    Diag::new(
        DiagCode::LowerFloatPrecisionLoss,
        format!("literal `{value}` may lose precision in `{dtype}`"),
        Some(span),
        hint,
    )
}

pub fn lower_unsafe_uint_to_sint(
    op: &str,
    lhs: DType,
    rhs: DType,
    out: DType,
    span: (usize, usize),
) -> Diag {
    Diag::new(
        DiagCode::LowerUnsafeUIntToSInt,
        format!("unsafe promotion in {op}: {lhs} and {rhs} promoted to {out}"),
        Some(span),
        Some(
            "u64 values may exceed i64 range; consider casting to f64/c128 or constraining the range"
                .to_string(),
        ),
    )
}

pub fn lower_redundant_cast_chain(from: DType, to: DType, span: (usize, usize)) -> Diag {
    Diag::new(
        DiagCode::LowerRedundantCastChain,
        format!("multiple sequential casts: {from} -> {to}; consider casting directly to {to}"),
        Some(span),
        Some(
            "simplify to a single `expr@dtype` unless the intermediate cast is required"
                .to_string(),
        ),
    )
}

#[cfg(test)]
mod tests {
    use super::{lower_implicit_promotion_with_operands, lower_type_mismatch};
    use crate::diag::Severity;
    use crate::ir::DType;

    #[test]
    fn lower_error_report_contains_message() {
        let err = lower_type_mismatch("Add", DType::BOOL, DType::F64, (2, 7));
        let report = err.report_string("input", "x + y", Severity::Error);
        assert!(report.contains("type mismatch in Add"));
    }

    #[test]
    fn lower_implicit_promotion_with_operands_sets_labels() {
        let diag = lower_implicit_promotion_with_operands(
            "Add",
            DType::I64,
            DType::F16,
            DType::F64,
            (2, 4),
            (7, 9),
        );

        assert_eq!(diag.span, Some((2, 9)));

        let labels = diag.labels.expect("expected operand labels");
        assert_eq!(labels.len(), 2);

        let (l_start, l_end, l_msg) = &labels[0];
        assert_eq!((*l_start, *l_end), (2, 4));
        assert!(l_msg.contains("left operand"));
        assert!(l_msg.contains("i64"));

        let (r_start, r_end, r_msg) = &labels[1];
        assert_eq!((*r_start, *r_end), (7, 9));
        assert!(r_msg.contains("right operand"));
        assert!(r_msg.contains("f16"));

        let hint = diag
            .hint
            .as_ref()
            .expect("expected hint for implicit promotion");
        assert!(hint.contains("@f64"));
        assert!(hint.contains("cast"));
    }
}
