use oxc_allocator::ArenaVec;
use oxc_ast::{
    AstKind,
    ast::{
        ChainElement, Expression, FormalParameter, TSLiteral, TSType, TSTypeAnnotation, TSTypeName,
        UnaryOperator,
    },
};
use oxc_diagnostics::OxcDiagnostic;
use oxc_macros::declare_oxc_lint;
use oxc_span::{GetSpan, Span};
use schemars::JsonSchema;
use serde::Deserialize;

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

fn no_inferrable_types_diagnostic(span: Span) -> OxcDiagnostic {
    OxcDiagnostic::warn("Type can be trivially inferred from the initializer")
        .with_help("Remove the type annotation")
        .with_label(span)
}

#[derive(Debug, Default, Clone, JsonSchema, Deserialize)]
#[serde(rename_all = "camelCase", default, deny_unknown_fields)]
pub struct NoInferrableTypes {
    /// When set to `true`, ignores type annotations on function parameters.
    ignore_parameters: bool,
    /// When set to `true`, ignores type annotations on class properties.
    ignore_properties: bool,
}

declare_oxc_lint!(
    /// ### What it does
    ///
    /// Disallow explicit type declarations for variables or parameters initialized to a number, string, or boolean.
    ///
    /// ### Why is this bad?
    ///
    /// Explicitly typing variables or parameters that are initialized to a literal value is unnecessary because TypeScript can infer the type from the value.
    ///
    /// ### Examples
    ///
    /// Examples of **incorrect** code for this rule:
    /// ```ts
    /// const a: number = 5;
    /// const b: string = 'foo';
    /// const c: boolean = true;
    /// const fn = (a: number = 5, b: boolean = true, c: string = 'foo') => {};
    /// ```
    ///
    /// Examples of **correct** code for this rule:
    /// ```ts
    /// const a = 5;
    /// const b = 'foo';
    /// const c = true;
    /// const fn = (a = 5, b = true, c = 'foo') => {};
    /// ```
    NoInferrableTypes,
    typescript,
    style,
    suggestion,
    config = NoInferrableTypes,
    version = "0.14.0",
    short_description = "Disallow explicit type declarations for variables or parameters initialized to a number, string, or boolean.",
);

impl Rule for NoInferrableTypes {
    fn from_configuration(value: serde_json::Value) -> Result<Self, serde_json::error::Error> {
        serde_json::from_value::<DefaultRuleConfig<Self>>(value).map(DefaultRuleConfig::into_inner)
    }

    fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
        match node.kind() {
            AstKind::VariableDeclarator(variable_decl) => {
                if let (Some(init), Some(type_annotation)) =
                    (&variable_decl.init, &variable_decl.type_annotation)
                    && is_inferrable_type(type_annotation, init)
                {
                    let delete_span =
                        get_delete_span(ctx, type_annotation.span(), variable_decl.definite, false);
                    ctx.diagnostic_with_suggestion(
                        no_inferrable_types_diagnostic(type_annotation.span()),
                        |fixer| fixer.delete_range(delete_span),
                    );
                }
            }
            AstKind::Function(function) => {
                self.check_formal_parameters(&function.params.items, ctx);
            }
            AstKind::ArrowFunctionExpression(arrow_function_expression) => {
                self.check_formal_parameters(&arrow_function_expression.params.items, ctx);
            }
            AstKind::PropertyDefinition(property_definition) => {
                // We ignore `readonly` because of Microsoft/TypeScript#14416
                // Essentially a readonly property without a type
                // will result in its value being the type, leading to
                // compile errors if the type is stripped.
                if self.ignore_properties
                    || property_definition.readonly
                    || property_definition.optional
                {
                    return;
                }
                if let (Some(init), Some(type_annotation)) =
                    (&property_definition.value, &property_definition.type_annotation)
                    && is_inferrable_type(type_annotation, init)
                {
                    let delete_span = get_delete_span(
                        ctx,
                        type_annotation.span(),
                        property_definition.definite,
                        false,
                    );
                    ctx.diagnostic_with_suggestion(
                        no_inferrable_types_diagnostic(type_annotation.span()),
                        |fixer| fixer.delete_range(delete_span),
                    );
                }
            }
            AstKind::AccessorProperty(accessor_property) => {
                if self.ignore_properties {
                    return;
                }
                if let (Some(init), Some(type_annotation)) =
                    (&accessor_property.value, &accessor_property.type_annotation)
                    && is_inferrable_type(type_annotation, init)
                {
                    let delete_span = get_delete_span(
                        ctx,
                        type_annotation.span(),
                        accessor_property.definite,
                        false,
                    );
                    ctx.diagnostic_with_suggestion(
                        no_inferrable_types_diagnostic(type_annotation.span()),
                        |fixer| fixer.delete_range(delete_span),
                    );
                }
            }
            _ => {}
        }
    }
}

