use std::{
    borrow::Cow,
    ffi::OsStr,
    fs,
    path::{Path, PathBuf},
    rc::Rc,
    sync::Arc,
};

use rayon::{
    iter::{IntoParallelRefIterator, ParallelBridge},
    prelude::ParallelIterator,
};
use rustc_hash::FxHashSet;

use oxc_allocator::Allocator;
use oxc_diagnostics::{DiagnosticSender, DiagnosticService, Error, OxcDiagnostic};
use oxc_parser::{ParseOptions, Parser};
use oxc_resolver::Resolver;
use oxc_semantic::SemanticBuilder;
use oxc_span::{SourceType, VALID_EXTENSIONS};

use crate::{
    Fixer, Linter, Message,
    loader::{JavaScriptSource, LINT_PARTIAL_LOADER_EXT, PartialLoader},
    module_record::ModuleRecord,
    utils::read_to_string,
};

use super::{
    LintServiceOptions,
    module_cache::{ModuleCache, ModuleState},
};

pub struct Runtime {
    cwd: Box<Path>,
    /// All paths to lint
    paths: FxHashSet<Box<Path>>,
    pub(super) linter: Linter,
    resolver: Option<Resolver>,
    modules: ModuleCache,
}

impl Runtime {
    pub(super) fn new(linter: Linter, options: LintServiceOptions) -> Self {
        let resolver = options.cross_module.then(|| {
            Self::get_resolver(options.tsconfig.or_else(|| Some(options.cwd.join("tsconfig.json"))))
        });
        Self {
            cwd: options.cwd,
            paths: options.paths.iter().cloned().collect(),
            linter,
            resolver,
            modules: ModuleCache::default(),
        }
    }

    fn get_resolver(tsconfig_path: Option<PathBuf>) -> Resolver {
        use oxc_resolver::{ResolveOptions, TsconfigOptions, TsconfigReferences};
        let tsconfig = tsconfig_path.and_then(|path| {
            path.is_file().then_some(TsconfigOptions {
                config_file: path,
                references: TsconfigReferences::Auto,
            })
        });
        let extension_alias = tsconfig.as_ref().map_or_else(Vec::new, |_| {
            vec![
                (".js".into(), vec![".js".into(), ".ts".into()]),
                (".mjs".into(), vec![".mjs".into(), ".mts".into()]),
                (".cjs".into(), vec![".cjs".into(), ".cts".into()]),
            ]
        });
        Resolver::new(ResolveOptions {
            extensions: VALID_EXTENSIONS.iter().map(|ext| format!(".{ext}")).collect(),
            main_fields: vec!["module".into(), "main".into()],
            condition_names: vec!["module".into(), "import".into()],
            extension_alias,
            tsconfig,
            ..ResolveOptions::default()
        })
    }

    fn get_source_type_and_text(
        path: &Path,
        ext: &str,
    ) -> Option<Result<(SourceType, String), Error>> {
        let source_type = SourceType::from_path(path);
        let not_supported_yet =
            source_type.as_ref().is_err_and(|_| !LINT_PARTIAL_LOADER_EXT.contains(&ext));
        if not_supported_yet {
            return None;
        }
        let source_type = source_type.unwrap_or_default();
        let file_result = read_to_string(path).map_err(|e| {
            Error::new(OxcDiagnostic::error(format!(
                "Failed to open file {path:?} with error \"{e}\""
            )))
        });
        Some(match file_result {
            Ok(source_text) => Ok((source_type, source_text)),
            Err(e) => Err(e),
        })
    }

