//! MDX import/export statement extraction.
//!
//! Extracts `import` and `export` lines from MDX files (Markdown with JSX),
//! handling multi-line imports via brace depth tracking.

use std::path::Path;

use oxc_allocator::Allocator;
use oxc_ast_visit::Visit;
use oxc_parser::Parser;
use oxc_span::SourceType;

use crate::ModuleInfo;
use crate::source_map::{ExtractionResult, SfcExtractor};
use crate::visitor::ModuleInfoExtractor;
use fallow_types::discover::FileId;

struct MdxExtractor;

impl SfcExtractor for MdxExtractor {
    fn extract(&self, source: &str) -> Vec<ExtractionResult> {
        let extracted = extract_mdx_source(source);
        if extracted.body.is_empty() {
            Vec::new()
        } else {
            vec![extracted]
        }
    }
}

/// Extract import/export statements from MDX content.
///
/// MDX files are Markdown with JSX. Only `import` and `export` lines are relevant
/// for dead code analysis. Multi-line imports (with unmatched braces) are handled
/// by tracking brace depth.
///
/// NOTE: CSS/SCSS `@apply` is handled in `parse_css_to_module()`, not here.
/// MDX import/export extraction only handles JS/TS `import`/`export` statements.
#[must_use]
pub fn extract_mdx_statements(source: &str) -> String {
    extract_mdx_source(source).body
}

fn extract_mdx_source(source: &str) -> ExtractionResult {
    let mut scanner = MdxStatementScanner::default();
    for (line_start, line) in lines_with_offsets(source) {
        scanner.scan_line(line_start, line);
    }
    scanner.into_extraction()
}

#[derive(Default)]
struct MdxStatementScanner<'a> {
    statements: Vec<(usize, &'a str)>,
    in_multiline: bool,
    brace_depth: i32,
    code_fence: Option<CodeFence>,
}

impl<'a> MdxStatementScanner<'a> {
    fn scan_line(&mut self, line_start: usize, line: &'a str) {
        let trimmed = line.trim();

        if self.consume_code_fence(trimmed) {
            return;
        }

        if self.in_multiline {
            self.push_multiline_line(line_start, line, trimmed);
            return;
        }

        if is_statement_start(trimmed) {
            self.push_statement_start(line_start, line, trimmed);
        }
    }

    fn consume_code_fence(&mut self, trimmed: &str) -> bool {
        if let Some(fence) = self.code_fence {
            if fence.is_closing_line(trimmed) {
                self.code_fence = None;
            }
            return true;
        }

        if self.in_multiline {
            return false;
        }

        if let Some(fence) = CodeFence::opening(trimmed) {
            self.code_fence = Some(fence);
            return true;
        }

        false
    }

    fn push_statement_start(&mut self, line_start: usize, line: &'a str, trimmed: &str) {
        self.statements.push((line_start, line));
        self.brace_depth = brace_delta(trimmed);
        if self.brace_depth > 0 && !has_spaced_source_clause(trimmed) {
            self.in_multiline = true;
        }
    }

    fn push_multiline_line(&mut self, line_start: usize, line: &'a str, trimmed: &str) {
        self.statements.push((line_start, line));
        self.brace_depth += brace_delta(trimmed);
        if self.brace_depth <= 0 || trimmed.ends_with(';') || has_any_source_clause(trimmed) {
            self.in_multiline = false;
            self.brace_depth = 0;
        }
    }

    fn into_extraction(self) -> ExtractionResult {
        let mut result = ExtractionResult::default();
        for (line_start, line) in self.statements {
            result.push_mapped(line, line_start);
        }
        result
    }
}

fn is_statement_start(trimmed: &str) -> bool {
    trimmed.starts_with("import ")
        || trimmed.starts_with("import{")
        || trimmed.starts_with("export ")
        || trimmed.starts_with("export{")
}

fn brace_delta(line: &str) -> i32 {
    let opens = line.chars().filter(|&ch| ch == '{').count();
    let closes = line.chars().filter(|&ch| ch == '}').count();
    let opens = i32::try_from(opens).unwrap_or(i32::MAX);
    let closes = i32::try_from(closes).unwrap_or(i32::MAX);
    opens.saturating_sub(closes)
}

