use std::path::Path;

use js_sys::Error;
use ruff_db::diagnostic;
use ruff_linter::preview::is_human_readable_names_enabled;
use ruff_linter::settings::types::PythonVersion;
use ruff_linter::suppression::Suppressions;
use serde::{Deserialize, Serialize};
use wasm_bindgen::prelude::*;

use ruff_formatter::printer::SourceMapGeneration;
use ruff_formatter::{FormatResult, Formatted, IndentStyle};
use ruff_linter::directives;
use ruff_linter::line_width::{IndentWidth, LineLength};
use ruff_linter::linter::check_path;
use ruff_linter::settings::{DEFAULT_SELECTORS, DUMMY_VARIABLE_RGX, flags};
use ruff_linter::source_kind::SourceKind;
use ruff_linter::{Locator, UnresolvedRuleSelector};
use ruff_python_ast::{Mod, PySourceType};
use ruff_python_codegen::Stylist;
use ruff_python_formatter::{PyFormatContext, QuoteStyle, format_module_ast, pretty_comments};
use ruff_python_index::Indexer;
use ruff_python_parser::{Mode, ParseOptions, Parsed, parse, parse_unchecked};
use ruff_python_trivia::TriviaRanges;
use ruff_ranged_value::{ValueSource, ValueSourceGuard};
use ruff_source_file::{OneIndexed, PositionEncoding as SourcePositionEncoding, SourceLocation};
use ruff_text_size::Ranged;
use ruff_workspace::Settings;
use ruff_workspace::configuration::Configuration;
use ruff_workspace::options::{FormatOptions, LintCommonOptions, LintOptions, Options};

#[wasm_bindgen(typescript_custom_section)]
const TYPES: &'static str = r#"
export interface Diagnostic {
    code: string | null;
    message: string;
    tags: DiagnosticTag[];
    annotations: DiagnosticAnnotation[];
    subDiagnostics: SubDiagnostic[];
    start_location: {
        row: number;
        column: number;
    };
    end_location: {
        row: number;
        column: number;
    };
    fix: {
        message: string | null;
        edits: {
            content: string | null;
            location: {
                row: number;
                column: number;
            };
            end_location: {
                row: number;
                column: number;
            };
        }[];
    } | null;
}

export type DiagnosticTag = "unnecessary" | "deprecated";

export interface DiagnosticAnnotation {
    primary: boolean;
    message: string | null;
    location: DiagnosticLocation | null;
}

export interface SubDiagnostic {
    severity: SubDiagnosticSeverity;
    message: string;
    location: DiagnosticLocation | null;
}

export enum SubDiagnosticSeverity {
    Help = "help",
    Info = "info",
    Warning = "warning",
    Error = "error",
    Fatal = "fatal",
}

export interface DiagnosticLocation {
    path: string;
    start_location: {
        row: number;
        column: number;
    };
    end_location: {
        row: number;
        column: number;
    };
}
"#;

#[derive(Serialize, Deserialize, Eq, PartialEq, Debug)]
pub struct ExpandedMessage {
    pub code: String,
    pub message: String,
    pub tags: Vec<ExpandedDiagnosticTag>,
    pub annotations: Vec<ExpandedDiagnosticAnnotation>,
    #[serde(rename = "subDiagnostics")]
    pub sub_diagnostics: Vec<ExpandedSubDiagnostic>,
    pub start_location: Location,
    pub end_location: Location,
    pub fix: Option<ExpandedFix>,
}

#[derive(Serialize, Deserialize, Eq, PartialEq, Debug)]
pub struct ExpandedDiagnosticAnnotation {
    pub primary: bool,
    pub message: Option<String>,
    pub location: Option<ExpandedDiagnosticLocation>,
}

#[derive(Serialize, Deserialize, Eq, PartialEq, Debug)]
pub struct ExpandedSubDiagnostic {
    pub severity: SubDiagnosticSeverity,
    pub message: String,
    pub location: Option<ExpandedDiagnosticLocation>,
}

#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Debug)]
#[serde(rename_all = "lowercase")]
pub enum SubDiagnosticSeverity {
    Help,
    Info,
    Warning,
    Error,
    Fatal,
}

impl From<diagnostic::SubDiagnosticSeverity> for SubDiagnosticSeverity {
    fn from(value: diagnostic::SubDiagnosticSeverity) -> Self {
        match value {
            diagnostic::SubDiagnosticSeverity::Help => Self::Help,
            diagnostic::SubDiagnosticSeverity::Info => Self::Info,
            diagnostic::SubDiagnosticSeverity::Warning => Self::Warning,
            diagnostic::SubDiagnosticSeverity::Error => Self::Error,
            diagnostic::SubDiagnosticSeverity::Fatal => Self::Fatal,
        }
    }
}

