use std::{path::Path, str::FromStr};

use oxc_allocator::{Allocator, ArenaVec, CloneIn};
use oxc_ast::{
    AstKind,
    ast::{ImportDeclaration, ImportDeclarationSpecifier, ImportOrExportKind},
    builder::AstBuilder,
};
use oxc_codegen::{Context, Gen};
use oxc_diagnostics::OxcDiagnostic;
use oxc_macros::declare_oxc_lint;
use oxc_span::{FileExtension, GetSpan, SPAN, Span};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::Value;

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

fn use_top_level_for_declaration_file_import_diagnostic(span: Span) -> OxcDiagnostic {
    OxcDiagnostic::warn(
        "Type imports from declaration files must use top-level `import type` syntax.",
    )
    .with_help("Replace inline type specifiers with a top-level import type statement.")
    .with_label(span)
}

fn consistent_type_specifier_style_diagnostic(span: Span, mode: &Mode) -> OxcDiagnostic {
    let (warn_msg, help_msg) = match mode {
        Mode::Inline => (
            "Prefer using inline type specifiers instead of a top-level type-only import.",
            "Replace top‐level import type with an inline type specifier.",
        ),
        Mode::TopLevel => (
            "Prefer using a top-level type-only import instead of inline type specifiers.",
            "Replace inline type specifiers with a top‐level import type statement.",
        ),
        Mode::TopLevelIfOnlyTypeImports => (
            "Prefer using a top-level type-only import instead of inline type specifiers when there are only type imports.",
            "Replace inline type specifiers with a top‐level import type statement.",
        ),
    };
    OxcDiagnostic::warn(warn_msg).with_help(help_msg).with_label(span)
}

#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize, JsonSchema)]
enum Mode {
    /// Prefer `import type { Foo } from 'foo'` for type imports.
    #[default]
    #[serde(rename = "prefer-top-level")]
    TopLevel,
    /// Prefer `import { type Foo } from 'foo'` for type imports.
    #[serde(rename = "prefer-inline")]
    Inline,
    /// Prefer `import type { Foo } from 'foo'` when all named imports are types, but allow
    /// `import { type Foo, bar } from 'foo'` when value imports are present.
    #[serde(rename = "prefer-top-level-if-only-type-imports")]
    TopLevelIfOnlyTypeImports,
}

#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct ConsistentTypeSpecifierStyle(Mode);

declare_oxc_lint!(
    /// ### What it does
    ///
    /// Enforces or bans the use of inline type-only markers for named imports.
    ///
    /// ### Why is this bad?
    ///
    /// Mixing top-level `import type { Foo } from 'foo'` with inline `{ type Bar }`
    /// forces readers to mentally switch contexts when scanning your imports.
    /// Enforcing one style makes it immediately obvious which imports are types and which are value imports.
    ///
    /// ### Examples
    ///
    /// Examples of incorrect code for the default `prefer-top-level` option:
    /// ```typescript
    /// import { type Foo } from 'Foo';
    /// import Foo, { type Bar } from 'Foo';
    /// ```
    ///
    /// Examples of correct code for the default option:
    /// ```typescript
    /// import type { Foo } from 'Foo';
    /// import type Foo, { Bar } from 'Foo';
    /// ```
    ///
    /// Examples of incorrect code for the `prefer-top-level-if-only-type-imports` option:
    /// ```typescript
    /// import { type Foo } from 'Foo';
    /// import { type Foo, type Bar } from 'Foo';
    /// ```
    ///
    /// Examples of correct code for the `prefer-top-level-if-only-type-imports` option:
    /// ```typescript
    /// import type { Foo } from 'Foo';
    /// import { type Foo, someValue } from 'Foo';
    /// import type Foo, { Bar } from 'Foo';
    /// ```
    ///
    /// Examples of incorrect code for the `prefer-inline` option:
    /// ```typescript
    /// import type { Foo } from 'Foo';
    /// import type Foo, { Bar } from 'Foo';
    /// ```
    ///
    /// Examples of correct code for the `prefer-inline` option:
    /// ```typescript
    /// import { type Foo } from 'Foo';
    /// import Foo, { type Bar } from 'Foo';
    /// ```
    ConsistentTypeSpecifierStyle,
    import,
    style,
    conditional_fix,
    config = Mode,
    version = "0.16.11",
    short_description = "Enforces or bans the use of inline type-only markers for named imports.",
);

