use std::ffi::{OsStr, OsString};
use std::future::Future;
use std::path::Path;
use std::pin::Pin;
use std::str::FromStr;
use std::sync::Arc;

use anyhow::Result;
use prek_consts::env_vars::{EnvVars, EnvVarsRead};
use prek_identify::parse_shebang;
use tracing::{Instrument, trace, trace_span};

use crate::cli::reporter::HookInstallReporter;
use crate::cli::run::HookRunReporter;
use crate::config::Language;
use crate::fs::CWD;
use crate::hook::{Hook, InstallInfo, InstalledHook, Repo};
use crate::hooks;
use crate::store::{CacheBucket, Store, ToolBucket};

mod bun;
mod conda;
mod coursier;
mod dart;
mod deno;
mod docker;
mod docker_image;
mod dotnet;
mod fail;
mod golang;
mod haskell;
mod julia;
mod lua;
mod node;
mod perl;
mod php;
mod pygrep;
mod python;
mod r;
mod ruby;
mod rust;
mod script;
mod swift;
mod system;
pub(crate) mod version;

// Backend futures are awaited in place rather than spawned. Requiring `Send` here would impose a
// stronger contract than callers need and rejects the borrowed async closures used by backends.
#[async_trait::async_trait(?Send)]
trait LanguageBackend: Sync {
    async fn install(
        &self,
        store: &Store,
        hook: Arc<Hook>,
        reporter: &HookInstallReporter,
    ) -> Result<InstalledHook>;

    async fn check_health(&self, info: &InstallInfo) -> Result<()>;

    async fn run(
        &self,
        store: &Store,
        hook: &InstalledHook,
        filenames: &[&Path],
        reporter: &HookRunReporter,
    ) -> Result<(i32, Vec<u8>)>;
}

