use oxc_ast::AstKind;
use oxc_diagnostics::OxcDiagnostic;
use oxc_macros::declare_oxc_lint;
use oxc_semantic::Semantic;
use oxc_span::{GetSpan, Span};
use schemars::JsonSchema;
use serde::Deserialize;
use serde_json::Value;

use crate::{
    AstNode,
    ast_util::{is_function_node, iter_outer_expressions},
    context::LintContext,
    rule::{DefaultRuleConfig, Rule},
};

fn max_nested_callbacks_diagnostic(num: u32, max: u32, span: Span) -> OxcDiagnostic {
    OxcDiagnostic::warn(format!("Too many nested callbacks ({num}). Maximum allowed is {max}."))
        .with_help("Reduce nesting with promises or refactoring your code.")
        .with_label(span)
}

#[derive(Debug, Clone, JsonSchema, Deserialize)]
#[serde(rename_all = "camelCase", default, deny_unknown_fields)]
pub struct MaxNestedCallbacks {
    /// The `max` enforces a maximum depth that callbacks can be nested.
    max: u32,
}

const DEFAULT_MAX_NESTED_CALLBACKS: u32 = 10;

impl Default for MaxNestedCallbacks {
    fn default() -> Self {
        Self { max: DEFAULT_MAX_NESTED_CALLBACKS }
    }
}

#[derive(Debug, JsonSchema, Deserialize)]
#[serde(untagged)]
#[expect(unused)]
enum MaxNestedCallbacksConfigEnum {
    Number(u32),
    Object(MaxNestedCallbacks),
}

declare_oxc_lint!(
    /// ### What it does
    ///
    /// Enforce a maximum depth that callbacks can be nested. This rule helps to limit
    /// the complexity of callback nesting, ensuring that callbacks do not become too
    /// deeply nested, improving code readability and maintainability.
    ///
    /// ### Why is this bad?
    ///
    /// Many JavaScript libraries use the callback pattern to manage asynchronous
    /// operations. A program of any complexity will most likely need to manage several
    /// asynchronous operations at various levels of concurrency. A common pitfall is
    /// nesting callbacks excessively, making code harder to read and understand.
    ///
    /// ### Examples
    ///
    /// Examples of **incorrect** code for this rule with the `{ "max": 3 }` option:
    /// ```js
    /// foo1(function() {
    ///     foo2(function() {
    ///         foo3(function() {
    ///             foo4(function() {
    ///                 // ...
    ///             });
    ///         });
    ///     });
    /// });
    /// ```
    ///
    /// Examples of **correct** code for this rule with the `{ "max": 3 }` option:
    /// ```js
    /// foo1(handleFoo1);
    ///
    /// function handleFoo1() {
    ///     foo2(handleFoo2);
    /// }
    ///
    /// function handleFoo2() {
    ///     foo3(handleFoo3);
    /// }
    ///
    /// function handleFoo3() {
    ///     foo4(handleFoo4);
    /// }
    ///
    /// function handleFoo4() {
    ///     foo5();
    /// }
    /// ```
    MaxNestedCallbacks,
    eslint,
    pedantic,
    config = MaxNestedCallbacksConfigEnum,
    version = "0.15.12",
    short_description = "Enforce a maximum depth that callbacks can be nested.",
);

impl Rule for MaxNestedCallbacks {
    #[expect(clippy::cast_possible_truncation)] // the length of nested ancestors can't be over u32::MAX, because the source code is already limited by u32::MAX.
    fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
        match node.kind() {
            AstKind::Function(f) if f.is_function_declaration() => {}
            AstKind::Function(f) if f.is_expression() => {}
            AstKind::ArrowFunctionExpression(_) => {}
            _ => return,
        }

        if is_callback(node, ctx) {
            let depth = 1 + ctx
                .semantic()
                .nodes()
                .ancestors(node.id())
                .filter(|node| is_callback(node, ctx))
                .count() as u32;
            if depth > self.max {
                ctx.diagnostic(max_nested_callbacks_diagnostic(depth, self.max, node.span()));
            }
        }
    }

    fn from_configuration(value: serde_json::Value) -> Result<Self, serde_json::error::Error> {
        if let Some(max) = value
            .get(0)
            .and_then(Value::as_number)
            .and_then(serde_json::Number::as_u64)
            .and_then(|v| u32::try_from(v).ok())
        {
            Ok(Self { max })
        } else {
            serde_json::from_value::<DefaultRuleConfig<Self>>(value)
                .map(DefaultRuleConfig::into_inner)
        }
    }
}

fn is_callback<'a>(node: &AstNode<'a>, semantic: &Semantic<'a>) -> bool {
    is_function_node(node)
        && matches!(
            iter_outer_expressions(semantic.nodes(), node.id()).next(),
            Some(AstKind::CallExpression(_))
        )
}

#[test]
fn test() {
    use crate::tester::Tester;

    fn nested_functions(d: usize) -> String {
        ["foo(function() {".repeat(d), "});".repeat(d)].concat()
    }

    let defaults = MaxNestedCallbacks::default();
    assert_eq!(defaults.max, 10);

    let nested_10 = nested_functions(10);
    let nested_11 = nested_functions(11);

    let pass = vec![
        ("foo(function() { bar(thing, function(data) {}); });", Some(serde_json::json!([3]))),
        (
            "var foo = function() {}; bar(function(){ baz(function() { qux(foo); }) });",
            Some(serde_json::json!([2])),
        ),
        ("fn(function(){}, function(){}, function(){});", Some(serde_json::json!([2]))),
        ("fn(() => {}, function(){}, function(){});", Some(serde_json::json!([2]))), // { "ecmaVersion": 6 },
        (&nested_10, Some(serde_json::json!([{}]))),
        (
            "foo(function() { bar(thing, function(data) {}); });",
            Some(serde_json::json!([{ "max": 3 }])),
        ),
    ];

    let fail = vec![
        (
            "foo(function() { bar(thing, function(data) { baz(function() {}); }); });",
            Some(serde_json::json!([2])),
        ),
        (
            "foo(function() { bar(thing, (data) => { baz(function() {}); }); });",
            Some(serde_json::json!([2])),
        ), // { "ecmaVersion": 6 },
        (
            "foo(() => { bar(thing, (data) => { baz( () => {}); }); });",
            Some(serde_json::json!([2])),
        ), // { "ecmaVersion": 6 },
        (
            "foo(function() { if (isTrue) { bar(function(data) { baz(function() {}); }); } });",
            Some(serde_json::json!([2])),
        ),
        (&nested_11, Some(serde_json::json!([{}]))),
        ("foo(function() {})", Some(serde_json::json!([{ "max": 0 }]))),
        (
            "foo(function() { bar(thing, function(data) { baz(function() {}); }); });",
            Some(serde_json::json!([{ "max": 2 }])),
        ),
    ];

    Tester::new(MaxNestedCallbacks::NAME, MaxNestedCallbacks::PLUGIN, pass, fail)
        .test_and_snapshot();
}
