use std::io::Write;
use std::path::Path;
use std::process::{Command, Stdio};

use anyhow::Context;

use ruff_formatter::{FormatOptions, PrintedRange};
use ruff_markdown::{MarkdownResult, format_code_blocks};
use ruff_python_ast::{PySourceType, SourceType};
use ruff_python_formatter::{FormatModuleError, PyFormatOptions, format_module_source};
use ruff_source_file::LineIndex;
use ruff_text_size::TextRange;
use ruff_workspace::FormatterSettings;

use crate::edit::TextDocument;

/// The backend to use for formatting.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Deserialize)]
#[serde(rename_all = "lowercase")]
pub(crate) enum FormatBackend {
    /// Use the built-in Ruff formatter.
    ///
    /// The formatter version will match the LSP version.
    #[default]
    Internal,
    /// Use uv for formatting.
    ///
    /// The formatter version may differ from the LSP version.
    Uv,
}

#[derive(Debug, PartialEq, Eq)]
pub(crate) enum FormatResult {
    Formatted(String),
    Unchanged,
    PreviewOnly { file_format: &'static str },
}

impl FormatResult {
    fn into_formatted(self) -> Option<String> {
        match self {
            Self::Formatted(formatted) => Some(formatted),
            Self::Unchanged | Self::PreviewOnly { .. } => None,
        }
    }
}

pub(crate) fn format(
    document: &TextDocument,
    source_type: SourceType,
    formatter_settings: &FormatterSettings,
    path: &Path,
    backend: FormatBackend,
) -> crate::Result<FormatResult> {
    match backend {
        FormatBackend::Uv => format_external(document, source_type, formatter_settings, path),
        FormatBackend::Internal => format_internal(document, source_type, formatter_settings, path),
    }
}

/// Format using the built-in Ruff formatter.
fn format_internal(
    document: &TextDocument,
    source_type: SourceType,
    formatter_settings: &FormatterSettings,
    path: &Path,
) -> crate::Result<FormatResult> {
    match source_type {
        SourceType::Python(py_source_type) => {
            let format_options = formatter_settings.to_format_options(
                py_source_type,
                document.contents(),
                Some(path),
            );
            match format_module_source(document.contents(), format_options) {
                Ok(formatted) => {
                    let formatted = formatted.into_code();
                    if formatted == document.contents() {
                        Ok(FormatResult::Unchanged)
                    } else {
                        Ok(FormatResult::Formatted(formatted))
                    }
                }
                // Special case - syntax/parse errors are handled here instead of
                // being propagated as visible server errors.
                Err(FormatModuleError::ParseError(error)) => {
                    tracing::warn!("Unable to format document: {error}");
                    Ok(FormatResult::Unchanged)
                }
                Err(err) => Err(err.into()),
            }
        }
        SourceType::Markdown => {
            if !formatter_settings.preview.is_enabled() {
                tracing::warn!("Markdown formatting is experimental, enable preview mode.");
                return Ok(FormatResult::PreviewOnly {
                    file_format: "Markdown",
                });
            }

            match format_code_blocks(document.contents(), Some(path), formatter_settings) {
                MarkdownResult::Formatted(formatted) => Ok(FormatResult::Formatted(formatted)),
                MarkdownResult::Unchanged => Ok(FormatResult::Unchanged),
            }
        }
        SourceType::Toml(_) => {
            tracing::warn!("Formatting TOML files is not supported");
            Ok(FormatResult::Unchanged)
        }
    }
}

/// Format using an external uv command.
fn format_external(
    document: &TextDocument,
    source_type: SourceType,
    formatter_settings: &FormatterSettings,
    path: &Path,
) -> crate::Result<FormatResult> {
    let format_options = match source_type {
        SourceType::Python(py_source_type) => {
            formatter_settings.to_format_options(py_source_type, document.contents(), Some(path))
        }
        SourceType::Markdown => formatter_settings.to_format_options(
            PySourceType::Python,
            document.contents(),
            Some(path),
        ),
        SourceType::Toml(_) => {
            tracing::warn!("Formatting TOML files not supported");
            return Ok(FormatResult::Unchanged);
        }
    };
    let uv_command = UvFormatCommand::from(format_options);
    uv_command.format_document(document.contents(), path)
}

pub(crate) fn format_range(
    document: &TextDocument,
    source_type: SourceType,
    formatter_settings: &FormatterSettings,
    range: TextRange,
    path: &Path,
    backend: FormatBackend,
) -> crate::Result<Option<PrintedRange>> {
    let py_source_type = match source_type {
        SourceType::Python(py_source_type) => py_source_type,
        SourceType::Markdown => {
            tracing::warn!("Range formatting for Markdown files not supported");
            return Ok(None);
        }
        SourceType::Toml(_) => {
            tracing::warn!("Formatting TOML files not supported");
            return Ok(None);
        }
    };
    match backend {
        FormatBackend::Uv => {
            format_range_external(document, py_source_type, formatter_settings, range, path)
        }
        FormatBackend::Internal => {
            format_range_internal(document, py_source_type, formatter_settings, range, path)
        }
    }
}

/// Format range using the built-in Ruff formatter
fn format_range_internal(
    document: &TextDocument,
    source_type: PySourceType,
    formatter_settings: &FormatterSettings,
    range: TextRange,
    path: &Path,
) -> crate::Result<Option<PrintedRange>> {
    let format_options =
        formatter_settings.to_format_options(source_type, document.contents(), Some(path));

    match ruff_python_formatter::format_range(document.contents(), range, format_options) {
        Ok(formatted) => {
            if formatted.as_code() == document.contents() {
                Ok(None)
            } else {
                Ok(Some(formatted))
            }
        }
        // Special case - syntax/parse errors are handled here instead of
        // being propagated as visible server errors.
        Err(FormatModuleError::ParseError(error)) => {
            tracing::warn!("Unable to format document range: {error}");
            Ok(None)
        }
        Err(err) => Err(err.into()),
    }
}

/// Format range using an external command, i.e., `uv`.
fn format_range_external(
    document: &TextDocument,
    source_type: PySourceType,
    formatter_settings: &FormatterSettings,
    range: TextRange,
    path: &Path,
) -> crate::Result<Option<PrintedRange>> {
    let format_options =
        formatter_settings.to_format_options(source_type, document.contents(), Some(path));
    let uv_command = UvFormatCommand::from(format_options);

    // Format the range using uv and convert the result to `PrintedRange`
    Ok(uv_command
        .format_range(document.contents(), range, path, document.index())?
        .into_formatted()
        .map(|formatted| PrintedRange::new(formatted, range)))
}

/// Builder for uv format commands
#[derive(Debug)]
pub(crate) struct UvFormatCommand {
    options: PyFormatOptions,
}

impl From<PyFormatOptions> for UvFormatCommand {
    fn from(options: PyFormatOptions) -> Self {
        Self { options }
    }
}

impl UvFormatCommand {
    /// Build the command with all necessary arguments
    fn build_command(
        &self,
        path: &Path,
        range_with_index: Option<(TextRange, &LineIndex, &str)>,
    ) -> Command {
        let mut command = Command::new("uv");
        command.arg("format");
        command.arg("--");

        let target_version = format!(
            "py{}{}",
            self.options.target_version().major,
            self.options.target_version().minor
        );

        // Add only the formatting options that the CLI supports
        command.arg("--target-version");
        command.arg(&target_version);

        command.arg("--line-length");
        command.arg(self.options.line_width().to_string());

        if self.options.preview().is_enabled() {
            command.arg("--preview");
        }

        // Pass other formatting options via --config
        command.arg("--config");
        command.arg(format!(
            "format.indent-style = '{}'",
            self.options.indent_style()
        ));

        command.arg("--config");
        command.arg(format!("indent-width = {}", self.options.indent_width()));

        command.arg("--config");
        command.arg(format!(
            "format.quote-style = '{}'",
            self.options.quote_style()
        ));

        command.arg("--config");
        command.arg(format!(
            "format.line-ending = '{}'",
            self.options.line_ending().as_setting_str()
        ));

        command.arg("--config");
        command.arg(format!(
            "format.skip-magic-trailing-comma = {}",
            match self.options.magic_trailing_comma() {
                ruff_python_formatter::MagicTrailingComma::Respect => "false",
                ruff_python_formatter::MagicTrailingComma::Ignore => "true",
            }
        ));

        if let Some((range, line_index, source)) = range_with_index {
            // The CLI expects line:column format
            let start_pos = line_index.line_column(range.start(), source);
            let end_pos = line_index.line_column(range.end(), source);
            let range_str = format!(
                "{}:{}-{}:{}",
                start_pos.line.get(),
                start_pos.column.get(),
                end_pos.line.get(),
                end_pos.column.get()
            );
            command.arg("--range");
            command.arg(&range_str);
        }

        command.arg("--stdin-filename");
        command.arg(path.to_string_lossy().as_ref());

        command.stdin(Stdio::piped());
        command.stdout(Stdio::piped());
        command.stderr(Stdio::piped());

        command
    }