impl Rule for ConsistentTypeSpecifierStyle {
    fn from_configuration(value: 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>) {
        let AstKind::ImportDeclaration(import_decl) = node.kind() else {
            return;
        };
        let Some(specifiers) = &import_decl.specifiers else {
            return;
        };
        let len = specifiers.len();
        if len == 0
            || (len == 1
                && !matches!(specifiers[0], ImportDeclarationSpecifier::ImportSpecifier(_)))
        {
            return;
        }

        // Declaration file imports should always be top-level type imports.
        if import_decl.import_kind.is_value() {
            let is_declaration_file_import = is_declaration_file_import(import_decl);
            if self.0 == Mode::Inline && !is_declaration_file_import {
                return;
            }

            let (value_specifiers, type_specifiers) = split_import_specifiers_by_kind(specifiers);
            if type_specifiers.is_empty() {
                return;
            }

            let should_use_top_level = is_declaration_file_import
                || self.0 == Mode::TopLevel
                || (self.0 == Mode::TopLevelIfOnlyTypeImports && value_specifiers.is_empty());
            if !should_use_top_level {
                return;
            }

            let report = |diagnostic| {
                ctx.diagnostic_with_fix(diagnostic, |fixer| {
                    let mut import_source = String::new();

                    if !value_specifiers.is_empty() {
                        let value_import_declaration =
                            gen_value_import_declaration(fixer, import_decl, &value_specifiers);
                        import_source.push_str(&value_import_declaration);
                    }

                    let type_import_declaration =
                        gen_type_import_declaration(fixer, import_decl, &type_specifiers);
                    import_source.push_str(&type_import_declaration);

                    fixer
                        .replace(import_decl.span, import_source.trim_end().to_string())
                        .with_message("Convert to a `top-level` type import")
                });
            };

            if self.0 == Mode::TopLevelIfOnlyTypeImports && !is_declaration_file_import {
                report(consistent_type_specifier_style_diagnostic(import_decl.span, &self.0));
            } else {
                for item in &type_specifiers {
                    let diagnostic = if is_declaration_file_import {
                        use_top_level_for_declaration_file_import_diagnostic(item.span())
                    } else {
                        consistent_type_specifier_style_diagnostic(item.span(), &self.0)
                    };
                    report(diagnostic);
                }
            }
        } else if self.0 == Mode::Inline && import_decl.import_kind.is_type() {
            if is_declaration_file_import(import_decl) {
                return;
            }
            ctx.diagnostic_with_fix(
                consistent_type_specifier_style_diagnostic(import_decl.span, &self.0),
                |fixer| {
                    let fixer = fixer.for_multifix();
                    let mut rule_fixes = fixer.new_fix_with_capacity(len);
                    for item in specifiers {
                        rule_fixes.push(fixer.insert_text_before(item, "type "));
                    }
                    // find the 'type' keyword and remove it
                    if let Some(pos) = ctx.find_next_token_from(import_decl.span.start, "type") {
                        let type_token_span = Span::sized(import_decl.span.start + pos, 4);
                        let remove_fix = fixer.delete_range(type_token_span);
                        rule_fixes.push(remove_fix);
                    }
                    rule_fixes.with_message("Convert to an `inline` type import")
                },
            );
        }
    }
}

fn split_import_specifiers_by_kind<'a, I>(specifiers: I) -> (Vec<I::Item>, Vec<I::Item>)
where
    I: IntoIterator<Item = &'a ImportDeclarationSpecifier<'a>>,
{
    let (value_kind_specifiers, type_kind_specifiers) = specifiers.into_iter().fold(
        (vec![], vec![]),
        |(mut value_kind_specifiers, mut type_kind_specifiers), it| {
            match it {
                ImportDeclarationSpecifier::ImportDefaultSpecifier(_) => {
                    value_kind_specifiers.push(it);
                }
                ImportDeclarationSpecifier::ImportSpecifier(specifier) => {
                    if specifier.import_kind.is_value() {
                        value_kind_specifiers.push(it);
                    } else {
                        type_kind_specifiers.push(it);
                    }
                }
                ImportDeclarationSpecifier::ImportNamespaceSpecifier(_) => {}
            }
            (value_kind_specifiers, type_kind_specifiers)
        },
    );
    (value_kind_specifiers, type_kind_specifiers)
}