fn has_spaced_source_clause(line: &str) -> bool {
    line.contains(" from ")
}

fn has_any_source_clause(line: &str) -> bool {
    has_spaced_source_clause(line) || line.contains(" from'") || line.contains(" from\"")
}

fn lines_with_offsets(source: &str) -> impl Iterator<Item = (usize, &str)> {
    let mut offset = 0usize;
    source.split_inclusive('\n').map(move |line| {
        let start = offset;
        offset += line.len();
        (start, line)
    })
}

#[derive(Clone, Copy)]
struct CodeFence {
    marker: char,
    len: usize,
}

impl CodeFence {
    fn opening(line: &str) -> Option<Self> {
        let marker = line.chars().next()?;
        if marker != '`' && marker != '~' {
            return None;
        }

        let len = line.chars().take_while(|&c| c == marker).count();
        (len >= 3).then_some(Self { marker, len })
    }

    fn is_closing_line(self, line: &str) -> bool {
        if !line.starts_with(self.marker) {
            return false;
        }

        let len = line.chars().take_while(|&c| c == self.marker).count();
        len >= self.len && line[len..].trim().is_empty()
    }
}

pub(crate) fn is_mdx_file(path: &Path) -> bool {
    path.extension()
        .and_then(|e| e.to_str())
        .is_some_and(|ext| ext == "mdx")
}

/// Parse an MDX file by extracting import/export statements.
pub(crate) fn parse_mdx_to_module(file_id: FileId, source: &str, content_hash: u64) -> ModuleInfo {
    let parsed_suppressions = crate::suppress::parse_suppressions_from_source(source);
    let line_offsets = fallow_types::extract::compute_line_offsets(source);
    let extraction = MdxExtractor.extract(source).into_iter().next();

    if let Some(statements) = extraction {
        let source_type = SourceType::jsx();
        let allocator = Allocator::default();
        let parser_return = Parser::new(&allocator, &statements.body, source_type).parse();
        let mut extractor = ModuleInfoExtractor::new();
        extractor.visit_program(&parser_return.program);
        extractor.remap_spans_with(|span| statements.remap_span(span));
        let mut info = extractor.into_module_info(file_id, content_hash, parsed_suppressions);
        info.line_offsets = line_offsets;
        return info;
    }

    let mut info =
        ModuleInfoExtractor::new().into_module_info(file_id, content_hash, parsed_suppressions);
    info.line_offsets = line_offsets;
    info
}

#[cfg(all(test, not(miri)))]
mod tests {
    use super::*;

    #[test]
    fn is_mdx_file_positive() {
        assert!(is_mdx_file(Path::new("post.mdx")));
    }

    #[test]
    fn is_mdx_file_rejects_md() {
        assert!(!is_mdx_file(Path::new("readme.md")));
    }

    #[test]
    fn is_mdx_file_rejects_tsx() {
        assert!(!is_mdx_file(Path::new("component.tsx")));
    }

    #[test]
    fn is_mdx_file_rejects_jsx() {
        assert!(!is_mdx_file(Path::new("component.jsx")));
    }

    #[test]
    fn extracts_single_import() {
        let result = extract_mdx_statements("import { Chart } from './Chart'\n\n# Title\n");
        assert!(result.contains("import { Chart } from './Chart'"));
    }

    #[test]
    fn extracts_default_import() {
        let result = extract_mdx_statements("import Button from './Button'\n\n# Title\n");
        assert!(result.contains("import Button from './Button'"));
    }

    #[test]
    fn extracts_multiple_imports() {
        let source = "import { A } from './a'\nimport { B } from './b'\n\n# Title\n";
        let result = extract_mdx_statements(source);
        assert!(result.contains("import { A } from './a'"));
        assert!(result.contains("import { B } from './b'"));
    }

    #[test]
    fn extracts_import_no_space() {
        let result = extract_mdx_statements("import{ Chart } from './Chart'\n\n# Title\n");
        assert!(result.contains("import{ Chart }"));
    }

    #[test]
    fn extracts_export_const() {
        let result = extract_mdx_statements("export const meta = { title: 'Hello' }\n\n# Title\n");
        assert!(result.contains("export const meta"));
    }