    /// Execute the format command on the given source.
    pub(crate) fn format(
        &self,
        source: &str,
        path: &Path,
        range_with_index: Option<(TextRange, &LineIndex)>,
    ) -> crate::Result<FormatResult> {
        let mut command =
            self.build_command(path, range_with_index.map(|(r, idx)| (r, idx, source)));
        let mut child = match command.spawn() {
            Ok(child) => child,
            Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
                anyhow::bail!("uv was not found; is it installed and on the PATH?")
            }
            Err(err) => return Err(err).context("Failed to spawn uv"),
        };

        let mut stdin = child
            .stdin
            .take()
            .context("Failed to get stdin from format subprocess")?;
        stdin
            .write_all(source.as_bytes())
            .context("Failed to write to stdin")?;
        drop(stdin);

        let result = child
            .wait_with_output()
            .context("Failed to get output from format subprocess")?;

        if !result.status.success() {
            let stderr = String::from_utf8_lossy(&result.stderr);
            // We don't propagate format errors due to invalid syntax
            if stderr.contains("Failed to parse") {
                tracing::warn!("Unable to format document: {}", stderr);
                return Ok(FormatResult::Unchanged);
            }
            // Special-case for when `uv format` is not available
            if stderr.contains("unrecognized subcommand 'format'") {
                anyhow::bail!(
                    "The installed version of uv does not support `uv format`; upgrade to a newer version"
                );
            }
            anyhow::bail!("Failed to format document: {stderr}");
        }