fn gen_value_import_declaration<'c, 'a: 'c>(
    fixer: RuleFixer<'c, 'a>,
    import_decl: &'a ImportDeclaration<'a>,
    specifiers: &Vec<&'a ImportDeclarationSpecifier<'a>>,
) -> String {
    let mut codegen = fixer.codegen();

    let alloc = Allocator::default();
    let ast_builder = AstBuilder::new(&alloc);

    let specifiers: Vec<_> = specifiers.iter().map(|it| it.clone_in(&alloc)).collect();
    let import_declaration = ImportDeclaration::boxed(
        SPAN,
        Some(ArenaVec::from_iter_in(specifiers, &ast_builder)),
        import_decl.source.clone_in(&alloc),
        None,
        import_decl.with_clause.clone_in(&alloc),
        ImportOrExportKind::Value,
        &ast_builder,
    );

    import_declaration.print(&mut codegen, Context::empty());
    codegen.into_source_text()
}

fn gen_type_import_declaration<'c, 'a: 'c>(
    fixer: RuleFixer<'c, 'a>,
    import_decl: &'a ImportDeclaration<'a>,
    specifiers: &Vec<&'a ImportDeclarationSpecifier<'a>>,
) -> String {
    let mut codegen = fixer.codegen();

    let alloc = Allocator::default();
    let ast_builder = AstBuilder::new(&alloc);

    let specifiers: Vec<_> = specifiers
        .iter()
        .filter_map(|it| match it {
            ImportDeclarationSpecifier::ImportSpecifier(specifier) => {
                Some(ImportDeclarationSpecifier::new_import_specifier(
                    SPAN,
                    specifier.imported.clone_in(&alloc),
                    specifier.local.clone_in(&alloc),
                    ImportOrExportKind::Value,
                    &ast_builder,
                ))
            }
            _ => None,
        })
        .collect();
    let import_declaration = ImportDeclaration::boxed(
        SPAN,
        Some(ArenaVec::from_iter_in(specifiers, &ast_builder)),
        import_decl.source.clone_in(&alloc),
        None,
        import_decl.with_clause.clone_in(&alloc),
        ImportOrExportKind::Type,
        &ast_builder,
    );

    import_declaration.print(&mut codegen, Context::empty());
    codegen.into_source_text()
}