    #[test]
    fn multiline_export_const_with_object_literal() {
        let result = extract_mdx_statements(
            "export const meta = {\n  title: 'Hello',\n  draft: false\n};\n\n# Title\n",
        );
        assert!(result.contains("export const meta"));
        assert!(result.contains("title: 'Hello'"));
        assert!(!result.contains("# Title"));
    }

    #[test]
    fn multiline_export_const_closed_by_bare_brace() {
        let result =
            extract_mdx_statements("export const meta = {\n  title: 'Hello'\n}\n\n# Title\n");
        assert!(result.contains("export const meta"));
        assert!(result.contains("title: 'Hello'"));
        assert!(!result.contains("# Title"));
    }

    #[test]
    fn extracts_export_no_space() {
        let result = extract_mdx_statements("export{ foo } from './foo'\n\n# Title\n");
        assert!(result.contains("export{ foo }"));
    }

    #[test]
    fn multiline_import_with_braces() {
        let source =
            "import {\n  Chart,\n  Table,\n  Graph\n} from './components'\n\n# Dashboard\n";
        let result = extract_mdx_statements(source);
        assert!(result.contains("Chart"));
        assert!(result.contains("Table"));
        assert!(result.contains("Graph"));
        assert!(result.contains("from './components'"));
    }

    #[test]
    fn multiline_import_closed_by_from() {
        let source = "import {\n  Foo,\n  Bar\n} from './mod'\n\n# Content\n";
        let result = extract_mdx_statements(source);
        assert!(result.contains("Foo"));
        assert!(result.contains("Bar"));
    }

    #[test]
    fn imports_between_prose() {
        let source = "import { Header } from './Header'\n\n# Section 1\n\nSome content.\n\nimport { Footer } from './Footer'\n\n## Section 2\n";
        let result = extract_mdx_statements(source);
        assert!(result.contains("Header"));
        assert!(result.contains("Footer"));
    }

    #[test]
    fn prose_lines_excluded() {
        let source =
            "import { A } from './a'\n\n# Title\n\nSome **markdown** text.\n\n- List item\n";
        let result = extract_mdx_statements(source);
        assert!(!result.contains("Title"));
        assert!(!result.contains("markdown"));
        assert!(!result.contains("List item"));
    }

    #[test]
    fn fenced_import_is_ignored() {
        let source = r"import { Live } from './Live'

# Example

```ts
file: exampleSlice.ts
import exampleSliceReducer from './exampleSlice'
```
";
        let result = extract_mdx_statements(source);
        assert!(result.contains("import { Live } from './Live'"));
        assert!(!result.contains("./exampleSlice"));
        assert_eq!(result.lines().count(), 1);
    }

    #[test]
    fn fenced_export_is_ignored() {
        let source = r"# Example

```tsx
export const Example = () => null
```
";
        let result = extract_mdx_statements(source);
        assert!(result.is_empty());
    }

    #[test]
    fn tilde_fenced_import_is_ignored() {
        let source = r"import { Live } from './Live'

~~~ts
import virtual from './virtual'
~~~

export { Live }
";
        let result = extract_mdx_statements(source);
        assert!(result.contains("import { Live } from './Live'"));
        assert!(result.contains("export { Live }"));
        assert!(!result.contains("./virtual"));
        assert_eq!(result.lines().count(), 2);
    }

    #[test]
    fn longer_matching_fence_closes_code_block() {
        let source = r"````ts
import hidden from './hidden'
````

import { Visible } from './Visible'
";
        let result = extract_mdx_statements(source);
        assert!(!result.contains("./hidden"));
        assert!(result.contains("import { Visible } from './Visible'"));
    }