        let formatted = String::from_utf8(result.stdout)
            .context("Failed to parse stdout from format subprocess as utf-8")?;

        if formatted == source {
            Ok(FormatResult::Unchanged)
        } else {
            Ok(FormatResult::Formatted(formatted))
        }
    }

    /// Format the entire document.
    pub(crate) fn format_document(&self, source: &str, path: &Path) -> crate::Result<FormatResult> {
        self.format(source, path, None)
    }

    /// Format a specific range.
    pub(crate) fn format_range(
        &self,
        source: &str,
        range: TextRange,
        path: &Path,
        line_index: &LineIndex,
    ) -> crate::Result<FormatResult> {
        self.format(source, path, Some((range, line_index)))
    }
}

#[cfg(test)]
mod tests {
    use std::path::Path;

    use insta::assert_snapshot;
    use ruff_linter::settings::types::{CompiledPerFileTargetVersionList, PerFileTargetVersion};
    use ruff_python_ast::{PySourceType, PythonVersion, SourceType};
    use ruff_text_size::{TextRange, TextSize};
    use ruff_workspace::FormatterSettings;

    use crate::TextDocument;
    use crate::format::{FormatBackend, FormatResult, format, format_range};

    fn expect_formatted(result: FormatResult) -> String {
        result
            .into_formatted()
            .expect("Expected formatting changes")
    }