type LanguageFuture<'a, T> = Pin<Box<dyn Future<Output = Result<T>> + 'a>>;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ShellSupport {
    Supported,
    Unsupported(&'static str),
}

// `pre-commit` language support:
// bun: install requested version, support env, support additional deps
// conda: only system version, support env, support additional deps
// coursier: only system version, support env, support additional deps
// dart: only system version, support env, support additional deps
// docker_image: only system version, no env, no additional deps
// docker: only system version, support env, no additional deps
// dotnet: install requested version, support env, support additional deps
// fail: only system version, no env, no additional deps
// golang: install requested version, support env, support additional deps
// haskell: only system version, support env, support additional deps
// lua: only system version, support env, support additional deps
// node: install requested version, support env, support additional deps (delegated to nodeenv)
// perl: only system version, support env, support additional deps
// php: only system version, support env, support additional deps
// pygrep: only system version, no env, no additional deps
// python: install requested version, support env, support additional deps (delegated to virtualenv)
// r: only system version, support env, support additional deps
// ruby: install requested version, support env, support additional deps (delegated to rbenv)
// rust: install requested version, support env, support additional deps (delegated to rustup and cargo)
// script: only system version, no env, no additional deps
// swift: only system version, support env, no additional deps
// system: only system version, no env, no additional deps

impl Language {
    fn backend(self) -> &'static dyn LanguageBackend {
        match self {
            Self::Bun => &bun::Bun,
            Self::Conda => &conda::Conda,
            Self::Coursier => &coursier::Coursier,
            Self::Dart => &dart::Dart,
            Self::Deno => &deno::Deno,
            Self::Docker => &docker::Docker,
            Self::DockerImage => &docker_image::DockerImage,
            Self::Dotnet => &dotnet::Dotnet,
            Self::Fail => &fail::Fail,
            Self::Golang => &golang::Golang,
            Self::Haskell => &haskell::Haskell,
            Self::Julia => &julia::Julia,
            Self::Lua => &lua::Lua,
            Self::Node => &node::Node,
            Self::Perl => &perl::Perl,
            Self::Php => &php::Php,
            Self::Pygrep => &pygrep::Pygrep,
            Self::Python => &python::Python,
            Self::R => &r::R,
            Self::Ruby => &ruby::Ruby,
            Self::Rust => &rust::Rust,
            Self::Script => &script::Script,
            Self::Swift => &swift::Swift,
            Self::System => &system::System,
        }
    }

    pub(crate) fn supports_install_env(self) -> bool {
        match self {
            Self::Bun
            | Self::Conda
            | Self::Coursier
            | Self::Dart
            | Self::Deno
            | Self::Docker
            | Self::Dotnet
            | Self::Golang
            | Self::Haskell
            | Self::Julia
            | Self::Lua
            | Self::Node
            | Self::Perl
            | Self::Php
            | Self::Pygrep
            | Self::Python
            | Self::R
            | Self::Ruby
            | Self::Rust
            | Self::Swift => true,
            Self::DockerImage | Self::Fail | Self::Script | Self::System => false,
        }
    }

    pub(crate) fn shell_support(self) -> ShellSupport {
        match self {
            Self::Bun
            | Self::Conda
            | Self::Coursier
            | Self::Deno
            | Self::Dotnet
            | Self::Golang
            | Self::Haskell
            | Self::Lua
            | Self::Node
            | Self::Perl
            | Self::Php
            | Self::Python
            | Self::Ruby
            | Self::Script
            | Self::Swift
            | Self::System => ShellSupport::Supported,
            Self::R => ShellSupport::Unsupported(
                "`entry` must use the R backend's `Rscript -e <expr>` or `Rscript <file>` forms",
            ),
            Self::Dart => ShellSupport::Unsupported(
                "`--packages` injection requires the resolved argv to contain `dart` directly",
            ),
            Self::Docker | Self::DockerImage => ShellSupport::Unsupported(
                "`entry` participates in container image or entrypoint selection",
            ),
            Self::Fail => ShellSupport::Unsupported("`entry` is the failure message body"),
            Self::Julia | Self::Rust => ShellSupport::Unsupported(
                "`entry` participates in install/runtime package resolution and is split before execution",
            ),
            Self::Pygrep => ShellSupport::Unsupported("`entry` is the regex pattern"),
        }
    }

    pub(crate) fn tool_buckets(self) -> &'static [ToolBucket] {
        match self {
            Self::Bun => &[ToolBucket::Bun],
            Self::Deno => &[ToolBucket::Deno],
            Self::Dotnet => &[ToolBucket::Dotnet],
            Self::Golang => &[ToolBucket::Go],
            Self::Node => &[ToolBucket::Node],
            Self::Python | Self::Pygrep => &[ToolBucket::Uv, ToolBucket::Python],
            Self::Ruby => &[ToolBucket::Ruby],
            Self::Rust => &[ToolBucket::Rustup],
            Self::Conda
            | Self::Coursier
            | Self::Dart
            | Self::Docker
            | Self::DockerImage
            | Self::Fail
            | Self::Haskell
            | Self::Julia
            | Self::Lua
            | Self::Perl
            | Self::Php
            | Self::R
            | Self::Script
            | Self::Swift
            | Self::System => &[],
        }
    }

    pub(crate) fn cache_buckets(self) -> &'static [CacheBucket] {
        match self {
            Self::Coursier => &[CacheBucket::Coursier],
            Self::Deno => &[CacheBucket::Deno],
            Self::Golang => &[CacheBucket::Go],
            Self::Node => &[CacheBucket::Npm],
            Self::Python | Self::Pygrep => &[CacheBucket::Uv, CacheBucket::Python],
            Self::Rust => &[CacheBucket::Cargo],
            Self::Bun
            | Self::Conda
            | Self::Dart
            | Self::Docker
            | Self::DockerImage
            | Self::Dotnet
            | Self::Fail
            | Self::Haskell
            | Self::Julia
            | Self::Lua
            | Self::Perl
            | Self::Php
            | Self::R
            | Self::Ruby
            | Self::Script
            | Self::Swift
            | Self::System => &[],
        }
    }

    /// Return whether the language allows specifying the version, e.g. we can install a specific
    /// requested language version.
    /// See <https://pre-commit.com/#overriding-language-version>
    pub(crate) fn supports_language_version(self) -> bool {
        match self {
            Self::Bun
            | Self::Deno
            | Self::Dotnet
            | Self::Golang
            | Self::Node
            | Self::Python
            | Self::Ruby
            | Self::Rust => true,
            Self::Conda
            | Self::Coursier
            | Self::Dart
            | Self::Docker
            | Self::DockerImage
            | Self::Fail
            | Self::Haskell
            | Self::Julia
            | Self::Lua
            | Self::Perl
            | Self::Php
            | Self::Pygrep
            | Self::R
            | Self::Script
            | Self::Swift
            | Self::System => false,
        }
    }

    /// Whether the language supports installing dependencies.
    ///
    /// For example, Python and Node.js support installing dependencies, while
    /// System and Fail do not.
    pub(crate) fn supports_dependency(self) -> bool {
        match self {
            Self::Bun
            | Self::Conda
            | Self::Coursier
            | Self::Dart
            | Self::Deno
            | Self::Dotnet
            | Self::Golang
            | Self::Haskell
            | Self::Julia
            | Self::Lua
            | Self::Node
            | Self::Perl
            | Self::Php
            | Self::Python
            | Self::R
            | Self::Ruby
            | Self::Rust => true,
            Self::Docker
            | Self::DockerImage
            | Self::Fail
            | Self::Pygrep
            | Self::Script
            | Self::Swift
            | Self::System => false,
        }
    }

    pub(crate) fn install<'a>(
        &'a self,
        store: &'a Store,
        hook: Arc<Hook>,
        reporter: &'a HookInstallReporter,
    ) -> LanguageFuture<'a, InstalledHook> {
        self.backend().install(store, hook, reporter)
    }

    pub(crate) fn check_health<'a>(&'a self, info: &'a InstallInfo) -> LanguageFuture<'a, ()> {
        self.backend().check_health(info)
    }

    pub(crate) fn run<'a, 'p>(
        &'a self,
        store: &'a Store,
        hook: &'a InstalledHook,
        filenames: &'a [&'p Path],
        reporter: &'a HookRunReporter,
    ) -> impl Future<Output = Result<(i32, Vec<u8>)>> + 'a
    where
        'p: 'a,
    {
        let future: LanguageFuture<'a, (i32, Vec<u8>)> = match hook.repo() {
            Repo::Meta { .. } => Box::pin(
                hooks::MetaHooks::from_str(&hook.id)
                    .unwrap()
                    .run(store, hook, filenames, reporter),
            ),
            Repo::Builtin { .. } => Box::pin(
                hooks::BuiltinHooks::from_str(&hook.id)
                    .unwrap()
                    .run(store, hook, filenames, reporter),
            ),
            // Fast path for hooks implemented in Rust
            Repo::Remote { .. } if hooks::check_fast_path(hook) => {
                Box::pin(hooks::run_fast_path(store, hook, filenames, reporter))
            }
            Repo::Remote { .. } | Repo::Local { .. } => {
                self.backend().run(store, hook, filenames, reporter)
            }
        };

        future.instrument(trace_span!(
            "run",
            hook_id = %hook.id,
            language = %hook.language,
        ))
    }
}