impl NoInferrableTypes {
    fn check_formal_parameters<'a>(
        &self,
        params: &ArenaVec<'a, FormalParameter<'a>>,
        ctx: &LintContext<'a>,
    ) {
        if self.ignore_parameters {
            return;
        }

        for param in params {
            if let Some(init) = &param.initializer
                && let Some(type_annotation) = &param.type_annotation
                && is_inferrable_type(type_annotation, init)
            {
                let delete_span =
                    get_delete_span(ctx, type_annotation.span(), false, param.optional);
                ctx.diagnostic_with_suggestion(
                    no_inferrable_types_diagnostic(type_annotation.span()),
                    |fixer| fixer.delete_range(delete_span),
                );
            }
        }
    }
}

/// Get the span to delete, including any `!` (definite) or `?` (optional) before the type annotation.
fn get_delete_span(
    ctx: &LintContext<'_>,
    type_annotation_span: Span,
    definite: bool,
    optional: bool,
) -> Span {
    if definite || optional {
        // Check if there's a `!` or `?` before the `:` in the type annotation
        let start = type_annotation_span.start;
        if start > 0 {
            let source_bytes = ctx.source_text().as_bytes();
            let prev_char = source_bytes[(start - 1) as usize];
            if (definite && prev_char == b'!') || (optional && prev_char == b'?') {
                return Span::new(start - 1, type_annotation_span.end);
            }
        }
    }
    type_annotation_span
}

fn is_inferrable_type(type_annotation: &TSTypeAnnotation, init: &Expression) -> bool {
    match &type_annotation.type_annotation {
        TSType::TSLiteralType(ts_literal_type) => match &ts_literal_type.literal {
            TSLiteral::BooleanLiteral(_) => is_init_boolean(init),
            TSLiteral::NumericLiteral(_) => is_init_number(init),
            TSLiteral::BigIntLiteral(_) => is_init_bigint(init),
            TSLiteral::StringLiteral(_) => is_init_string(init),
            TSLiteral::TemplateLiteral(_) | TSLiteral::UnaryExpression(_) => false,
        },
        TSType::TSStringKeyword(_) => is_init_string(init),
        TSType::TSBigIntKeyword(_) => is_init_bigint(init),
        TSType::TSBooleanKeyword(_) => is_init_boolean(init),
        TSType::TSNumberKeyword(_) => is_init_number(init),
        TSType::TSNullKeyword(_) => is_init_null(init),
        TSType::TSSymbolKeyword(_) => {
            if is_chain_call_expression_with_name(init, "Symbol") {
                return true;
            }
            if let Expression::CallExpression(call_expr) = init.get_inner_expression() {
                call_expr.callee.get_identifier_reference().is_some_and(|id| id.name == "Symbol")
            } else {
                false
            }
        }
        TSType::TSTypeReference(type_reference) => {
            if let TSTypeName::IdentifierReference(ident) = &type_reference.type_name
                && ident.name == "RegExp"
            {
                return is_init_regexp(init);
            }

            false
        }
        TSType::TSUndefinedKeyword(_) => match init.get_inner_expression() {
            Expression::Identifier(id) => id.name == "undefined",
            Expression::UnaryExpression(unary_expr) => {
                matches!(unary_expr.operator, UnaryOperator::Void)
            }
            _ => false,
        },
        _ => false,
    }
}

fn is_chain_call_expression_with_name(init: &Expression, name: &str) -> bool {
    if let Expression::ChainExpression(chain_expr) = init
        && let ChainElement::CallExpression(call_expr) = &chain_expr.expression
    {
        return call_expr.callee.get_identifier_reference().is_some_and(|id| id.name == name);
    }
    false
}

fn is_init_bigint(init: &Expression) -> bool {
    let init = {
        let init = init.get_inner_expression();
        if let Expression::UnaryExpression(unary_expr) = init {
            if matches!(
                unary_expr.operator,
                UnaryOperator::UnaryPlus | UnaryOperator::UnaryNegation
            ) {
                unary_expr.argument.get_inner_expression()
            } else {
                init
            }
        } else {
            init
        }
    };

    if is_chain_call_expression_with_name(init, "BigInt") {
        return true;
    }

    match init {
        Expression::CallExpression(call_expr) => {
            call_expr.callee.get_identifier_reference().is_some_and(|id| id.name == "BigInt")
        }
        Expression::BigIntLiteral(_) => true,
        _ => false,
    }
}

