use oxc_ast::{
    AstKind,
    ast::{
        ClassElement, Declaration, ExportDefaultDeclarationKind, FunctionType, ModuleDeclaration,
        PropertyKey, Statement, TSSignature, match_expression,
    },
};
use oxc_diagnostics::OxcDiagnostic;
use oxc_macros::declare_oxc_lint;
use oxc_span::{GetSpan, Span};
use oxc_str::CompactStr;

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

fn adjacent_overload_signatures_diagnostic(
    fn_name: &str,
    first: Option<Span>,
    second: Span,
) -> OxcDiagnostic {
    let mut d = OxcDiagnostic::warn(format!("All {fn_name:?} signatures should be adjacent."))
        .with_help(format!("Move all {fn_name:?} overload signatures together, placing them consecutively before any other members."))
        .with_note("Function overload signatures represent multiple ways a function can be called. Keeping them adjacent makes it easier for developers to understand all available call signatures at a glance.");
    if let Some(span) = first {
        d = d.and_label(span);
    }
    d.and_label(second)
}

#[derive(Debug, Default, Clone)]
pub struct AdjacentOverloadSignatures;

declare_oxc_lint!(
    /// ### What it does
    ///
    /// Require that function overload signatures be consecutive.
    ///
    /// ### Why is this bad?
    ///
    /// Function overload signatures represent multiple ways
    /// a function can be called, potentially with different return types.
    /// It's typical for an interface or type alias describing a function to place all overload signatures next to each other.
    /// If Signatures placed elsewhere in the type are easier to be missed by future developers reading the code.
    ///
    /// ### Examples
    ///
    /// Examples of **incorrect** code for this rule:
    /// ```typescript
    /// declare namespace Foo {
    ///   export function foo(s: string): void;
    ///   export function foo(n: number): void;
    ///   export function bar(): void;
    ///   export function foo(sn: string | number): void;
    /// }
    ///
    /// type Foo = {
    ///   foo(s: string): void;
    ///   foo(n: number): void;
    ///   bar(): void;
    ///   foo(sn: string | number): void;
    /// };
    ///
    /// interface Foo {
    ///   foo(s: string): void;
    ///   foo(n: number): void;
    ///   bar(): void;
    ///   foo(sn: string | number): void;
    /// }
    ///
    /// class Foo {
    ///   foo(s: string): void;
    ///   foo(n: number): void;
    ///   bar(): void {}
    ///   foo(sn: string | number): void {}
    /// }
    ///
    /// export function foo(s: string): void;
    /// export function foo(n: number): void;
    /// export function bar(): void;
    /// export function foo(sn: string | number): void;
    /// ```
    AdjacentOverloadSignatures,
    typescript,
    style,
    version = "0.0.7",
    short_description = "Require that function overload signatures be consecutive.",
);

#[derive(PartialEq, Debug)]
enum MethodKind {
    Private,
    Quoted,
    Normal,
    Expression,
}

fn get_kind_from_key(key: &PropertyKey) -> MethodKind {
    #[expect(clippy::match_same_arms)]
    match key {
        PropertyKey::StaticIdentifier(_) => MethodKind::Normal,
        PropertyKey::PrivateIdentifier(_) => MethodKind::Private,
        PropertyKey::StringLiteral(_) => MethodKind::Normal,
        PropertyKey::NumericLiteral(_)
        | PropertyKey::BigIntLiteral(_)
        | PropertyKey::TemplateLiteral(_)
        | PropertyKey::RegExpLiteral(_)
        | PropertyKey::NullLiteral(_) => MethodKind::Quoted,
        match_expression!(PropertyKey) => MethodKind::Expression,
    }
}

#[derive(Debug)]
struct Method {
    name: CompactStr,
    r#static: bool,
    call_signature: bool,
    kind: MethodKind,
    span: Span,
}

impl Method {
    fn is_same_method(&self, other: Option<&Self>) -> bool {
        other.is_some_and(|other| {
            self.name == other.name
                && self.r#static == other.r#static
                && self.call_signature == other.call_signature
                && self.kind == other.kind
        })
    }
}

trait GetMethod {
    fn get_method(&self) -> Option<Method>;
}