#[derive(Serialize, Deserialize, Eq, PartialEq, Debug)]
pub struct ExpandedDiagnosticLocation {
    pub path: String,
    pub start_location: Location,
    pub end_location: Location,
}

fn expanded_diagnostic_location(
    span: &diagnostic::Span,
    position_encoding: SourcePositionEncoding,
) -> Option<ExpandedDiagnosticLocation> {
    let source_file = span.as_ruff_file()?;
    let source_code = source_file.to_source_code();
    let range = span.range()?;

    Some(ExpandedDiagnosticLocation {
        path: source_file.name().to_string(),
        start_location: source_code
            .source_location(range.start(), position_encoding)
            .into(),
        end_location: source_code
            .source_location(range.end(), position_encoding)
            .into(),
    })
}

#[derive(Serialize, Deserialize, Eq, PartialEq, Debug)]
pub struct ExpandedFix {
    message: Option<String>,
    edits: Vec<ExpandedEdit>,
}

#[derive(Serialize, Deserialize, Eq, PartialEq, Debug)]
struct ExpandedEdit {
    location: Location,
    end_location: Location,
    content: Option<String>,
}

#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Debug)]
#[serde(rename_all = "lowercase")]
pub enum ExpandedDiagnosticTag {
    Unnecessary,
    Deprecated,
}

impl From<&diagnostic::DiagnosticTag> for ExpandedDiagnosticTag {
    fn from(value: &diagnostic::DiagnosticTag) -> Self {
        match value {
            diagnostic::DiagnosticTag::Unnecessary => Self::Unnecessary,
            diagnostic::DiagnosticTag::Deprecated => Self::Deprecated,
        }
    }
}

/// Perform global constructor initialization.
#[cfg(target_family = "wasm")]
#[expect(unsafe_code)]
pub fn before_main() {
    unsafe extern "C" {
        fn __wasm_call_ctors();
    }

    // Salsa uses the `inventory` crate, which registers global constructors that may need to be
    // called explicitly on WASM. See <https://github.com/dtolnay/inventory/blob/master/src/lib.rs#L105>
    // for details.
    unsafe {
        __wasm_call_ctors();
    }
}

#[cfg(not(target_family = "wasm"))]
pub fn before_main() {}

#[wasm_bindgen(start)]
pub fn run() {
    before_main();

    // When the `console_error_panic_hook` feature is enabled, we can call the
    // `set_panic_hook` function at least once during initialization, and then
    // we will get better error messages if our code ever panics.
    //
    // For more details see
    // https://github.com/rustwasm/console_error_panic_hook#readme
    #[cfg(feature = "console_error_panic_hook")]
    console_error_panic_hook::set_once();
}

/// Initializes the logger with the given log level.
///
/// ## Panics
/// If this function is called more than once.
#[wasm_bindgen(js_name = "initLogging")]
pub fn init_logging(level: LogLevel) {
    console_log::init_with_level(level.into())
        .expect("`initLogging` to only be called at most once.");
}

#[derive(Copy, Clone, Debug)]
#[wasm_bindgen]
pub enum LogLevel {
    Trace,
    Debug,
    Info,
    Warn,
    Error,
}

impl From<LogLevel> for log::Level {
    fn from(level: LogLevel) -> Self {
        match level {
            LogLevel::Trace => log::Level::Trace,
            LogLevel::Debug => log::Level::Debug,
            LogLevel::Info => log::Level::Info,
            LogLevel::Warn => log::Level::Warn,
            LogLevel::Error => log::Level::Error,
        }
    }
}

#[wasm_bindgen]
pub struct Workspace {
    settings: Settings,
    position_encoding: SourcePositionEncoding,
}

#[wasm_bindgen]
impl Workspace {
    pub fn version() -> String {
        ruff_linter::VERSION.to_string()
    }

    #[wasm_bindgen(constructor)]
    pub fn new(options: JsValue, position_encoding: PositionEncoding) -> Result<Workspace, Error> {
        let _guard = ValueSourceGuard::new(ValueSource::Cli, false);
        let options: Options = serde_wasm_bindgen::from_value(options).map_err(into_error)?;
        let configuration =
            Configuration::from_options(options, Some(Path::new(".")), Path::new("."))
                .map_err(into_error)?;
        let settings = configuration
            .into_settings(Path::new("."))
            .map_err(into_error)?;

        Ok(Workspace {
            settings,
            position_encoding: position_encoding.into(),
        })
    }