fn is_init_boolean(init: &Expression) -> bool {
    if is_chain_call_expression_with_name(init, "Boolean") {
        return true;
    }
    match init.get_inner_expression() {
        Expression::UnaryExpression(unary_expr) => {
            matches!(unary_expr.operator, UnaryOperator::LogicalNot)
        }
        Expression::CallExpression(call_expr) => {
            call_expr.callee.get_identifier_reference().is_some_and(|id| id.name == "Boolean")
        }
        Expression::BooleanLiteral(_) => true,
        _ => false,
    }
}

fn is_init_null(init: &Expression) -> bool {
    let init = init.get_inner_expression();
    matches!(init, Expression::NullLiteral(_))
}

fn is_init_number(init: &Expression) -> bool {
    let init = {
        let init = init.get_inner_expression();
        if let Expression::UnaryExpression(unary_expr) = init {
            if matches!(
                unary_expr.operator,
                UnaryOperator::UnaryPlus | UnaryOperator::UnaryNegation
            ) {
                unary_expr.argument.get_inner_expression()
            } else {
                init
            }
        } else {
            init
        }
    };
    if is_chain_call_expression_with_name(init, "Number") {
        return true;
    }
    match init {
        Expression::Identifier(id) => id.name == "Infinity" || id.name == "NaN",
        Expression::CallExpression(call_expr) => {
            call_expr.callee.get_identifier_reference().is_some_and(|id| id.name == "Number")
        }
        Expression::NumericLiteral(_) => true,
        _ => false,
    }
}
fn is_init_string(init: &Expression) -> bool {
    if is_chain_call_expression_with_name(init, "String") {
        return true;
    }
    match init {
        Expression::CallExpression(call_expr) => {
            call_expr.callee.get_identifier_reference().is_some_and(|id| id.name == "String")
        }
        Expression::StringLiteral(_) | Expression::TemplateLiteral(_) => true,
        _ => false,
    }
}
fn is_init_regexp(init: &Expression) -> bool {
    if is_chain_call_expression_with_name(init, "RegExp") {
        return true;
    }
    match init.get_inner_expression() {
        Expression::RegExpLiteral(_) => true,
        Expression::NewExpression(new_expr) => {
            if let Expression::Identifier(id) = new_expr.callee.get_inner_expression() {
                id.name == "RegExp"
            } else {
                false
            }
        }
        Expression::CallExpression(call_expr) => {
            if let Expression::Identifier(id) = call_expr.callee.get_inner_expression() {
                id.name == "RegExp"
            } else {
                false
            }
        }
        _ => false,
    }
}

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

    let pass = vec![
        ("const a = 10n;", None),
        ("const a = -10n;", None),
        ("const a = BigInt(10);", None),
        ("const a = -BigInt(10);", None),
        ("const a = BigInt?.(10);", None),
        ("const a = -BigInt?.(10);", None),
        ("const a = false;", None),
        ("const a = true;", None),
        ("const a = Boolean(null);", None),
        ("const a = Boolean?.(null);", None),
        ("const a = !0;", None),
        ("const a = 10;", None),
        ("const a = +10;", None),
        ("const a = -10;", None),
        ("const a = Number('1');", None),
        ("const a = +Number('1');", None),
        ("const a = -Number('1');", None),
        ("const a = Number?.('1');", None),
        ("const a = +Number?.('1');", None),
        ("const a = -Number?.('1');", None),
        ("const a = Infinity;", None),
        ("const a = +Infinity;", None),
        ("const a = -Infinity;", None),
        ("const a = NaN;", None),
        ("const a = +NaN;", None),
        ("const a = -NaN;", None),
        ("const a = null;", None),
        ("const a = /a/;", None),
        ("const a = RegExp('a');", None),
        ("const a = RegExp?.('a');", None),
        ("const a = new RegExp('a');", None),
        (r#"const a = "str";"#, None), // Single and double-quotes both work.
        ("const a = 'str';", None),
        ("const a = `str`;", None),
        ("const a = String(1);", None),
        ("const a = String?.(1);", None),
        ("const a = Symbol('a');", None),
        ("const a = Symbol?.('a');", None),
        ("const a = undefined;", None),
        ("const a = void someValue;", None),
        ("const fn = (a = 5, b = true, c = 'foo') => {};", None),
        ("const fn = function (a = 5, b = true, c = 'foo') {};", None),
        ("function fn(a = 5, b = true, c = 'foo') {}", None),
        ("function fn(a: number, b: boolean, c: string) {}", None),
        (
            "
            class Foo {
              a = 5;
              b = true;
              c = 'foo';
            }
                ",
            None,
        ),
        ("class Foo { readonly a: number = 5; }", None),
        (
            "
            class Foo {
              accessor a = 5;
            }
                ",
            None,
        ),
        ("const a: any = 5;", None),
        ("const fn = function (a: any = 5, b: any = true, c: any = 'foo') {};", None),
        (
            "const fn = (a: number = 5, b: boolean = true, c: string = 'foo') => {};",
            Some(serde_json::json!([{ "ignoreParameters": true }])),
        ),
        (
            "function fn(a: number = 5, b: boolean = true, c: string = 'foo') {}",
            Some(serde_json::json!([{ "ignoreParameters": true }])),
        ),
        (
            "const fn = function (a: number = 5, b: boolean = true, c: string = 'foo') {};",
            Some(serde_json::json!([{ "ignoreParameters": true }])),
        ),
        (
            "
            class Foo {
              a: number = 5;
              b: boolean = true;
              c: string = 'foo';
            }
                  ",
            Some(serde_json::json!([{ "ignoreProperties": true }])),
        ),
        (
            "
            class Foo {
              accessor a: number = 5;
            }
                  ",
            Some(serde_json::json!([{ "ignoreProperties": true }])),
        ),
        (
            "
            class Foo {
              a?: number = 5;
              b?: boolean = true;
              c?: string = 'foo';
            }
                  ",
            None,
        ),
        ("class Foo { constructor(public a = true) {} }", None),
    ];

    let fail = vec![
        ("const a: bigint = 10n;", None),
        ("const a: bigint = -10n;", None),
        ("const a: bigint = BigInt(10);", None),
        ("const a: bigint = -BigInt(10);", None),
        ("const a: bigint = BigInt?.(10);", None),
        ("const a: bigint = -BigInt?.(10);", None),
        ("const a: boolean = false;", None),
        ("const a: boolean = true;", None),
        ("const a: boolean = Boolean(null);", None),
        ("const a: boolean = Boolean?.(null);", None),
        ("const a: boolean = !0;", None),
        ("const a: number = 10;", None),
        ("const a: number = +10;", None),
        ("const a: number = -10;", None),
        ("const a: number = Number('1');", None),
        ("const a: number = +Number('1');", None),
        ("const a: number = -Number('1');", None),
        ("const a: number = Number?.('1');", None),
        ("const a: number = +Number?.('1');", None),
        ("const a: number = -Number?.('1');", None),
        ("const a: number = Infinity;", None),
        ("const a: number = +Infinity;", None),
        ("const a: number = -Infinity;", None),
        ("const a: number = NaN;", None),
        ("const a: number = +NaN;", None),
        ("const a: number = -NaN;", None),
        ("const a: null = null;", None),
        ("const a: RegExp = /a/;", None),
        ("const a: RegExp = RegExp('a');", None),
        ("const a: RegExp = RegExp?.('a');", None),
        ("const a: RegExp = new RegExp('a');", None),
        (r#"const a: string = "str";"#, None), // This one exists to ensure single and double quotes both work.
        ("const a: string = 'str';", None),
        ("const a: string = `str`;", None),
        ("const a: string = String(1);", None),
        ("const a: string = String?.(1);", None),
        ("const a: symbol = Symbol('a');", None),
        ("const a: symbol = Symbol?.('a');", None),
        ("const a: undefined = undefined;", None),
        ("const a: undefined = void someValue;", None),
        (
            "const fn = (a?: number = 5) => {};",
            Some(serde_json::json!([ { "ignoreParameters": false, }, ])),
        ),
        (
            "
            class A {
              a: number = 1;
            }
                  ",
            Some(serde_json::json!([ { "ignoreProperties": false, }, ])),
        ),
        (
            "const fn = (a: number = 5, b: boolean = true, c: string = 'foo') => {};",
            Some(serde_json::json!([ { "ignoreParameters": false, "ignoreProperties": false, }, ])),
        ),
        (
            "
            class Foo {
              a: number = 5;
              b: boolean = true;
              c: string = 'foo';
            }",
            Some(serde_json::json!([ { "ignoreParameters": false, "ignoreProperties": false, }, ])),
        ),
        (
            "
            class Foo {
              constructor(public a: boolean = true) {}
            }
                  ",
            Some(serde_json::json!([ { "ignoreParameters": false, "ignoreProperties": false, }, ])),
        ),
        (
            "
            class Foo {
              accessor a: number = 5;
            }
                  ",
            None,
        ),
    ];

    let fix = vec![
        ("const a: bigint = 10n;", "const a = 10n;", None),
        ("const a: bigint = -10n;", "const a = -10n;", None),
        ("const a: bigint = BigInt(10);", "const a = BigInt(10);", None),
        ("const a: bigint = -BigInt(10);", "const a = -BigInt(10);", None),
        ("const a: bigint = BigInt?.(10);", "const a = BigInt?.(10);", None),
        ("const a: bigint = -BigInt?.(10);", "const a = -BigInt?.(10);", None),
        ("const a: boolean = false;", "const a = false;", None),
        ("const a: boolean = true;", "const a = true;", None),
        ("const a: boolean = Boolean(null);", "const a = Boolean(null);", None),
        ("const a: boolean = Boolean?.(null);", "const a = Boolean?.(null);", None),
        ("const a: boolean = !0;", "const a = !0;", None),
        ("const a: number = 10;", "const a = 10;", None),
        ("const a: number = +10;", "const a = +10;", None),
        ("const a: number = -10;", "const a = -10;", None),
        ("const a: number = Number('1');", "const a = Number('1');", None),
        ("const a: number = +Number('1');", "const a = +Number('1');", None),
        ("const a: number = -Number('1');", "const a = -Number('1');", None),
        ("const a: number = Number?.('1');", "const a = Number?.('1');", None),
        ("const a: number = +Number?.('1');", "const a = +Number?.('1');", None),
        ("const a: number = -Number?.('1');", "const a = -Number?.('1');", None),
        ("const a: number = Infinity;", "const a = Infinity;", None),
        ("const a: number = +Infinity;", "const a = +Infinity;", None),
        ("const a: number = -Infinity;", "const a = -Infinity;", None),
        ("const a: number = NaN;", "const a = NaN;", None),
        ("const a: number = +NaN;", "const a = +NaN;", None),
        ("const a: number = -NaN;", "const a = -NaN;", None),
        ("const a: null = null;", "const a = null;", None),
        ("const a: RegExp = /a/;", "const a = /a/;", None),
        ("const a: RegExp = RegExp('a');", "const a = RegExp('a');", None),
        ("const a: RegExp = RegExp?.('a');", "const a = RegExp?.('a');", None),
        ("const a: RegExp = new RegExp('a');", "const a = new RegExp('a');", None),
        ("const a: string = 'str';", "const a = 'str';", None),
        ("const a: string = `str`;", "const a = `str`;", None),
        ("const a: string = String(1);", "const a = String(1);", None),
        ("const a: string = String?.(1);", "const a = String?.(1);", None),
        ("const a: symbol = Symbol('a');", "const a = Symbol('a');", None),
        ("const a: symbol = Symbol?.('a');", "const a = Symbol?.('a');", None),
        ("const a: undefined = undefined;", "const a = undefined;", None),
        ("const a: undefined = void someValue;", "const a = void someValue;", None),
        // (
        //     "const fn = (a?: number = 5) => {};",
        //     "const fn = (a = 5) => {};",
        //     Some(serde_json::json!([ { "ignoreParameters": false, }, ])),
        // ),
        (
            "
            class A {
              a: number = 1;
            }
                  ",
            "
            class A {
              a = 1;
            }
                  ",
            Some(serde_json::json!([ { "ignoreProperties": false, }, ])),
        ),
        (
            "const fn = (a: number = 5, b: boolean = true, c: string = 'foo') => {};",
            "const fn = (a = 5, b = true, c = 'foo') => {};",
            Some(serde_json::json!([ { "ignoreParameters": false, "ignoreProperties": false, }, ])),
        ),
        (
            "
            class Foo {
              a: number = 5;
              b: boolean = true;
              c: string = 'foo';
            }
                  ",
            "
            class Foo {
              a = 5;
              b = true;
              c = 'foo';
            }
                  ",
            Some(serde_json::json!([ { "ignoreParameters": false, "ignoreProperties": false, }, ])),
        ),
        (
            "
            class Foo {
              constructor(public a: boolean = true) {}
            }
                  ",
            "
            class Foo {
              constructor(public a = true) {}
            }
                  ",
            Some(serde_json::json!([ { "ignoreParameters": false, "ignoreProperties": false, }, ])),
        ),
        (
            "
            class Foo {
              accessor a: number = 5;
            }
                  ",
            "
            class Foo {
              accessor a = 5;
            }
                  ",
            None,
        ),
    ];

    Tester::new(NoInferrableTypes::NAME, NoInferrableTypes::PLUGIN, pass, fail)
        .expect_fix(fix)
        .test_and_snapshot();
}