    #[test]
    fn format_per_file_version() {
        let document = TextDocument::new(r#"
with open("a_really_long_foo") as foo, open("a_really_long_bar") as bar, open("a_really_long_baz") as baz:
    pass
"#.to_string(), 0);
        let per_file_target_version =
            CompiledPerFileTargetVersionList::resolve(vec![PerFileTargetVersion::new(
                "test.py".to_string(),
                PythonVersion::PY310,
                Some(Path::new(".")),
            )])
            .unwrap();
        let result = format(
            &document,
            SourceType::Python(PySourceType::Python),
            &FormatterSettings {
                unresolved_target_version: PythonVersion::PY38,
                per_file_target_version,
                ..Default::default()
            },
            Path::new("test.py"),
            FormatBackend::Internal,
        )
        .expect("Expected no errors when formatting");

        let result = expect_formatted(result);

        assert_snapshot!(result, @r#"
        with (
            open("a_really_long_foo") as foo,
            open("a_really_long_bar") as bar,
            open("a_really_long_baz") as baz,
        ):
            pass
        "#);

        // same as above but without the per_file_target_version override
        let result = format(
            &document,
            SourceType::Python(PySourceType::Python),
            &FormatterSettings {
                unresolved_target_version: PythonVersion::PY38,
                ..Default::default()
            },
            Path::new("test.py"),
            FormatBackend::Internal,
        )
        .expect("Expected no errors when formatting");

        let result = expect_formatted(result);

        assert_snapshot!(result, @r#"
        with open("a_really_long_foo") as foo, open("a_really_long_bar") as bar, open(
            "a_really_long_baz"
        ) as baz:
            pass
        "#);
    }

    #[test]
    fn format_per_file_version_range() -> anyhow::Result<()> {
        // prepare a document with formatting changes before and after the intended range (the
        // context manager)
        let document = TextDocument::new(r#"
def fn(x: str) -> Foo | Bar: return foobar(x)

with open("a_really_long_foo") as foo, open("a_really_long_bar") as bar, open("a_really_long_baz") as baz:
    pass

sys.exit(
1
)
"#.to_string(), 0);

        let start = document.contents().find("with").unwrap();
        let end = document.contents().find("pass").unwrap() + "pass".len();
        let range = TextRange::new(TextSize::try_from(start)?, TextSize::try_from(end)?);

        let per_file_target_version =
            CompiledPerFileTargetVersionList::resolve(vec![PerFileTargetVersion::new(
                "test.py".to_string(),
                PythonVersion::PY310,
                Some(Path::new(".")),
            )])
            .unwrap();
        let result = format_range(
            &document,
            SourceType::Python(PySourceType::Python),
            &FormatterSettings {
                unresolved_target_version: PythonVersion::PY38,
                per_file_target_version,
                ..Default::default()
            },
            range,
            Path::new("test.py"),
            FormatBackend::Internal,
        )
        .expect("Expected no errors when formatting")
        .expect("Expected formatting changes");

        assert_snapshot!(result.as_code(), @r#"
        with (
            open("a_really_long_foo") as foo,
            open("a_really_long_bar") as bar,
            open("a_really_long_baz") as baz,
        ):
            pass
        "#);

        // same as above but without the per_file_target_version override
        let result = format_range(
            &document,
            SourceType::Python(PySourceType::Python),
            &FormatterSettings {
                unresolved_target_version: PythonVersion::PY38,
                ..Default::default()
            },
            range,
            Path::new("test.py"),
            FormatBackend::Internal,
        )
        .expect("Expected no errors when formatting")
        .expect("Expected formatting changes");

        assert_snapshot!(result.as_code(), @r#"
        with open("a_really_long_foo") as foo, open("a_really_long_bar") as bar, open(
            "a_really_long_baz"
        ) as baz:
            pass
        "#);

        Ok(())
    }

    #[cfg(feature = "test-uv")]
    mod uv_tests {
        use super::*;

        #[test]
        fn test_uv_format_document() {
            let document = TextDocument::new(
                r#"
def hello(  x,y ,z  ):
    return x+y  +z


def world(  ):
    pass
"#
                .to_string(),
                0,
            );

            let result = format(
                &document,
                SourceType::Python(PySourceType::Python),
                &FormatterSettings::default(),
                Path::new("test.py"),
                FormatBackend::Uv,
            )
            .expect("Expected no errors when formatting with uv");

            let result = expect_formatted(result);

            // uv should format this to a consistent style
            assert_snapshot!(result, @r#"
            def hello(x, y, z):
                return x + y + z


            def world():
                pass
            "#);
        }

        #[test]
        fn test_uv_format_range() -> anyhow::Result<()> {
            let document = TextDocument::new(
                r#"
def messy_function(  a,  b,c   ):
    return a+b+c

def another_function(x,y,z):
    result=x+y+z
    return result
"#
                .to_string(),
                0,
            );

            // Find the range of the second function
            let start = document.contents().find("def another_function").unwrap();
            let end = document.contents().find("return result").unwrap() + "return result".len();
            let range = TextRange::new(TextSize::try_from(start)?, TextSize::try_from(end)?);

            let result = format_range(
                &document,
                SourceType::Python(PySourceType::Python),
                &FormatterSettings::default(),
                range,
                Path::new("test.py"),
                FormatBackend::Uv,
            )
            .expect("Expected no errors when formatting range with uv")
            .expect("Expected formatting changes");

            assert_snapshot!(result.as_code(), @r#"
            def messy_function(  a,  b,c   ):
                return a+b+c

            def another_function(x, y, z):
                result = x + y + z
                return result
            "#);

            Ok(())
        }

        #[test]
        fn test_uv_format_with_line_length() {
            use ruff_formatter::LineWidth;

            let document = TextDocument::new(
                r#"
def hello(very_long_parameter_name_1, very_long_parameter_name_2, very_long_parameter_name_3):
    return very_long_parameter_name_1 + very_long_parameter_name_2 + very_long_parameter_name_3
"#
                .to_string(),
                0,
            );

            // Test with shorter line length
            let formatter_settings = FormatterSettings {
                line_width: LineWidth::try_from(60).unwrap(),
                ..Default::default()
            };

            let result = format(
                &document,
                SourceType::Python(PySourceType::Python),
                &formatter_settings,
                Path::new("test.py"),
                FormatBackend::Uv,
            )
            .expect("Expected no errors when formatting with uv");

            let result = expect_formatted(result);

            // With line length 60, the function should be wrapped
            assert_snapshot!(result, @r#"
            def hello(
                very_long_parameter_name_1,
                very_long_parameter_name_2,
                very_long_parameter_name_3,
            ):
                return (
                    very_long_parameter_name_1
                    + very_long_parameter_name_2
                    + very_long_parameter_name_3
                )
            "#);
        }

        #[test]
        fn test_uv_format_with_indent_style() {
            use ruff_formatter::IndentStyle;

            let document = TextDocument::new(
                r#"
def hello():
    if True:
        print("Hello")
        if False:
            print("World")
"#
                .to_string(),
                0,
            );

            // Test with tabs instead of spaces
            let formatter_settings = FormatterSettings {
                indent_style: IndentStyle::Tab,
                ..Default::default()
            };

            let result = format(
                &document,
                SourceType::Python(PySourceType::Python),
                &formatter_settings,
                Path::new("test.py"),
                FormatBackend::Uv,
            )
            .expect("Expected no errors when formatting with uv");

            let result = expect_formatted(result);

            // Should have formatting changes (spaces to tabs)
            assert_snapshot!(result, @r#"
            def hello():
            	if True:
            		print("Hello")
            		if False:
            			print("World")
            "#);
        }

        #[test]
        fn test_uv_format_syntax_error() {
            let document = TextDocument::new(
                r#"
def broken(:
    pass
"#
                .to_string(),
                0,
            );

            let result = format(
                &document,
                SourceType::Python(PySourceType::Python),
                &FormatterSettings::default(),
                Path::new("test.py"),
                FormatBackend::Uv,
            )
            .expect("Expected no errors from format function");

            assert_eq!(
                result,
                FormatResult::Unchanged,
                "Expected unchanged for syntax error"
            );
        }

        #[test]
        fn test_uv_format_with_quote_style() {
            use ruff_python_formatter::QuoteStyle;

            let document = TextDocument::new(
                r#"
x = "hello"
y = 'world'
z = '''multi
line'''
"#
                .to_string(),
                0,
            );

            // Test with single quotes
            let formatter_settings = FormatterSettings {
                quote_style: QuoteStyle::Single,
                ..Default::default()
            };

            let result = format(
                &document,
                SourceType::Python(PySourceType::Python),
                &formatter_settings,
                Path::new("test.py"),
                FormatBackend::Uv,
            )
            .expect("Expected no errors when formatting with uv");

            let result = expect_formatted(result);

            assert_snapshot!(result, @r#"
            x = 'hello'
            y = 'world'
            z = """multi
            line"""
            "#);
        }

        #[test]
        fn test_uv_format_with_magic_trailing_comma() {
            use ruff_python_formatter::MagicTrailingComma;

            let document = TextDocument::new(
                r#"
foo = [
    1,
    2,
    3,
]

bar = [1, 2, 3,]
"#
                .to_string(),
                0,
            );

            // Test with ignore magic trailing comma
            let formatter_settings = FormatterSettings {
                magic_trailing_comma: MagicTrailingComma::Ignore,
                ..Default::default()
            };

            let result = format(
                &document,
                SourceType::Python(PySourceType::Python),
                &formatter_settings,
                Path::new("test.py"),
                FormatBackend::Uv,
            )
            .expect("Expected no errors when formatting with uv");

            let result = expect_formatted(result);

            assert_snapshot!(result, @r#"
            foo = [1, 2, 3]

            bar = [1, 2, 3]
            "#);
        }
    }
}