    // clippy: the source field is checked and assumed to be less than 4GB, and
    // we assume that the fix offset will not exceed 2GB in either direction
    #[expect(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
    pub(super) fn process_path(&self, path: &Path, tx_error: &DiagnosticSender) {
        if self.init_cache_state(path) {
            return;
        }

        let Some(ext) = path.extension().and_then(OsStr::to_str) else {
            self.ignore_path(path);
            return;
        };

        let Some(source_type_and_text) = Self::get_source_type_and_text(path, ext) else {
            self.ignore_path(path);
            return;
        };

        let (source_type, source_text) = match source_type_and_text {
            Ok(source_text) => source_text,
            Err(e) => {
                self.ignore_path(path);
                tx_error.send(Some((path.to_path_buf(), vec![e]))).unwrap();
                return;
            }
        };

        let sources = PartialLoader::parse(ext, &source_text)
            .unwrap_or_else(|| vec![JavaScriptSource::partial(&source_text, source_type, 0)]);

        if sources.is_empty() {
            self.ignore_path(path);
            return;
        }

        // If there are fixes, we will accumulate all of them and write to the file at the end.
        // This means we do not write multiple times to the same file if there are multiple sources
        // in the same file (for example, multiple scripts in an `.astro` file).
        let mut new_source_text = Cow::from(&source_text);
        // This is used to keep track of the cumulative offset from applying fixes.
        // Otherwise, spans for fixes will be incorrect due to varying size of the
        // source code after each fix.
        let mut fix_offset: i32 = 0;

        let mut allocator = Allocator::default();
        for (i, source) in sources.into_iter().enumerate() {
            if i >= 1 {
                allocator.reset();
            }
            let mut messages = self.process_source(
                path,
                &allocator,
                source.source_text,
                source.source_type,
                true,
                tx_error,
            );

            if self.linter.options().fix.is_some() {
                let fix_result = Fixer::new(source.source_text, messages).fix();
                if fix_result.fixed {
                    // write to file, replacing only the changed part
                    let start = source.start.saturating_add_signed(fix_offset) as usize;
                    let end = start + source.source_text.len();
                    new_source_text.to_mut().replace_range(start..end, &fix_result.fixed_code);
                    let old_code_len = source.source_text.len() as u32;
                    let new_code_len = fix_result.fixed_code.len() as u32;
                    fix_offset += new_code_len as i32;
                    fix_offset -= old_code_len as i32;
                }
                messages = fix_result.messages;
            }

            if !messages.is_empty() {
                self.ignore_path(path);
                let errors = messages.into_iter().map(Into::into).collect();
                let path = path.strip_prefix(&self.cwd).unwrap_or(path);
                let diagnostics =
                    DiagnosticService::wrap_diagnostics(path, &source_text, source.start, errors);
                tx_error.send(Some(diagnostics)).unwrap();
            }
        }

        // If the new source text is owned, that means it was modified,
        // so we write the new source text to the file.
        if let Cow::Owned(new_source_text) = new_source_text {
            fs::write(path, new_source_text).unwrap();
        }
    }

    pub(super) fn process_source<'a>(
        &self,
        path: &Path,
        allocator: &'a Allocator,
        source_text: &'a str,
        source_type: SourceType,
        check_syntax_errors: bool,
        tx_error: &DiagnosticSender,
    ) -> Vec<Message<'a>> {
        let ret = Parser::new(allocator, source_text, source_type)
            .with_options(ParseOptions {
                parse_regular_expression: true,
                allow_return_outside_function: true,
                ..ParseOptions::default()
            })
            .parse();

        if !ret.errors.is_empty() {
            if self.resolver.is_some() {
                self.modules.add_resolved_module(path, Arc::new(ModuleRecord::default()));
            }
            return if ret.is_flow_language {
                vec![]
            } else {
                ret.errors.into_iter().map(|err| Message::new(err, None)).collect()
            };
        };

        let semantic_ret = SemanticBuilder::new()
            .with_cfg(true)
            .with_scope_tree_child_ids(true)
            .with_build_jsdoc(true)
            .with_check_syntax_error(check_syntax_errors)
            .build(allocator.alloc(ret.program));

        if !semantic_ret.errors.is_empty() {
            return semantic_ret.errors.into_iter().map(|err| Message::new(err, None)).collect();
        };

        let mut semantic = semantic_ret.semantic;
        semantic.set_irregular_whitespaces(ret.irregular_whitespaces);

        let module_record = Arc::new(ModuleRecord::new(path, &ret.module_record, &semantic));

        // If import plugin is enabled.
        if self.resolver.is_some() {
            self.modules.add_resolved_module(path, Arc::clone(&module_record));
            // Retrieve all dependent modules from this module.
            let dir = path.parent().unwrap();
            module_record
                .requested_modules
                .keys()
                .par_bridge()
                .map_with(self.resolver.as_ref().unwrap(), |resolver, specifier| {
                    resolver.resolve(dir, specifier).ok().map(|r| (specifier, r))
                })
                .flatten()
                .for_each_with(tx_error, |tx_error, (specifier, resolution)| {
                    let path = resolution.path();
                    self.process_path(path, tx_error);
                    // Append target_module to loaded_modules
                    if let Some(ModuleState::Resolved(target_module_record)) =
                        self.modules.get(path)
                    {
                        module_record
                            .loaded_modules
                            .write()
                            .unwrap()
                            .insert(specifier.clone(), Arc::clone(&target_module_record));
                    };
                });

            // The thread is blocked here until all dependent modules are resolved.

            // Stop if the current module is not marked for lint.
            if !self.paths.contains(path) {
                return vec![];
            }
        }

        self.linter.run(path, Rc::new(semantic), Arc::clone(&module_record))
    }

    pub(super) fn init_cache_state(&self, path: &Path) -> bool {
        if self.resolver.is_none() {
            return false;
        }
        self.modules.init_cache_state(path)
    }

    fn ignore_path(&self, path: &Path) {
        self.resolver.is_some().then(|| self.modules.ignore_path(path));
    }

    pub(super) fn number_of_dependencies(&self) -> usize {
        self.modules.len() - self.paths.len()
    }

    pub(super) fn par_iter_paths(&self) -> impl ParallelIterator<Item = &Box<Path>> {
        self.paths.par_iter()
    }
}