/// Type error reporting — inspired by Elm's famously helpful errors.

use crate::types::Type;

/// A type error with context for helpful error messages.
#[derive(Debug, Clone)]
pub struct TypeError {
    pub kind: TypeErrorKind,
    pub span: Option<(usize, usize)>, // (line, col)
    pub source: Option<String>,       // source code snippet
}

/// The kind of type error.
#[derive(Debug, Clone)]
pub enum TypeErrorKind {
    /// Expected one type but found another.
    Mismatch {
        expected: Type,
        found: Type,
        context: String,
    },

    /// Using a non-reactive value where a Signal/Derived is expected.
    NotReactive {
        found: Type,
        context: String,
    },

    /// An effect was performed but not handled.
    UnhandledEffect {
        effect: String,
        function: String,
    },

    /// A view expression appears outside a `view` block.
    ViewOutsideBlock {
        expr: String,
    },

    /// An unknown variable reference.
    UnboundVariable {
        name: String,
    },

    /// Occurs check failure (infinite type).
    InfiniteType {
        var: String,
        ty: Type,
    },

    /// Wrong number of arguments to a function.
    ArityMismatch {
        function: String,
        expected: usize,
        found: usize,
    },

    /// Accessing a field that doesn't exist on a record.
    MissingField {
        field: String,
        record_type: Type,
    },

    /// A refinement predicate was violated at compile time.
    RefinementViolation {
        type_name: String,
        predicate: String,
        value: String,
    },

    /// Circular type alias definition.
    TypeAliasCycle {
        name: String,
    },

    /// A match expression does not cover all enum variants.
    NonExhaustiveMatch {
        enum_name: String,
        missing_variants: Vec<String>,
    },
}

impl TypeError {
    pub fn new(kind: TypeErrorKind) -> Self {
        TypeError {
            kind,
            span: None,
            source: None,
        }
    }

    pub fn with_span(mut self, line: usize, col: usize) -> Self {
        self.span = Some((line, col));
        self
    }

    pub fn with_source(mut self, source: String) -> Self {
        self.source = Some(source);
        self
    }

    /// Format the error like Elm — helpful, specific, actionable.
    pub fn display(&self) -> String {
        let mut out = String::new();

        // Header
        let (title, body) = match &self.kind {
            TypeErrorKind::Mismatch { expected, found, context } => {
                ("TYPE MISMATCH".to_string(), format!(
                    "I was expecting:\n\n    {}\n\nbut found:\n\n    {}\n\n{}",
                    expected.display(),
                    found.display(),
                    context
                ))
            }
            TypeErrorKind::NotReactive { found, context } => {
                ("NOT REACTIVE".to_string(), format!(
                    "This value has type:\n\n    {}\n\nbut it's used in a context that expects a reactive value (Signal or Derived).\n\n{}",
                    found.display(),
                    context
                ))
            }
            TypeErrorKind::UnhandledEffect { effect, function } => {
                ("UNHANDLED EFFECT".to_string(), format!(
                    "The function `{}` performs the `{}` effect, but no handler is installed.\n\n\
                    Hint: Wrap the call in `handle(() => {}(...), {{ \"{}\": ... }})`",
                    function, effect, function, effect
                ))
            }
            TypeErrorKind::ViewOutsideBlock { expr } => {
                ("VIEW OUTSIDE BLOCK".to_string(), format!(
                    "This UI expression:\n\n    {}\n\nappears outside a `view` block. UI elements can only be created inside views.",
                    expr
                ))
            }
            TypeErrorKind::UnboundVariable { name } => {
                ("UNBOUND VARIABLE".to_string(), format!(
                    "I cannot find a `{}` variable. Did you mean to declare it with `signal` or `let`?",
                    name
                ))
            }
            TypeErrorKind::InfiniteType { var, ty } => {
                ("INFINITE TYPE".to_string(), format!(
                    "Unification would create an infinite type:\n\n    {} ~ {}\n\n\
                    This usually means a recursive definition without a base case.",
                    var, ty.display()
                ))
            }
            TypeErrorKind::ArityMismatch { function, expected, found } => {
                ("WRONG NUMBER OF ARGUMENTS".to_string(), format!(
                    "The function `{}` expects {} argument{} but was given {}.",
                    function,
                    expected,
                    if *expected == 1 { "" } else { "s" },
                    found
                ))
            }
            TypeErrorKind::MissingField { field, record_type } => {
                ("MISSING FIELD".to_string(), format!(
                    "This record:\n\n    {}\n\ndoes not have a field called `{}`.",
                    record_type.display(),
                    field
                ))
            }
            TypeErrorKind::RefinementViolation { type_name, predicate, value } => {
                ("REFINEMENT VIOLATED".to_string(), format!(
                    "The value `{}` does not satisfy the refinement type `{}`.\n\n\
                     The predicate `{}` is not satisfied.\n\n\
                     Hint: Ensure the value meets the constraint before assignment.",
                    value, type_name, predicate
                ))
            }
            TypeErrorKind::TypeAliasCycle { name } => {
                ("TYPE ALIAS CYCLE".to_string(), format!(
                    "The type alias `{}` refers to itself, creating an infinite loop.\n\n\
                     Break the cycle by using a concrete base type.",
                    name
                ))
            }
            TypeErrorKind::NonExhaustiveMatch { enum_name, missing_variants } => {
                ("NON-EXHAUSTIVE MATCH".to_string(), format!(
                    "This `match` on `{}` does not cover all variants.\n\n\
                     Missing: {}\n\n\
                     Hint: Add the missing arms, or use `_` as a wildcard pattern.",
                    enum_name,
                    missing_variants.join(", ")
                ))
            }
        };

        // Format like Elm
        out.push_str("── ");
        out.push_str(&title);
        out.push_str(" ");
        out.push_str(&"─".repeat(60 - title.len() - 3));
        out.push('\n');

        if let Some((line, col)) = self.span {
            out.push_str(&format!("{}:{}\n", line, col));
        }

        if let Some(source) = &self.source {
            out.push('\n');
            out.push_str("    ");
            out.push_str(source);
            out.push_str("\n\n");
        }

        out.push_str(&body);
        out.push('\n');

        out
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_mismatch_error() {
        let err = TypeError::new(TypeErrorKind::Mismatch {
            expected: Type::Int,
            found: Type::String,
            context: "in the expression `count + name`".to_string(),
        })
        .with_span(5, 12)
        .with_source("count + name".to_string());

        let msg = err.display();
        assert!(msg.contains("TYPE MISMATCH"));
        assert!(msg.contains("Int"));
        assert!(msg.contains("String"));
        assert!(msg.contains("5:12"));
    }

    #[test]
    fn test_unhandled_effect_error() {
        let err = TypeError::new(TypeErrorKind::UnhandledEffect {
            effect: "Http".to_string(),
            function: "fetchUser".to_string(),
        });

        let msg = err.display();
        assert!(msg.contains("UNHANDLED EFFECT"));
        assert!(msg.contains("fetchUser"));
        assert!(msg.contains("Http"));
    }

    #[test]
    fn test_view_outside_block() {
        let err = TypeError::new(TypeErrorKind::ViewOutsideBlock {
            expr: "button { \"click me\" }".to_string(),
        });

        let msg = err.display();
        assert!(msg.contains("VIEW OUTSIDE BLOCK"));
        assert!(msg.contains("view"));
    }
}