    #[wasm_bindgen(js_name = defaultSettings)]
    pub fn default_settings() -> Result<JsValue, Error> {
        serde_wasm_bindgen::to_value(&Options {
            preview: Some(false),

            // Propagate defaults.
            builtins: Some(Vec::default()),

            line_length: Some(LineLength::default()),

            indent_width: Some(IndentWidth::default()),
            target_version: Some(PythonVersion::default()),

            lint: Some(LintOptions {
                common: LintCommonOptions {
                    allowed_confusables: Some(Vec::default()),
                    dummy_variable_rgx: Some(DUMMY_VARIABLE_RGX.as_str().to_string()),
                    ignore: Some(Vec::default()),
                    select: Some(
                        DEFAULT_SELECTORS
                            .iter()
                            .map(|selector| {
                                let (prefix, code) = selector.prefix_and_code();
                                UnresolvedRuleSelector::cli(format!("{prefix}{code}"))
                            })
                            .collect(),
                    ),
                    extend_fixable: Some(Vec::default()),
                    extend_select: Some(Vec::default()),
                    external: Some(Vec::default()),
                    ..LintCommonOptions::default()
                },

                ..LintOptions::default()
            }),
            format: Some(FormatOptions {
                indent_style: Some(IndentStyle::Space),
                quote_style: Some(QuoteStyle::Double),
                ..FormatOptions::default()
            }),
            ..Options::default()
        })
        .map_err(into_error)
    }

    pub fn check(&self, contents: &str) -> Result<JsValue, Error> {
        let source_type = PySourceType::default();

        // TODO(dhruvmanila): Support Jupyter Notebooks
        let source_kind = SourceKind::Python {
            code: contents.to_string(),
            is_stub: source_type.is_stub(),
        };

        // Use the unresolved version because we don't have a file path.
        let target_version = self.settings.linter.unresolved_target_version;

        // Parse once.
        let options =
            ParseOptions::from(source_type).with_target_version(target_version.parser_version());
        let parsed = parse_unchecked(source_kind.source_code(), options)
            .try_into_module()
            .expect("`PySourceType` always parses to a `ModModule`.");

        // Map row and column locations to byte slices (lazily).
        let locator = Locator::new(contents);

        // Detect the current code style (lazily).
        let stylist = Stylist::from_tokens(parsed.tokens(), locator.contents());

        // Extra indices from the code.
        let indexer = Indexer::from_tokens(parsed.tokens(), locator.contents());

        // Extract the `# noqa` and `# isort: skip` directives from the source.
        let directives = directives::extract_directives(
            parsed.tokens(),
            directives::Flags::from_settings(&self.settings.linter),
            &locator,
            &indexer,
        );

        let suppressions = Suppressions::from_tokens(
            locator.contents(),
            parsed.tokens(),
            &indexer,
            &self.settings.linter,
        );

        // Generate checks.
        let diagnostics = check_path(
            Path::new("<filename>"),
            None,
            &locator,
            &stylist,
            &indexer,
            &directives,
            &self.settings.linter,
            flags::Noqa::Enabled,
            &source_kind,
            source_type,
            &parsed,
            target_version,
            &suppressions,
        );

        let source_code = locator.to_source_code();

        let messages: Vec<ExpandedMessage> = diagnostics
            .into_iter()
            .map(|msg| {
                let range = msg.range().unwrap_or_default();
                let annotations = msg
                    .annotations()
                    .iter()
                    .map(|annotation| ExpandedDiagnosticAnnotation {
                        primary: annotation.is_primary(),
                        message: annotation.get_message().map(ToOwned::to_owned),
                        location: expanded_diagnostic_location(
                            annotation.get_span(),
                            self.position_encoding,
                        ),
                    })
                    .collect();
                let sub_diagnostics = msg
                    .sub_diagnostics()
                    .iter()
                    .map(|sub_diagnostic| ExpandedSubDiagnostic {
                        severity: sub_diagnostic.severity().into(),
                        message: sub_diagnostic.concise_message().to_string(),
                        location: sub_diagnostic.primary_span_ref().and_then(|span| {
                            expanded_diagnostic_location(span, self.position_encoding)
                        }),
                    })
                    .collect();

                let code = if (!is_human_readable_names_enabled(self.settings.linter.preview)
                    || self.settings.output_prefer_rule_codes)
                    && let Some(code) = msg.secondary_code()
                {
                    code.as_str()
                } else {
                    msg.id().as_str()
                };

                ExpandedMessage {
                    code: code.to_string(),
                    message: msg.concise_message().to_string(),
                    tags: msg
                        .primary_tags()
                        .unwrap_or_default()
                        .iter()
                        .map(ExpandedDiagnosticTag::from)
                        .collect(),
                    annotations,
                    sub_diagnostics,
                    start_location: source_code
                        .source_location(range.start(), self.position_encoding)
                        .into(),
                    end_location: source_code
                        .source_location(range.end(), self.position_encoding)
                        .into(),
                    fix: msg.fix().map(|fix| ExpandedFix {
                        message: msg.first_help_text().map(ToString::to_string),
                        edits: fix
                            .edits()
                            .iter()
                            .map(|edit| ExpandedEdit {
                                location: source_code
                                    .source_location(edit.start(), self.position_encoding)
                                    .into(),
                                end_location: source_code
                                    .source_location(edit.end(), self.position_encoding)
                                    .into(),
                                content: edit.content().map(ToString::to_string),
                            })
                            .collect(),
                    }),
                }
            })
            .collect();

        messages
            .serialize(&serde_wasm_bindgen::Serializer::new().serialize_missing_as_null(true))
            .map_err(into_error)
    }

