use crate::rule::{Fix, LintError, LintResult, LintWarning, Rule, RuleCategory, Severity};
use crate::utils::range_utils::calculate_line_range;

/// Rule MD040: Fenced code blocks should have a language
///
/// See [docs/md040.md](../../docs/md040.md) for full documentation, configuration, and examples.
/// Helper struct to track and update disable/enable state for a rule
struct DisableState {
    is_disabled: bool,
}

impl DisableState {
    fn new() -> Self {
        Self { is_disabled: false }
    }

    /// Update the disable state based on a line's content
    fn update(&mut self, line: &str, rule_name: &str) {
        // Check for disable comment
        if let Some(rules) = crate::rule::parse_disable_comment(line)
            && (rules.is_empty() || rules.contains(&rule_name))
        {
            self.is_disabled = true;
        }
        // Check for enable comment
        if let Some(rules) = crate::rule::parse_enable_comment(line)
            && (rules.is_empty() || rules.contains(&rule_name))
        {
            self.is_disabled = false;
        }
    }

    fn is_disabled(&self) -> bool {
        self.is_disabled
    }
}

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

impl Rule for MD040FencedCodeLanguage {
    fn name(&self) -> &'static str {
        "MD040"
    }

    fn description(&self) -> &'static str {
        "Code blocks should have a language specified"
    }

    fn check(&self, ctx: &crate::lint_context::LintContext) -> LintResult {
        let content = ctx.content;
        let _line_index = &ctx.line_index;

        let mut warnings = Vec::new();

        let mut in_code_block = false;
        let mut current_fence_marker: Option<String> = None;
        let mut opening_fence_indent: usize = 0;

        // Pre-compute disabled state to avoid O(n²) complexity
        let mut is_disabled = false;

        for (i, line) in content.lines().enumerate() {
            let trimmed = line.trim();

            // Update disabled state incrementally
            if let Some(rules) = crate::rule::parse_disable_comment(trimmed)
                && (rules.is_empty() || rules.contains(&self.name()))
            {
                is_disabled = true;
            }
            if let Some(rules) = crate::rule::parse_enable_comment(trimmed)
                && (rules.is_empty() || rules.contains(&self.name()))
            {
                is_disabled = false;
            }

            // Skip processing if rule is disabled
            if is_disabled {
                continue;
            }

            // Determine fence marker if this is a fence line
            let fence_marker = if trimmed.starts_with("```") {
                let backtick_count = trimmed.chars().take_while(|&c| c == '`').count();
                if backtick_count >= 3 {
                    Some("`".repeat(backtick_count))
                } else {
                    None
                }
            } else if trimmed.starts_with("~~~") {
                let tilde_count = trimmed.chars().take_while(|&c| c == '~').count();
                if tilde_count >= 3 {
                    Some("~".repeat(tilde_count))
                } else {
                    None
                }
            } else {
                None
            };

            if let Some(fence_marker) = fence_marker {
                if in_code_block {
                    // We're inside a code block, check if this closes it
                    if let Some(ref current_marker) = current_fence_marker {
                        let current_indent = line.len() - line.trim_start().len();
                        // Only close if the fence marker exactly matches the opening marker AND has no content after
                        // AND the indentation is not greater than the opening fence
                        if fence_marker == *current_marker
                            && trimmed[current_marker.len()..].trim().is_empty()
                            && current_indent <= opening_fence_indent
                        {
                            // This closes the current code block
                            in_code_block = false;
                            current_fence_marker = None;
                            opening_fence_indent = 0;
                        }
                        // else: This is content inside a code block, ignore completely
                    }
                } else {
                    // We're outside a code block, this opens one
                    // Check if language is specified
                    let after_fence = trimmed[fence_marker.len()..].trim();

                    // Check if it has MkDocs title attribute but no language
                    // Pattern: ``` title="Title" (missing language)
                    // Valid: ```python title="Title" or ```py title="Title"
                    let has_title_only =
                        ctx.flavor == crate::config::MarkdownFlavor::MkDocs && after_fence.starts_with("title=");

                    // Check for Quarto/RMarkdown code chunk syntax: {language} or {language, options}
                    // Examples: ```{python}, ```{r}, ```{r, echo=FALSE}
                    let has_quarto_syntax = ctx.flavor == crate::config::MarkdownFlavor::Quarto
                        && after_fence.starts_with('{')
                        && after_fence.contains('}');

                    if (after_fence.is_empty() || has_title_only) && !has_quarto_syntax {
                        // Calculate precise character range for the entire fence line that needs a language
                        let (start_line, start_col, end_line, end_col) = calculate_line_range(i + 1, line);

                        warnings.push(LintWarning {
                            rule_name: Some(self.name().to_string()),
                            line: start_line,
                            column: start_col,
                            end_line,
                            end_column: end_col,
                            message: "Code block (```) missing language".to_string(),
                            severity: Severity::Warning,
                            fix: Some(Fix {
                                range: {
                                    // Replace just the fence marker with fence+language
                                    let trimmed_start = line.len() - line.trim_start().len();
                                    let fence_len = fence_marker.len();
                                    let line_start_byte = ctx.line_offsets.get(i).copied().unwrap_or(0);
                                    let fence_start_byte = line_start_byte + trimmed_start;
                                    let fence_end_byte = fence_start_byte + fence_len;
                                    fence_start_byte..fence_end_byte
                                },
                                replacement: format!("{fence_marker}text"),
                            }),
                        });
                    }

                    in_code_block = true;
                    current_fence_marker = Some(fence_marker);
                    opening_fence_indent = line.len() - line.trim_start().len();
                }
            }
            // If we're inside a code block and this line is not a fence, ignore it
        }

        Ok(warnings)
    }

    fn fix(&self, ctx: &crate::lint_context::LintContext) -> Result<String, LintError> {
        let content = ctx.content;
        let _line_index = &ctx.line_index;

        let mut result = String::new();
        let mut in_code_block = false;
        let mut current_fence_marker: Option<String> = None;
        let mut fence_needs_language = false;
        let mut original_indent = String::new();
        let mut opening_fence_indent: usize = 0;

        let lines: Vec<&str> = content.lines().collect();

        // Pre-compute disabled state to avoid O(n²) complexity
        let mut disable_state = DisableState::new();

        for line in lines.iter() {
            let trimmed = line.trim();

            // Update disabled state incrementally
            disable_state.update(trimmed, self.name());

            // Skip processing if rule is disabled, preserve the line as-is
            if disable_state.is_disabled() {
                result.push_str(line);
                result.push('\n');
                continue;
            }

            // Determine fence marker if this is a fence line
            let fence_marker = if trimmed.starts_with("```") {
                let backtick_count = trimmed.chars().take_while(|&c| c == '`').count();
                if backtick_count >= 3 {
                    Some("`".repeat(backtick_count))
                } else {
                    None
                }
            } else if trimmed.starts_with("~~~") {
                let tilde_count = trimmed.chars().take_while(|&c| c == '~').count();
                if tilde_count >= 3 {
                    Some("~".repeat(tilde_count))
                } else {
                    None
                }
            } else {
                None
            };

            if let Some(fence_marker) = fence_marker {
                if in_code_block {
                    // We're inside a code block, check if this closes it
                    if let Some(ref current_marker) = current_fence_marker {
                        let current_indent = line.len() - line.trim_start().len();
                        if fence_marker == *current_marker
                            && trimmed[current_marker.len()..].trim().is_empty()
                            && current_indent <= opening_fence_indent
                        {
                            // This closes the current code block
                            if fence_needs_language {
                                // Use the same indentation as the opening fence
                                result.push_str(&format!("{original_indent}{trimmed}\n"));
                            } else {
                                // Preserve original line as-is
                                result.push_str(line);
                                result.push('\n');
                            }
                            in_code_block = false;
                            current_fence_marker = None;
                            fence_needs_language = false;
                            original_indent.clear();
                            opening_fence_indent = 0;
                        } else {
                            // This is content inside a code block (different fence marker) - preserve exactly as-is
                            result.push_str(line);
                            result.push('\n');
                        }
                    } else {
                        // This shouldn't happen, but preserve as content
                        result.push_str(line);
                        result.push('\n');
                    }
                } else {
                    // We're outside a code block, this opens one
                    // Capture the original indentation
                    let line_indent = line[..line.len() - line.trim_start().len()].to_string();

                    // Add 'text' as default language for opening fence if no language specified
                    let after_fence = trimmed[fence_marker.len()..].trim();

                    // Check if it has MkDocs title attribute but no language
                    let has_title_only =
                        ctx.flavor == crate::config::MarkdownFlavor::MkDocs && after_fence.starts_with("title=");

                    // Check for Quarto/RMarkdown code chunk syntax: {language} or {language, options}
                    let has_quarto_syntax = ctx.flavor == crate::config::MarkdownFlavor::Quarto
                        && after_fence.starts_with('{')
                        && after_fence.contains('}');

                    if (after_fence.is_empty() || has_title_only) && !has_quarto_syntax {
                        // Always preserve the original indentation - adding a language tag should not change indentation
                        original_indent = line_indent;
                        if has_title_only {
                            // Insert language before title attribute
                            result.push_str(&format!("{original_indent}{fence_marker}text {after_fence}\n"));
                        } else {
                            result.push_str(&format!("{original_indent}{fence_marker}text\n"));
                        }
                        fence_needs_language = true;
                    } else {
                        // Keep original line as-is since it already has a language
                        result.push_str(line);
                        result.push('\n');
                        fence_needs_language = false;
                    }

                    in_code_block = true;
                    current_fence_marker = Some(fence_marker);
                    opening_fence_indent = line.len() - line.trim_start().len();
                }
            } else if in_code_block {
                // We're inside a code block and this is not a fence line - preserve exactly as-is
                result.push_str(line);
                result.push('\n');
            } else {
                // We're outside code blocks and this is not a fence line - preserve as-is
                result.push_str(line);
                result.push('\n');
            }
        }

        // Remove trailing newline if the original content didn't have one
        if !content.ends_with('\n') {
            result.pop();
        }

        Ok(result)
    }

    /// Get the category of this rule for selective processing
    fn category(&self) -> RuleCategory {
        RuleCategory::CodeBlock
    }

    /// Check if this rule should be skipped
    fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
        ctx.content.is_empty() || (!ctx.likely_has_code() && !ctx.has_char('~'))
    }

    fn as_any(&self) -> &dyn std::any::Any {
        self
    }

    fn from_config(_config: &crate::config::Config) -> Box<dyn Rule>
    where
        Self: Sized,
    {
        Box::new(MD040FencedCodeLanguage)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::lint_context::LintContext;

    fn run_check(content: &str) -> LintResult {
        let rule = MD040FencedCodeLanguage;
        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
        rule.check(&ctx)
    }

    fn run_fix(content: &str) -> Result<String, LintError> {
        let rule = MD040FencedCodeLanguage;
        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
        rule.fix(&ctx)
    }

    #[test]
    fn test_code_blocks_with_language_specified() {
        // Basic test with language
        let content = r#"# Test

```python
print("Hello, world!")
```

```javascript
console.log("Hello!");
```
"#;
        let result = run_check(content).unwrap();
        assert!(result.is_empty(), "No warnings expected for code blocks with language");
    }

    #[test]
    fn test_code_blocks_without_language() {
        let content = r#"# Test

```
print("Hello, world!")
```
"#;
        let result = run_check(content).unwrap();
        assert_eq!(result.len(), 1);
        assert_eq!(result[0].message, "Code block (```) missing language");
        assert_eq!(result[0].line, 3);
    }

    #[test]
    fn test_code_blocks_with_empty_language() {
        // Test with spaces after the fence
        let content = r#"# Test

```
print("Hello, world!")
```
"#;
        let result = run_check(content).unwrap();
        assert_eq!(result.len(), 1);
        assert_eq!(result[0].message, "Code block (```) missing language");
    }

    #[test]
    fn test_indented_code_blocks_should_be_ignored() {
        // Indented code blocks (4 spaces) should not trigger the rule
        let content = r#"# Test

    This is an indented code block
    It should not trigger MD040
"#;
        let result = run_check(content).unwrap();
        assert!(result.is_empty(), "Indented code blocks should be ignored");
    }

    #[test]
    fn test_inline_code_spans_should_be_ignored() {
        let content = r#"# Test

This is `inline code` and should not trigger warnings.

Use the `print()` function.
"#;
        let result = run_check(content).unwrap();
        assert!(result.is_empty(), "Inline code spans should be ignored");
    }

    #[test]
    fn test_tildes_vs_backticks_for_fences() {
        // Test tilde fences without language
        let content_tildes_no_lang = r#"# Test

~~~
code here
~~~
"#;
        let result = run_check(content_tildes_no_lang).unwrap();
        assert_eq!(result.len(), 1);
        assert_eq!(result[0].message, "Code block (```) missing language");

        // Test tilde fences with language
        let content_tildes_with_lang = r#"# Test

~~~python
code here
~~~
"#;
        let result = run_check(content_tildes_with_lang).unwrap();
        assert!(result.is_empty());

        // Mixed fences
        let content_mixed = r#"# Test

```python
code here
```

~~~javascript
more code
~~~

```
no language
```

~~~
also no language
~~~
"#;
        let result = run_check(content_mixed).unwrap();
        assert_eq!(result.len(), 2);
    }

    #[test]
    fn test_language_with_additional_parameters() {
        let content = r#"# Test

```python {highlight=[1,2]}
print("Line 1")
print("Line 2")
```

```javascript {.line-numbers startFrom="10"}
console.log("Hello");
```

```ruby {data-line="1,3-4"}
puts "Hello"
puts "World"
puts "!"
```
"#;
        let result = run_check(content).unwrap();
        assert!(
            result.is_empty(),
            "Code blocks with language and parameters should pass"
        );
    }

    #[test]
    fn test_multiple_code_blocks_in_document() {
        let content = r#"# Test Document

First block without language:
```
code here
```

Second block with language:
```python
print("hello")
```

Third block without language:
```
more code
```

Fourth block with language:
```javascript
console.log("test");
```
"#;
        let result = run_check(content).unwrap();
        assert_eq!(result.len(), 2);
        assert_eq!(result[0].line, 4);
        assert_eq!(result[1].line, 14);
    }

    #[test]
    fn test_nested_code_blocks_in_lists() {
        let content = r#"# Test

- Item 1
  ```python
  print("nested with language")
  ```

- Item 2
  ```
  nested without language
  ```

- Item 3
  - Nested item
    ```javascript
    console.log("deeply nested");
    ```

  - Another nested
    ```
    no language
    ```
"#;
        let result = run_check(content).unwrap();
        assert_eq!(result.len(), 2);
        // Check that it detects the blocks without language
        assert_eq!(result[0].line, 9);
        assert_eq!(result[1].line, 20);
    }

    #[test]
    fn test_code_blocks_in_blockquotes() {
        let content = r#"# Test

> This is a blockquote
> ```python
> print("with language")
> ```

> Another blockquote
> ```
> without language
> ```
"#;
        let result = run_check(content).unwrap();
        // The implementation doesn't detect code blocks inside blockquotes
        // This is by design to avoid complexity with nested structures
        assert_eq!(result.len(), 0);
    }

    #[test]
    fn test_fix_method_adds_text_language() {
        let content = r#"# Test

```
code without language
```

```python
already has language
```

```
another block without
```
"#;
        let fixed = run_fix(content).unwrap();
        assert!(fixed.contains("```text"));
        assert!(fixed.contains("```python"));
        assert_eq!(fixed.matches("```text").count(), 2);
    }

    #[test]
    fn test_fix_preserves_indentation() {
        let content = r#"# Test

- List item
  ```
  indented code block
  ```
"#;
        let fixed = run_fix(content).unwrap();
        // Should preserve indentation for list items
        assert!(fixed.contains("  ```text"));
        assert!(fixed.contains("  indented code block"));
    }

    #[test]
    fn test_fix_preserves_indentation_numbered_list() {
        // Test case from issue #122
        let content = r#"1. Step 1

    ```
    foo
    bar
    ```
"#;
        let fixed = run_fix(content).unwrap();
        // Should preserve 4-space indentation for numbered list content
        assert!(fixed.contains("    ```text"));
        assert!(fixed.contains("    foo"));
        assert!(fixed.contains("    bar"));
        // Should not remove indentation
        assert!(!fixed.contains("\n```text\n"));
    }

    #[test]
    fn test_fix_preserves_all_indentation() {
        let content = r#"# Test

Top-level code block:
```
top level
```

1. List item

    ```
    nested in list
    ```

Indented by 2 spaces:
  ```
  content
  ```
"#;
        let fixed = run_fix(content).unwrap();

        // All indentation should be preserved exactly as-is
        assert!(
            fixed.contains("```text\ntop level"),
            "Top-level code block indentation preserved"
        );
        assert!(
            fixed.contains("    ```text\n    nested in list"),
            "List item code block indentation preserved"
        );
        assert!(
            fixed.contains("  ```text\n  content"),
            "2-space indented code block indentation preserved"
        );
    }

    #[test]
    fn test_fix_with_tilde_fences() {
        let content = r#"# Test

~~~
code with tildes
~~~
"#;
        let fixed = run_fix(content).unwrap();
        assert!(fixed.contains("~~~text"));
    }

    #[test]
    fn test_longer_fence_markers() {
        let content = r#"# Test

````
code with four backticks
````

`````python
code with five backticks and language
`````

~~~~~~
code with six tildes
~~~~~~
"#;
        let result = run_check(content).unwrap();
        assert_eq!(result.len(), 2);

        let fixed = run_fix(content).unwrap();
        assert!(fixed.contains("````text"));
        assert!(fixed.contains("~~~~~~text"));
        assert!(fixed.contains("`````python"));
    }

    #[test]
    fn test_nested_code_blocks_different_markers() {
        let content = r#"# Test

````markdown
This is a markdown block

```python
# This is nested code
print("hello")
```

More markdown
````
"#;
        let result = run_check(content).unwrap();
        assert!(
            result.is_empty(),
            "Nested code blocks with different markers should not trigger warnings"
        );
    }

    #[test]
    fn test_disable_enable_comments() {
        let content = r#"# Test

<!-- rumdl-disable MD040 -->
```
this should not trigger warning
```
<!-- rumdl-enable MD040 -->

```
this should trigger warning
```
"#;
        let result = run_check(content).unwrap();
        assert_eq!(result.len(), 1);
        assert_eq!(result[0].line, 9);
    }

    #[test]
    fn test_fence_with_language_only_on_closing() {
        // Edge case: language on closing fence should not be interpreted
        let content = r#"# Test

```
code
```python
"#;
        let result = run_check(content).unwrap();
        assert_eq!(result.len(), 1);
    }

    #[test]
    fn test_incomplete_code_blocks() {
        // Test unclosed code block
        let content = r#"# Test

```python
this code block is not closed"#;
        let result = run_check(content).unwrap();
        assert!(
            result.is_empty(),
            "Unclosed code blocks with language should not trigger warnings"
        );

        // Test unclosed code block without language
        let content_no_lang = r#"# Test

```
this code block is not closed"#;
        let result = run_check(content_no_lang).unwrap();
        assert_eq!(result.len(), 1);
    }

    #[test]
    fn test_fix_preserves_original_formatting() {
        let content = r#"# Test

```
code
```

No newline at end"#;
        let fixed = run_fix(content).unwrap();
        assert!(!fixed.ends_with('\n'), "Fix should preserve lack of trailing newline");

        let content_with_newline = "# Test\n\n```\ncode\n```\n";
        let fixed = run_fix(content_with_newline).unwrap();
        assert!(fixed.ends_with('\n'), "Fix should preserve trailing newline");
    }

    #[test]
    fn test_edge_case_backticks_in_content() {
        let content = r#"# Test

```javascript
console.log(`template string with backticks`);
// This line has ``` in a comment
```
"#;
        let result = run_check(content).unwrap();
        assert!(
            result.is_empty(),
            "Backticks inside code blocks should not affect parsing"
        );
    }

    #[test]
    fn test_empty_document() {
        let content = "";
        let result = run_check(content).unwrap();
        assert!(result.is_empty());
    }

    #[test]
    fn test_should_skip_optimization() {
        let rule = MD040FencedCodeLanguage;

        // Document without code fences should skip
        let ctx = LintContext::new("# Just a header\n\nSome text", crate::config::MarkdownFlavor::Standard);
        assert!(rule.should_skip(&ctx));

        // Document with backtick fences should not skip
        let ctx = LintContext::new("```\ncode\n```", crate::config::MarkdownFlavor::Standard);
        assert!(!rule.should_skip(&ctx));

        // Document with tilde fences should not skip
        let ctx = LintContext::new("~~~\ncode\n~~~", crate::config::MarkdownFlavor::Standard);
        assert!(!rule.should_skip(&ctx));

        // Empty document should skip
        let ctx = LintContext::new("", crate::config::MarkdownFlavor::Standard);
        assert!(rule.should_skip(&ctx));
    }

    #[test]
    fn test_quarto_code_chunk_syntax() {
        let rule = MD040FencedCodeLanguage;

        // Test Quarto {r} syntax - should NOT trigger warning
        let content = r#"# Test

```{r}
x <- 1
```

```{python}
x = 1
```

```{r, echo=FALSE}
plot(x)
```
"#;
        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Quarto);
        let result = rule.check(&ctx).unwrap();
        assert!(
            result.is_empty(),
            "Quarto code chunks with {{language}} syntax should not trigger warnings"
        );

        // Test that missing language DOES trigger warning for Quarto
        let content_no_lang = r#"# Test

```
code without language
```
"#;
        let ctx = LintContext::new(content_no_lang, crate::config::MarkdownFlavor::Quarto);
        let result = rule.check(&ctx).unwrap();
        assert_eq!(result.len(), 1, "Quarto files without language should trigger warning");

        // Test that standard flavor still requires standard language syntax
        let content_standard = r#"# Test

```{python}
code
```
"#;
        let ctx = LintContext::new(content_standard, crate::config::MarkdownFlavor::Standard);
        let result = rule.check(&ctx).unwrap();
        // In standard flavor, {python} is considered "after_fence" content, so it's valid
        // The fence marker is "```" and after_fence is "{python}", which is non-empty
        assert!(
            result.is_empty(),
            "Standard flavor should accept any non-empty after_fence content"
        );
    }
}