impl GetMethod for ClassElement<'_> {
    fn get_method(&self) -> Option<Method> {
        match self {
            ClassElement::MethodDefinition(def) => def.key.static_name().map(|name| Method {
                name: name.into(),
                r#static: def.r#static,
                call_signature: false,
                kind: get_kind_from_key(&def.key),
                span: Span::new(def.span.start, def.key.span().end),
            }),
            _ => None,
        }
    }
}

impl GetMethod for TSSignature<'_> {
    fn get_method(&self) -> Option<Method> {
        match self {
            TSSignature::TSMethodSignature(sig) => sig.key.static_name().map(|name| Method {
                name: name.into(),
                r#static: false,
                call_signature: false,
                kind: get_kind_from_key(&sig.key),
                span: sig.key.span(),
            }),
            TSSignature::TSCallSignatureDeclaration(sig) => Some(Method {
                name: "call".into(),
                r#static: false,
                call_signature: true,
                kind: MethodKind::Normal,
                span: sig.span,
            }),
            TSSignature::TSConstructSignatureDeclaration(decl) => Some(Method {
                name: "new".into(),
                r#static: false,
                call_signature: false,
                kind: MethodKind::Normal,
                span: Span::sized(decl.span.start, 3),
            }),
            _ => None,
        }
    }
}

impl GetMethod for ModuleDeclaration<'_> {
    fn get_method(&self) -> Option<Method> {
        match self {
            ModuleDeclaration::ExportDefaultDeclaration(default_decl) => {
                let decl_kind = &default_decl.declaration;

                match decl_kind {
                    ExportDefaultDeclarationKind::FunctionDeclaration(func_decl) => {
                        if matches!(
                            func_decl.r#type,
                            FunctionType::FunctionDeclaration | FunctionType::TSDeclareFunction
                        ) {
                            func_decl.id.as_ref().map(|id| Method {
                                name: id.name.to_compact_str(),
                                r#static: false,
                                call_signature: false,
                                kind: MethodKind::Normal,
                                span: id.span,
                            })
                        } else {
                            None
                        }
                    }
                    _ => None,
                }
            }
            ModuleDeclaration::ExportNamedDeclaration(named_decl) => {
                if let Some(Declaration::FunctionDeclaration(func_decl)) = &named_decl.declaration {
                    return func_decl.id.as_ref().map(|id| Method {
                        name: id.name.to_compact_str(),
                        r#static: false,
                        call_signature: false,
                        kind: MethodKind::Normal,
                        span: id.span,
                    });
                }
                None
            }
            _ => None,
        }
    }
}

impl GetMethod for Declaration<'_> {
    fn get_method(&self) -> Option<Method> {
        match self {
            Declaration::FunctionDeclaration(func_decl) => {
                if matches!(
                    func_decl.r#type,
                    FunctionType::FunctionDeclaration | FunctionType::TSDeclareFunction
                ) {
                    func_decl.id.as_ref().map(|id| Method {
                        name: id.name.to_compact_str(),
                        r#static: false,
                        call_signature: false,
                        kind: MethodKind::Normal,
                        span: id.span,
                    })
                } else {
                    None
                }
            }
            _ => None,
        }
    }
}

impl GetMethod for Statement<'_> {
    fn get_method(&self) -> Option<Method> {
        if let Some(decl) = self.as_module_declaration() {
            decl.get_method()
        } else if let Some(decl) = self.as_declaration() {
            decl.get_method()
        } else {
            None
        }
    }
}

fn check_and_report(methods: &Vec<Option<Method>>, ctx: &LintContext<'_>) {
    let mut last_method: Option<&Method> = None;
    let mut seen_methods: Vec<&Method> = Vec::new();

    for method in methods {
        if let Some(method) = method {
            let index = seen_methods.iter().position(|m| method.is_same_method(Some(m)));

            if index.is_some() && !method.is_same_method(last_method) {
                let name = if method.r#static {
                    format!("static {0}", method.name)
                } else {
                    method.name.to_string()
                };

                let last_same_method =
                    seen_methods.iter().rev().find(|m| m.is_same_method(Some(method)));

                ctx.diagnostic(adjacent_overload_signatures_diagnostic(
                    &name,
                    last_same_method.map(|m| m.span),
                    method.span,
                ));
            } else {
                seen_methods.push(method);
            }
            last_method = Some(method);
        } else {
            last_method = None;
        }
    }
}