/// Try to extract metadata from the given hook.
pub(crate) async fn extract_metadata(hook: &mut Hook) -> Result<()> {
    match hook.language {
        Language::Python => python::extract_metadata(hook).await,
        Language::Golang => golang::extract_go_mod_metadata(hook).await,
        Language::Bun
        | Language::Conda
        | Language::Coursier
        | Language::Dart
        | Language::Deno
        | Language::Docker
        | Language::DockerImage
        | Language::Dotnet
        | Language::Fail
        | Language::Haskell
        | Language::Julia
        | Language::Lua
        | Language::Node
        | Language::Perl
        | Language::Php
        | Language::Pygrep
        | Language::R
        | Language::Ruby
        | Language::Rust
        | Language::Script
        | Language::Swift
        | Language::System => Ok(()),
    }
}

/// Resolve the actual process invocation, honoring shebangs and PATH lookups.
pub(crate) fn resolve_command(mut cmds: Vec<OsString>, paths: Option<&OsStr>) -> Vec<OsString> {
    let Some(candidate) = cmds.first() else {
        return cmds;
    };

    let env_path = if paths.is_none() {
        EnvVars.var_os(EnvVars::PATH)
    } else {
        None
    };
    let paths = paths.or(env_path.as_deref());

    let resolved_binary =
        which::which_in(candidate, paths, &*CWD).unwrap_or_else(|_| Path::new(candidate).into());

    let Ok(shebang_argv) = parse_shebang(&resolved_binary) else {
        cmds[0] = resolved_binary.into_os_string();
        return cmds;
    };

    let mut shebang_argv = shebang_argv
        .into_iter()
        .map(OsString::from)
        .collect::<Vec<_>>();
    trace!("Found shebang: {:?}", shebang_argv);
    let interpreter = shebang_argv[0].as_os_str();
    #[cfg(windows)]
    let interpreter = {
        let interpreter_path = Path::new(interpreter);
        if !interpreter_path.exists()
            && (interpreter_path.has_root() || interpreter_path.components().count() > 1)
        {
            // Git for Windows behavior: if a shebang points to a Unix-style absolute
            // interpreter path (e.g. `/bin/sh`) that does not exist on Windows,
            // fall back to PATH lookup of its basename (`sh`).
            interpreter_path.file_name().unwrap_or(interpreter)
        } else {
            interpreter
        }
    };

    // Resolve the interpreter path, converting "python3" to "python3.exe" on Windows.
    if let Ok(path) = which::which_in(interpreter, paths, &*CWD) {
        shebang_argv[0] = path.into_os_string();
    }
    shebang_argv.push(resolved_binary.into_os_string());
    shebang_argv.extend(cmds.drain(1..));
    shebang_argv
}