fn is_declaration_file_import(import_decl: &ImportDeclaration) -> bool {
    let source = &import_decl.source.value;
    // Relatively fast check to avoid unnecessary Path and extension parsing
    // if it doesn't even look like a declaration file import
    if !source.contains(".d") {
        return false;
    }
    // Slower check that parses the file name to check if it's a declaration file
    let path = Path::new(source.as_str());
    let Some(extension) = path.extension().and_then(std::ffi::os_str::OsStr::to_str) else {
        return false;
    };
    match FileExtension::from_str(extension) {
        Ok(file_ext) => file_ext.is_ts_declaration(source),
        Err(_) => false,
    }
}

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

    let pass = vec![
        ("import Foo from 'Foo'", None),
        ("import type Foo from 'Foo'", None),
        ("import { Foo } from 'Foo';", None),
        ("import { Foo as Bar } from 'Foo';", None),
        ("import * as Foo from 'Foo';", None),
        ("import 'Foo';", None),
        ("import {} from 'Foo';", None),
        ("import type {} from 'Foo';", None),
        ("import type { Foo as Bar } from 'Foo';", Some(json!(["prefer-top-level"]))),
        ("import type { Foo, Bar, Baz, Bam } from 'Foo';", Some(json!(["prefer-top-level"]))),
        ("import type {Foo} from 'Foo'", Some(json!(["prefer-top-level"]))),
        ("import Foo from 'Foo';", Some(json!(["prefer-top-level-if-only-type-imports"]))),
        ("import type Foo from 'Foo';", Some(json!(["prefer-top-level-if-only-type-imports"]))),
        ("import { Foo } from 'Foo';", Some(json!(["prefer-top-level-if-only-type-imports"]))),
        (
            "import { Foo as Bar } from 'Foo';",
            Some(json!(["prefer-top-level-if-only-type-imports"])),
        ),
        ("import * as Foo from 'Foo';", Some(json!(["prefer-top-level-if-only-type-imports"]))),
        ("import 'Foo';", Some(json!(["prefer-top-level-if-only-type-imports"]))),
        ("import {} from 'Foo';", Some(json!(["prefer-top-level-if-only-type-imports"]))),
        ("import type {} from 'Foo';", Some(json!(["prefer-top-level-if-only-type-imports"]))),
        ("import type { Foo } from 'Foo';", Some(json!(["prefer-top-level-if-only-type-imports"]))),
        (
            "import type { Foo as Bar } from 'Foo';",
            Some(json!(["prefer-top-level-if-only-type-imports"])),
        ),
        (
            "import type { Foo, Bar, Baz, Bam } from 'Foo';",
            Some(json!(["prefer-top-level-if-only-type-imports"])),
        ),
        (
            "import { Foo, type Bar } from 'Foo';",
            Some(json!(["prefer-top-level-if-only-type-imports"])),
        ),
        (
            "import { type Foo, Bar } from 'Foo';",
            Some(json!(["prefer-top-level-if-only-type-imports"])),
        ),
        (
            "import Foo, { type Bar } from 'Foo';",
            Some(json!(["prefer-top-level-if-only-type-imports"])),
        ),
        (
            "import Foo, { type Bar, Baz } from 'Foo';",
            Some(json!(["prefer-top-level-if-only-type-imports"])),
        ),
        ("import {type Foo} from 'Foo'", Some(json!(["prefer-inline"]))),
        ("import Foo from 'Foo';", Some(json!(["prefer-inline"]))),
        ("import type Foo from 'Foo';", Some(json!(["prefer-inline"]))),
        ("import { Foo } from 'Foo';", Some(json!(["prefer-inline"]))),
        ("import { Foo as Bar } from 'Foo';", Some(json!(["prefer-inline"]))),
        ("import * as Foo from 'Foo';", Some(json!(["prefer-inline"]))),
        ("import 'Foo';", Some(json!(["prefer-inline"]))),
        ("import {} from 'Foo';", Some(json!(["prefer-inline"]))),
        ("import type {} from 'Foo';", Some(json!(["prefer-inline"]))),
        ("import { type Foo } from 'Foo';", Some(json!(["prefer-inline"]))),
        ("import { type Foo as Bar } from 'Foo';", Some(json!(["prefer-inline"]))),
        ("import { type Foo, type Bar, Baz, Bam } from 'Foo';", Some(json!(["prefer-inline"]))),
        ("import type * as Foo from 'Foo';", None),
        // declaration files always require `import type` syntax
        ("import type { Foo } from './index.d.ts';", Some(json!(["prefer-top-level"]))),
        ("import type { Foo } from './index.d.ts';", Some(json!(["prefer-inline"]))),
        ("import type { Foo } from './index.d.mts';", Some(json!(["prefer-top-level"]))),
        ("import type { Foo } from './index.d.mts';", Some(json!(["prefer-inline"]))),
        ("import type { Foo } from './index.d.cts';", Some(json!(["prefer-top-level"]))),
        ("import type { Foo } from './index.d.cts';", Some(json!(["prefer-inline"]))),
        ("import type { Foo } from './app.d.css.ts';", Some(json!(["prefer-top-level"]))),
        ("import type { Foo } from './app.d.css.ts';", Some(json!(["prefer-inline"]))),
        (
            "import type { Foo } from './index.d.ts';",
            Some(json!(["prefer-top-level-if-only-type-imports"])),
        ),
    ];

    let fail = vec![
        ("import { type Foo, type Bar } from 'Foo'", None),
        ("import type { Foo } from 'Foo'", Some(json!(["prefer-inline"]))),
        ("import { type Foo as Bar } from 'Foo';", None),
        ("import { type Foo, type Bar } from 'Foo';", None),
        ("import { Foo, type Bar } from 'Foo';", None),
        ("import { type Foo, Bar } from 'Foo';", None),
        ("import Foo, { type Bar } from 'Foo';", None),
        ("import Foo, { type Bar, Baz } from 'Foo';", None),
        ("import { Component, type ComponentProps } from 'package-1';", None),
        ("import type { Foo, Bar, Baz } from 'Foo';", Some(json!(["prefer-inline"]))),
        ("import { type Foo } from 'Foo';", Some(json!(["prefer-top-level-if-only-type-imports"]))),
        (
            "import { type Foo as Bar } from 'Foo';",
            Some(json!(["prefer-top-level-if-only-type-imports"])),
        ),
        (
            "import { type Foo, type Bar } from 'Foo';",
            Some(json!(["prefer-top-level-if-only-type-imports"])),
        ),
        // declaration files always require `import type` syntax
        ("import { type Foo } from './index.d.ts';", Some(json!(["prefer-top-level"]))),
        ("import { type Foo } from './index.d.ts';", Some(json!(["prefer-inline"]))),
        ("import { type Foo } from './index.d.mts';", Some(json!(["prefer-top-level"]))),
        ("import { type Foo } from './index.d.mts';", Some(json!(["prefer-inline"]))),
        ("import { type Foo } from './index.d.cts';", Some(json!(["prefer-top-level"]))),
        ("import { type Foo } from './index.d.cts';", Some(json!(["prefer-inline"]))),
        ("import { type Foo } from './app.d.css.ts';", Some(json!(["prefer-top-level"]))),
        ("import { type Foo } from './app.d.css.ts';", Some(json!(["prefer-inline"]))),
        (
            "import { Foo, type Bar } from './index.d.ts';",
            Some(json!(["prefer-top-level-if-only-type-imports"])),
        ),
    ];

    let fix = vec![
        (
            "import type { foo, bar } from 'foo'",
            "import  { type foo, type bar } from 'foo'",
            Some(json!(["prefer-inline"])),
        ),
        (
            "import type{ foo } from 'foo'",
            "import { type foo } from 'foo'",
            Some(json!(["prefer-inline"])),
        ),
        (
            "import type /** comment */{ foo } from 'foo'",
            "import  /** comment */{ type foo } from 'foo'",
            Some(json!(["prefer-inline"])),
        ),
        (
            "import type { foo, /** comments */ bar } from 'foo'",
            "import  { type foo, /** comments */ type bar } from 'foo'",
            Some(json!(["prefer-inline"])),
        ),
        (
            "import /* type */ type { foo } from 'foo'",
            "import /* type */  { type foo } from 'foo'",
            Some(json!(["prefer-inline"])),
        ),
        (
            r"
                import type {
                    bar,
                } from 'foo'
            ",
            r"
                import  {
                    type bar,
                } from 'foo'
            ",
            Some(json!(["prefer-inline"])),
        ),
        (
            "import { type Foo } from 'Foo';",
            "import type { Foo } from 'Foo';",
            Some(json!(["prefer-top-level"])),
        ),
        (
            "import { type Foo as Bar } from 'Foo';",
            "import type { Foo as Bar } from 'Foo';",
            Some(json!(["prefer-top-level"])),
        ),
        (
            "import { type Foo, type Bar } from 'Foo';",
            "import type { Foo, Bar } from 'Foo';",
            Some(json!(["prefer-top-level"])),
        ),
        (
            "import { type Foo, type Bar } from 'Foo';",
            "import type { Foo, Bar } from 'Foo';",
            Some(json!(["prefer-top-level"])),
        ),
        (
            "import { Foo, type Bar } from 'Foo';",
            "import { Foo } from 'Foo';\nimport type { Bar } from 'Foo';",
            Some(json!(["prefer-top-level"])),
        ),
        (
            "import { type Foo, Bar } from 'Foo';",
            "import { Bar } from 'Foo';\nimport type { Foo } from 'Foo';",
            Some(json!(["prefer-top-level"])),
        ),
        (
            "import Foo, { type Bar } from 'Foo';",
            "import Foo from 'Foo';\nimport type { Bar } from 'Foo';",
            Some(json!(["prefer-top-level"])),
        ),
        (
            "import Foo, { type Bar, Baz } from 'Foo';",
            "import Foo, { Baz } from 'Foo';\nimport type { Bar } from 'Foo';",
            Some(json!(["prefer-top-level"])),
        ),
        (
            "import { type Foo } from 'index.d.ts';",
            "import type { Foo } from 'index.d.ts';",
            Some(json!(["prefer-top-level"])),
        ),
        (
            "import { type Foo } from 'index.d.ts';",
            "import type { Foo } from 'index.d.ts';",
            Some(json!(["prefer-inline"])),
        ),
        (
            "import { type Foo } from 'Foo';",
            "import type { Foo } from 'Foo';",
            Some(json!(["prefer-top-level-if-only-type-imports"])),
        ),
        (
            "import { type Foo as Bar } from 'Foo';",
            "import type { Foo as Bar } from 'Foo';",
            Some(json!(["prefer-top-level-if-only-type-imports"])),
        ),
        (
            "import { type Foo, type Bar } from 'Foo';",
            "import type { Foo, Bar } from 'Foo';",
            Some(json!(["prefer-top-level-if-only-type-imports"])),
        ),
        (
            "import { Foo, type Bar } from './index.d.ts';",
            "import { Foo } from './index.d.ts';\nimport type { Bar } from './index.d.ts';",
            Some(json!(["prefer-top-level-if-only-type-imports"])),
        ),
    ];

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