    #[test]
    fn shorter_matching_fence_does_not_close_code_block() {
        let source = r"````ts
import hidden from './hidden'
```
import stillHidden from './still-hidden'
````

import { Visible } from './Visible'
";
        let result = extract_mdx_statements(source);
        assert!(!result.contains("./hidden"));
        assert!(!result.contains("./still-hidden"));
        assert!(result.contains("import { Visible } from './Visible'"));
    }

    #[test]
    fn empty_source() {
        let result = extract_mdx_statements("");
        assert!(result.is_empty());
    }

    #[test]
    fn no_imports_or_exports() {
        let result = extract_mdx_statements("# Just Markdown\n\nNo imports here.\n");
        assert!(result.is_empty());
    }

    #[test]
    fn import_like_text_not_extracted() {
        let result = extract_mdx_statements("This is an important note.\n");
        assert!(result.is_empty());
    }

    #[test]
    fn export_like_text_not_extracted() {
        let result = extract_mdx_statements("We are exporting goods overseas.\n");
        assert!(result.is_empty());
    }

    #[test]
    fn side_effect_import() {
        let result = extract_mdx_statements("import './global.css'\n\n# Title\n");
        assert!(result.contains("import './global.css'"));
    }

    #[test]
    fn namespace_import() {
        let result = extract_mdx_statements("import * as utils from './utils'\n\n# Title\n");
        assert!(result.contains("import * as utils from './utils'"));
    }

    #[test]
    fn single_line_import_with_braces_balanced() {
        let source = "import { A } from './a'\n# Title\n";
        let result = extract_mdx_statements(source);
        assert_eq!(result.lines().count(), 1);
    }

    #[test]
    fn multiline_import_with_braces_extracted_as_one() {
        let source = "import {\n  Foo,\n  Bar\n} from './module'\n\n# Title\n";
        let result = extract_mdx_statements(source);
        assert!(result.contains("Foo"), "Foo should be in the result");
        assert!(result.contains("Bar"), "Bar should be in the result");
        assert!(
            result.contains("from './module'"),
            "from clause should be in the result"
        );
    }

    #[test]
    fn export_with_braces_from_module() {
        let source = "export { Foo, Bar } from './module'\n\n# Title\n";
        let result = extract_mdx_statements(source);
        assert!(result.contains("export { Foo, Bar } from './module'"));
    }

    #[test]
    fn non_import_lines_between_imports_ignored() {
        let source = "import { A } from './a'\n\n# Some heading\n\nA paragraph of text.\n\nimport { B } from './b'\n";
        let result = extract_mdx_statements(source);
        assert!(result.contains("import { A } from './a'"));
        assert!(result.contains("import { B } from './b'"));
        assert!(!result.contains("heading"), "prose should not be extracted");
        assert!(
            !result.contains("paragraph"),
            "prose should not be extracted"
        );
        assert_eq!(result.lines().count(), 2);
    }

    #[test]
    fn multiline_import_terminated_by_semicolon() {
        let source = "import {\n  Foo,\n  Bar\n};\n\n# Content\n";
        let result = extract_mdx_statements(source);
        assert!(result.contains("Foo"));
        assert!(result.contains("Bar"));
    }

    #[test]
    fn multiline_import_terminated_by_from_no_space_single_quote() {
        let source = "import {\n  Foo\n} from'./module'\n\n# Content\n";
        let result = extract_mdx_statements(source);
        assert!(result.contains("Foo"));
        assert!(result.contains("from'./module'"));
    }

    #[test]
    fn multiline_import_terminated_by_from_no_space_double_quote() {
        let source = "import {\n  Foo\n} from\"./module\"\n\n# Content\n";
        let result = extract_mdx_statements(source);
        assert!(result.contains("Foo"));
        assert!(result.contains("from\"./module\""));
    }

    #[test]
    fn multiline_export_with_braces() {
        let source = "export {\n  Foo,\n  Bar\n} from './module'\n\n# Content\n";
        let result = extract_mdx_statements(source);
        assert!(result.contains("Foo"));
        assert!(result.contains("Bar"));
        assert!(result.contains("from './module'"));
    }

    #[test]
    fn import_with_from_on_same_line_not_multiline() {
        let source = "import { A } from './a'\nimport { B } from './b'\n";
        let result = extract_mdx_statements(source);
        assert_eq!(result.lines().count(), 2);
    }

    #[test]
    fn mdx_empty_source_returns_empty_module() {
        let info = parse_mdx_to_module(fallow_types::discover::FileId(0), "", 0);
        assert!(info.imports.is_empty());
        assert!(info.exports.is_empty());
    }

    #[test]
    fn mdx_only_prose_returns_empty_module() {
        let info = parse_mdx_to_module(
            fallow_types::discover::FileId(0),
            "# Title\n\nSome text.\n",
            0,
        );
        assert!(info.imports.is_empty());
        assert!(info.exports.is_empty());
    }
}