#[cfg(test)]
mod tests {
    #[cfg(unix)]
    use std::ffi::OsStr;
    use std::ffi::OsString;
    #[cfg(unix)]
    use std::os::unix::ffi::OsStringExt;
    use std::path::Path;

    use tempfile::tempdir;

    use super::resolve_command;
    use crate::fs::make_executable;

    fn write_file(path: &Path, contents: &str) {
        fs_err::write(path, contents).expect("write test file");
    }

    #[test]
    fn resolve_command_passthrough_when_not_found() {
        let cmd = OsString::from("__prek_nonexistent_command__");
        let resolved = resolve_command(vec![cmd.clone()], None);
        assert_eq!(resolved, vec![cmd]);
    }

    #[test]
    fn resolve_command_passthrough_when_empty() {
        assert_eq!(resolve_command(Vec::new(), None), Vec::<OsString>::new());
    }

    #[cfg(unix)]
    #[test]
    fn resolve_command_preserves_non_utf8_arguments() {
        let cmd = OsString::from("__prek_nonexistent_command__");
        let arg = OsString::from_vec(vec![b'f', b'o', 0x80]);

        assert_eq!(
            resolve_command(vec![cmd.clone(), arg.clone()], Some(OsStr::new("")),),
            vec![cmd, arg]
        );
    }

    #[test]
    fn resolve_command_resolves_shebang_and_preserves_arguments() {
        let dir = tempdir().expect("create temp dir");
        let script_path = dir.path().join("hook-script");
        write_file(
            &script_path,
            "#!/usr/bin/env -S prek-test-interpreter --from-shebang\necho hi\n",
        );

        #[cfg(windows)]
        let interpreter_path = dir.path().join("prek-test-interpreter.exe");
        #[cfg(not(windows))]
        let interpreter_path = dir.path().join("prek-test-interpreter");

        write_file(&interpreter_path, "");
        make_executable(&interpreter_path).expect("set executable bit");

        let paths = OsString::from(dir.path().as_os_str());
        let script = script_path.into_os_string();
        let resolved = resolve_command(
            vec![script.clone(), OsString::from("--from-entry")],
            Some(paths.as_os_str()),
        );

        assert_eq!(
            resolved,
            vec![
                interpreter_path.into_os_string(),
                OsString::from("--from-shebang"),
                script,
                OsString::from("--from-entry"),
            ]
        );
    }

    #[cfg(windows)]
    #[test]
    fn resolve_command_windows_rewrites_bin_sh_to_path_sh() {
        let dir = tempdir().expect("create temp dir");
        let script_path = dir.path().join("legacy-hook");
        write_file(&script_path, "#!/bin/sh\necho legacy\n");

        let sh_path = dir.path().join("sh.exe");
        write_file(&sh_path, "");

        let paths = OsString::from(dir.path().as_os_str());
        let resolved = resolve_command(
            vec![script_path.as_os_str().to_owned()],
            Some(paths.as_os_str()),
        );

        assert_eq!(resolved[0].as_os_str(), sh_path.as_os_str());
        assert_eq!(resolved[1].as_os_str(), script_path.as_os_str());
    }

    #[cfg(windows)]
    #[test]
    fn resolve_command_windows_keeps_existing_absolute_interpreter_path() {
        let dir = tempdir().expect("create temp dir");

        let interp_dir = dir.path().join("bin");
        fs_err::create_dir_all(&interp_dir).expect("create interpreter dir");
        let interp_path = interp_dir.join("sh.exe");
        write_file(&interp_path, "");
        let shebang_interpreter = interp_path.to_string_lossy().replace('\\', "/");

        let script_path = dir.path().join("legacy-hook");
        write_file(
            &script_path,
            &format!("#!{shebang_interpreter}\necho legacy\n"),
        );

        let paths = OsString::from(dir.path().as_os_str());
        let resolved = resolve_command(
            vec![script_path.as_os_str().to_owned()],
            Some(paths.as_os_str()),
        );

        let resolved_interp = Path::new(&resolved[0]);
        assert_eq!(resolved_interp, interp_path.as_path());
        assert_eq!(resolved[1].as_os_str(), script_path.as_os_str());
    }
}