    pub fn format(&self, contents: &str) -> Result<String, Error> {
        let parsed = ParsedModule::from_source(contents)?;
        let formatted = parsed.format(&self.settings).map_err(into_error)?;
        let printed = formatted.print().map_err(into_error)?;

        Ok(printed.into_code())
    }

    pub fn format_ir(&self, contents: &str) -> Result<String, Error> {
        let parsed = ParsedModule::from_source(contents)?;
        let formatted = parsed.format(&self.settings).map_err(into_error)?;

        Ok(format!("{formatted}"))
    }

    pub fn comments(&self, contents: &str) -> Result<String, Error> {
        let parsed = ParsedModule::from_source(contents)?;
        let trivia_ranges = TriviaRanges::from(parsed.parsed.tokens());
        let comments = pretty_comments(parsed.parsed.syntax(), &trivia_ranges, contents);
        Ok(comments)
    }

    /// Parses the content and returns its AST
    pub fn parse(&self, contents: &str) -> Result<String, Error> {
        let parsed = parse_unchecked(contents, ParseOptions::from(Mode::Module));

        Ok(format!("{:#?}", parsed.into_syntax()))
    }

    pub fn tokens(&self, contents: &str) -> Result<String, Error> {
        let parsed = parse_unchecked(contents, ParseOptions::from(Mode::Module));

        Ok(format!("{:#?}", parsed.tokens().as_ref()))
    }
}

pub(crate) fn into_error<E: std::fmt::Display>(err: E) -> Error {
    Error::new(&err.to_string())
}

struct ParsedModule<'a> {
    source_code: &'a str,
    parsed: Parsed<Mod>,
    trivia_ranges: TriviaRanges,
}

impl<'a> ParsedModule<'a> {
    fn from_source(source_code: &'a str) -> Result<Self, Error> {
        let parsed = parse(source_code, ParseOptions::from(Mode::Module)).map_err(into_error)?;
        let trivia_ranges = TriviaRanges::from(parsed.tokens());
        Ok(Self {
            source_code,
            parsed,
            trivia_ranges,
        })
    }

    fn format(&self, settings: &Settings) -> FormatResult<Formatted<PyFormatContext<'_>>> {
        // TODO(konstin): Add an options for py/pyi to the UI (2/2)
        let options = settings
            .formatter
            .to_format_options(PySourceType::default(), self.source_code, None)
            .with_source_map_generation(SourceMapGeneration::Enabled);

        format_module_ast(&self.parsed, &self.trivia_ranges, self.source_code, options)
    }
}

#[derive(Serialize, Deserialize, Eq, PartialEq, Debug)]
pub struct Location {
    pub row: OneIndexed,
    /// The character offset from the start of the line.
    ///
    /// The semantic of the offset depends on the [`PositionEncoding`] used when creating
    /// the [`Workspace`].
    pub column: OneIndexed,
}

impl From<SourceLocation> for Location {
    fn from(value: SourceLocation) -> Self {
        Self {
            row: value.line,
            column: value.character_offset,
        }
    }
}

#[derive(Default, Copy, Clone)]
#[wasm_bindgen]
pub enum PositionEncoding {
    #[default]
    Utf8,
    Utf16,
    Utf32,
}

impl From<PositionEncoding> for SourcePositionEncoding {
    fn from(value: PositionEncoding) -> Self {
        match value {
            PositionEncoding::Utf8 => Self::Utf8,
            PositionEncoding::Utf16 => Self::Utf16,
            PositionEncoding::Utf32 => Self::Utf32,
        }
    }
}