impl Rule for AdjacentOverloadSignatures {
    fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
        match node.kind() {
            AstKind::Class(class) => {
                let members = &class.body.body;
                let methods = members.iter().map(GetMethod::get_method).collect();
                check_and_report(&methods, ctx);
            }
            AstKind::TSTypeLiteral(literal) => {
                let methods = literal.members.iter().map(GetMethod::get_method).collect();
                check_and_report(&methods, ctx);
            }
            AstKind::Program(program) => {
                let methods = program.body.iter().map(GetMethod::get_method).collect();

                check_and_report(&methods, ctx);
            }
            AstKind::TSModuleBlock(block) => {
                let methods = block.body.iter().map(GetMethod::get_method).collect();

                check_and_report(&methods, ctx);
            }
            AstKind::TSInterfaceDeclaration(decl) => {
                let methods = decl.body.body.iter().map(GetMethod::get_method).collect();

                check_and_report(&methods, ctx);
            }
            AstKind::BlockStatement(stmt) => {
                let methods = stmt.body.iter().map(GetMethod::get_method).collect();

                check_and_report(&methods, ctx);
            }
            AstKind::FunctionBody(body) => {
                let methods = body.statements.iter().map(GetMethod::get_method).collect();

                check_and_report(&methods, ctx);
            }
            _ => {}
        }
    }

    fn should_run(&self, ctx: &ContextHost) -> bool {
        ctx.source_type().is_typescript()
    }
}

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

    let pass = vec![
        r"function error(a: string);
      function error(b: number);
      function error(ab: string | number) {}
      export { error };",
        r"import { connect } from 'react-redux';
      export interface ErrorMessageModel {
        message: string;
      }
      function mapStateToProps() {}
      function mapDispatchToProps() {}
      export default connect(mapStateToProps, mapDispatchToProps)(ErrorMessage);",
        r"export const foo = 'a',
      bar = 'b';
      export interface Foo {}
      export class Foo {}",
        r"export interface Foo {}
      export const foo = 'a',
        bar = 'b';
      export class Foo {}",
        r"const foo = 'a',
      bar = 'b';
      interface Foo {}
      class Foo {}",
        r"interface Foo {}
      const foo = 'a',
        bar = 'b';
      class Foo {}",
        r"export class Foo {}
      export class Bar {}
      export type FooBar = Foo | Bar;",
        r"export interface Foo {}
      export class Foo {}
      export class Bar {}
      export type FooBar = Foo | Bar;",
        r"export function foo(s: string);
      export function foo(n: number);
      export function foo(sn: string | number) {}
      export function bar(): void {}
      export function baz(): void {}",
        r"function foo(s: string);
      function foo(n: number);
      function foo(sn: string | number) {}
      function bar(): void {}
      function baz(): void {}",
        r"declare function foo(s: string);
      declare function foo(n: number);
      declare function foo(sn: string | number);
      declare function bar(): void;
      declare function baz(): void;",
        r"declare module 'Foo' {
        export function foo(s: string): void;
        export function foo(n: number): void;
        export function foo(sn: string | number): void;
        export function bar(): void;
        export function baz(): void;
      }",
        r"declare namespace Foo {
        export function foo(s: string): void;
        export function foo(n: number): void;
        export function foo(sn: string | number): void;
        export function bar(): void;
        export function baz(): void;
      }",
        r"type Foo = {
        foo(s: string): void;
        foo(n: number): void;
        foo(sn: string | number): void;
        bar(): void;
        baz(): void;
      };",
        r"type Foo = {
        foo(s: string): void;
        ['foo'](n: number): void;
        foo(sn: string | number): void;
        bar(): void;
        baz(): void;
      };",
        r"interface Foo {
        (s: string): void;
        (n: number): void;
        (sn: string | number): void;
        foo(n: number): void;
        bar(): void;
        baz(): void;
      }",
        r"interface Foo {
        (s: string): void;
        (n: number): void;
        (sn: string | number): void;
        foo(n: number): void;
        bar(): void;
        baz(): void;
        call(): void;
      }",
        r"interface Foo {
        foo(s: string): void;
        foo(n: number): void;
        foo(sn: string | number): void;
        bar(): void;
        baz(): void;
      }",
        r"interface Foo {
        foo(s: string): void;
        ['foo'](n: number): void;
        foo(sn: string | number): void;
        bar(): void;
        baz(): void;
      }",
        r"interface Foo {
        foo(): void;
        bar: {
          baz(s: string): void;
          baz(n: number): void;
          baz(sn: string | number): void;
        };
      }",
        r"interface Foo {
        new (s: string);
        new (n: number);
        new (sn: string | number);
        foo(): void;
      }",
        r"class Foo {
        constructor(s: string);
        constructor(n: number);
        constructor(sn: string | number) {}
        bar(): void {}
        baz(): void {}
      }",
        r"class Foo {
        foo(s: string): void;
        foo(n: number): void;
        foo(sn: string | number): void {}
        bar(): void {}
        baz(): void {}
      }",
        r"class Foo {
        foo(s: string): void;
        ['foo'](n: number): void;
        foo(sn: string | number): void {}
        bar(): void {}
        baz(): void {}
      }",
        r"class Foo {
        name: string;
        foo(s: string): void;
        foo(n: number): void;
        foo(sn: string | number): void {}
        bar(): void {}
        baz(): void {}
      }",
        r"class Foo {
        name: string;
        static foo(s: string): void;
        static foo(n: number): void;
        static foo(sn: string | number): void {}
        bar(): void {}
        baz(): void {}
      }",
        r"class Test {
        static test() {}
        untest() {}
        test() {}
      }",
        "export default function <T>(foo: T) {}",
        "export default function named<T>(foo: T) {}",
        r"interface Foo {
        [Symbol.toStringTag](): void;
        [Symbol.iterator](): void;
      }",
        r"class Test {
        #private(): void;
        #private(arg: number): void {}

        bar() {}

        '#private'(): void;
        '#private'(arg: number): void {}
      }",
        r"function wrap() {
        function foo(s: string);
        function foo(n: number);
        function foo(sn: string | number) {}
      }",
        r"if (true) {
        function foo(s: string);
        function foo(n: number);
        function foo(sn: string | number) {}
      }",
    ];

    let fail = vec![
        r"function wrap() {
        function foo(s: string);
        function foo(n: number);
        type bar = number;
        function foo(sn: string | number) {}
      }",
        r"if (true) {
        function foo(s: string);
        function foo(n: number);
        let a = 1;
        function foo(sn: string | number) {}
        foo(a);
      }",
        r"export function foo(s: string);
      export function foo(n: number);
      export function bar(): void {}
      export function baz(): void {}
      export function foo(sn: string | number) {}",
        r"export function foo(s: string);
      export function foo(n: number);
      export type bar = number;
      export type baz = number | string;
      export function foo(sn: string | number) {}",
        r"function foo(s: string);
      function foo(n: number);
      function bar(): void {}
      function baz(): void {}
      function foo(sn: string | number) {}",
        r"function foo(s: string);
      function foo(n: number);
      type bar = number;
      type baz = number | string;
      function foo(sn: string | number) {}",
        // commented because it would raise a syntax error,
        // which is beyond the scope of linter's responsibilities.
        // r#"function foo(s: string) {}
        // function foo(n: number) {}
        // const a = '';
        // const b = '';
        // function foo(sn: string | number) {}"#,
        // r#"function foo(s: string) {}
        // function foo(n: number) {}
        // class Bar {}
        // function foo(sn: string | number) {}"#,
        // r#"function foo(s: string) {}
        // function foo(n: number) {}
        // function foo(sn: string | number) {}
        // class Bar {
        //   foo(s: string);
        //   foo(n: number);
        //   name: string;
        //   foo(sn: string | number) {}
        // }"#
        r"declare function foo(s: string);
      declare function foo(n: number);
      declare function bar(): void;
      declare function baz(): void;
      declare function foo(sn: string | number);",
        r"declare function foo(s: string);
      declare function foo(n: number);
      const a = '';
      const b = '';
      declare function foo(sn: string | number);",
        r"declare module 'Foo' {
        export function foo(s: string): void;
        export function foo(n: number): void;
        export function bar(): void;
        export function baz(): void;
        export function foo(sn: string | number): void;
      }",
        r"declare module 'Foo' {
        export function foo(s: string): void;
        export function foo(n: number): void;
        export function foo(sn: string | number): void;
        function baz(s: string): void;
        export function bar(): void;
        function baz(n: number): void;
        function baz(sn: string | number): void;
      }",
        r"declare namespace Foo {
        export function foo(s: string): void;
        export function foo(n: number): void;
        export function bar(): void;
        export function baz(): void;
        export function foo(sn: string | number): void;
      }",
        r"declare namespace Foo {
        export function foo(s: string): void;
        export function foo(n: number): void;
        export function foo(sn: string | number): void;
        function baz(s: string): void;
        export function bar(): void;
        function baz(n: number): void;
        function baz(sn: string | number): void;
      }",
        r"type Foo = {
        foo(s: string): void;
        foo(n: number): void;
        bar(): void;
        baz(): void;
        foo(sn: string | number): void;
      };",
        r"type Foo = {
        foo(s: string): void;
        ['foo'](n: number): void;
        bar(): void;
        baz(): void;
        foo(sn: string | number): void;
      };",
        r"type Foo = {
        foo(s: string): void;
        name: string;
        foo(n: number): void;
        foo(sn: string | number): void;
        bar(): void;
        baz(): void;
      };",
        r"interface Foo {
        (s: string): void;
        foo(n: number): void;
        (n: number): void;
        (sn: string | number): void;
        bar(): void;
        baz(): void;
        call(): void;
      }",
        r"interface Foo {
        foo(s: string): void;
        foo(n: number): void;
        bar(): void;
        baz(): void;
        foo(sn: string | number): void;
      }",
        r"interface Foo {
        foo(s: string): void;
        ['foo'](n: number): void;
        bar(): void;
        baz(): void;
        foo(sn: string | number): void;
      }",
        r"interface Foo {
        foo(s: string): void;
        'foo'(n: number): void;
        bar(): void;
        baz(): void;
        foo(sn: string | number): void;
      }",
        r"interface Foo {
        foo(s: string): void;
        name: string;
        foo(n: number): void;
        foo(sn: string | number): void;
        bar(): void;
        baz(): void;
      }",
        r"interface Foo {
        foo(): void;
        bar: {
          baz(s: string): void;
          baz(n: number): void;
          foo(): void;
          baz(sn: string | number): void;
        };
      }",
        r"interface Foo {
        new (s: string);
        new (n: number);
        foo(): void;
        bar(): void;
        new (sn: string | number);
      }",
        r"interface Foo {
        new (s: string);
        foo(): void;
        new (n: number);
        bar(): void;
        new (sn: string | number);
      }",
        r"class Foo {
        constructor(s: string);
        constructor(n: number);
        bar(): void {}
        baz(): void {}
        constructor(sn: string | number) {}
      }",
        r"class Foo {
        foo(s: string): void;
        foo(n: number): void;
        bar(): void {}
        baz(): void {}
        foo(sn: string | number): void {}
      }",
        r"class Foo {
        foo(s: string): void;
        ['foo'](n: number): void;
        bar(): void {}
        baz(): void {}
        foo(sn: string | number): void {}
      }",
        r#"class Foo {
        // prettier-ignore
        "foo"(s: string): void;
        foo(n: number): void;
        bar(): void {}
        baz(): void {}
        foo(sn: string | number): void {}
      }"#,
        r"class Foo {
        constructor(s: string);
        name: string;
        constructor(n: number);
        constructor(sn: string | number) {}
        bar(): void {}
        baz(): void {}
      }",
        r"class Foo {
        foo(s: string): void;
        name: string;
        foo(n: number): void;
        foo(sn: string | number): void {}
        bar(): void {}
        baz(): void {}
      }",
        r"class Foo {
        static foo(s: string): void;
        name: string;
        static foo(n: number): void;
        static foo(sn: string | number): void {}
        bar(): void {}
        baz(): void {}
      }",
        r"class Test {
        #private(): void;
        '#private'(): void;
        #private(arg: number): void {}
        '#private'(arg: number): void {}
      }",
    ];

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