use std::{
    env,
    fs::File,
    io::copy,
    path::{Path, PathBuf},
};

use anyhow::{Context, Result, bail};
use reqwest::blocking::Client;
use serde::Deserialize;
use tempfile::TempDir;
use xshell::{Shell, cmd};

use crate::{progress::Progress, r_version::ResolvedRVersion, util::emit_command_output};

const QUARTO_DOWNLOAD_INFO_URL: &str = "https://quarto.org/docs/download/_download.json";

/// Ensures the requested R toolchain is installed system-wide.
pub fn install_r(shell: &Shell, version: &ResolvedRVersion, progress: &Progress) -> Result<()> {
    let check_task = progress.task(format!(
        "Checking existing R {} installation",
        version.version
    ));
    let r_already_installed = is_r_already_installed(shell, version)?;
    if r_already_installed {
        check_task.finish_with_message(format!("Using existing R {}", version.version));
    } else {
        check_task.finish_with_message(format!("R {} not detected; installing", version.version));

        let download_task = progress.task(format!("Downloading R {} installer", version.version));
        let installer = match download_installer(version) {
            Ok(installer) => {
                let file_name = installer
                    .path()
                    .file_name()
                    .and_then(|name| name.to_str())
                    .unwrap_or("installer.deb");
                download_task
                    .finish_with_message(format!("Downloaded R {} ({file_name})", version.version));
                installer
            }
            Err(err) => {
                download_task.fail(format!("Download of R {} failed", version.version));
                return Err(err);
            }
        };

        install_prerequisites(shell, progress).context("failed to install R prerequisites")?;
        install_from_deb(shell, installer.path(), progress)
            .with_context(|| format!("failed to install {}", installer.path().display()))?;
        configure_symlinks(shell, version, progress).context("failed to configure R symlinks")?;

        progress.println(format!("R {} installation completed", version.version));
    }

    ensure_quarto(shell, progress).context("failed to provision Quarto")?;
    ensure_pandoc(shell, progress).context("failed to provision pandoc")?;
    ensure_tinytex(shell, progress).context("failed to provision TinyTeX")?;

    Ok(())
}

fn is_r_already_installed(shell: &Shell, version: &ResolvedRVersion) -> Result<bool> {
    let output = cmd!(shell, "R --version").ignore_status().read();
    Ok(match output {
        Ok(stdout) => stdout.contains(&version.version),
        Err(_) => false,
    })
}

fn install_prerequisites(shell: &Shell, progress: &Progress) -> Result<()> {
    run_command(
        progress,
        "Updating apt package metadata",
        "apt package metadata updated",
        cmd!(
            shell,
            "sudo env DEBIAN_FRONTEND=noninteractive apt-get update -y -qq"
        ),
    )?;

    run_command(
        progress,
        "Installing base R prerequisites",
        "base R prerequisites installed",
        cmd!(
            shell,
            "sudo env DEBIAN_FRONTEND=noninteractive apt-get install -y gdebi-core qpdf devscripts ghostscript"
        ),
    )?;

    run_command(
        progress,
        "Installing pak system requirements",
        "pak system requirements installed",
        cmd!(
            shell,
            "sudo env DEBIAN_FRONTEND=noninteractive apt-get install -y libcurl4-openssl-dev libssl-dev"
        ),
    )?;

    Ok(())
}

fn install_from_deb(shell: &Shell, package_path: &Path, progress: &Progress) -> Result<()> {
    let label = format!("Installing {}", package_path.display());
    run_command(
        progress,
        label.clone(),
        format!("Installed {}", package_path.display()),
        cmd!(shell, "sudo gdebi --non-interactive {package_path}"),
    )
}

fn configure_symlinks(
    shell: &Shell,
    version: &ResolvedRVersion,
    progress: &Progress,
) -> Result<()> {
    let install_dir = version.install_dir_name();
    let r_path = format!("/opt/R/{install_dir}/bin/R");
    let rscript_path = format!("/opt/R/{install_dir}/bin/Rscript");

    run_command(
        progress,
        "Linking R binary",
        format!("Linked /usr/local/bin/R -> {r_path}"),
        cmd!(shell, "sudo ln -sf {r_path} /usr/local/bin/R"),
    )?;

    run_command(
        progress,
        "Linking Rscript binary",
        format!("Linked /usr/local/bin/Rscript -> {rscript_path}"),
        cmd!(shell, "sudo ln -sf {rscript_path} /usr/local/bin/Rscript"),
    )?;
    Ok(())
}

fn run_command(
    progress: &Progress,
    start_message: impl Into<String>,
    success_message: impl Into<String>,
    command: xshell::Cmd<'_>,
) -> Result<()> {
    let start_message = start_message.into();
    let task = progress.task(start_message.clone());

    let output = match command.quiet().ignore_status().output() {
        Ok(output) => output,
        Err(err) => {
            task.fail(format!("{start_message} (failed to start)"));
            return Err(err.into());
        }
    };

    if output.status.success() {
        task.finish_with_message(success_message.into());
        return Ok(());
    }

    task.fail(format!("{start_message} (failed)"));
    emit_command_output(progress, &start_message, &output.stdout, &output.stderr);

    bail!("{start_message} failed with status {}", output.status);
}

struct DownloadedInstaller {
    #[allow(dead_code)]
    temp_dir: TempDir,
    path: PathBuf,
}

impl DownloadedInstaller {
    fn path(&self) -> &Path {
        &self.path
    }
}

fn download_installer(version: &ResolvedRVersion) -> Result<DownloadedInstaller> {
    let client = http_client()?;
    let response = client
        .get(version.url.clone())
        .send()
        .with_context(|| format!("failed to download {}", version.url))?
        .error_for_status()
        .with_context(|| format!("download returned error status for {}", version.url))?;

    let temp_dir = TempDir::new().context("failed to allocate temporary directory")?;
    let file_name = file_name_from_url(&version.url)?;
    let installer_path = temp_dir.path().join(file_name);

    let mut file = File::create(&installer_path)
        .with_context(|| format!("failed to create {}", installer_path.display()))?;
    let mut reader = response;
    copy(&mut reader, &mut file)
        .with_context(|| format!("failed to write {}", installer_path.display()))?;

    Ok(DownloadedInstaller {
        temp_dir,
        path: installer_path,
    })
}

fn ensure_quarto(shell: &Shell, progress: &Progress) -> Result<()> {
    ensure_curl(shell, progress)?;

    let client = http_client()?;
    let quarto_version = latest_quarto_version(&client)?;

    let check_task = progress.task(format!("Checking existing Quarto {quarto_version}"));
    let already_installed = match cmd!(shell, "quarto --version")
        .quiet()
        .ignore_status()
        .read()
    {
        Ok(output) => output.contains(&quarto_version),
        Err(_) => false,
    };

    if already_installed {
        check_task.finish_with_message(format!("Using existing Quarto {quarto_version}"));
        return Ok(());
    }
    check_task.finish_with_message(format!("Quarto {quarto_version} not detected; installing"));

    run_command(
        progress,
        format!("Creating /opt/quarto/{quarto_version}"),
        format!("Prepared /opt/quarto/{quarto_version}"),
        cmd!(shell, "sudo mkdir -p /opt/quarto/{quarto_version}"),
    )?;

    let tarball_path = format!("/tmp/quarto-{quarto_version}.tar.gz");
    let download_url = format!(
        "https://github.com/quarto-dev/quarto-cli/releases/download/v{}/quarto-{}-linux-amd64.tar.gz",
        quarto_version, quarto_version
    );

    run_command(
        progress,
        format!("Downloading Quarto {quarto_version} bundle"),
        format!("Downloaded Quarto {quarto_version} bundle"),
        cmd!(shell, "curl -fsSL -o {tarball_path} -L {download_url}"),
    )?;

    run_command(
        progress,
        format!("Extracting Quarto {quarto_version} bundle"),
        format!("Installed Quarto {quarto_version} to /opt/quarto/{quarto_version}"),
        cmd!(
            shell,
            "sudo tar -xzf {tarball_path} -C /opt/quarto/{quarto_version} --strip-components=1"
        ),
    )?;

    run_command(
        progress,
        "Cleaning temporary Quarto archive",
        "Removed temporary Quarto archive",
        cmd!(shell, "rm -f {tarball_path}"),
    )?;

    run_command(
        progress,
        "Linking Quarto binary",
        format!("Linked /usr/local/bin/quarto -> /opt/quarto/{quarto_version}/bin/quarto"),
        cmd!(
            shell,
            "sudo ln -sf /opt/quarto/{quarto_version}/bin/quarto /usr/local/bin/quarto"
        ),
    )?;

    progress.println(format!("Quarto {quarto_version} installation completed"));

    Ok(())
}

fn ensure_pandoc(shell: &Shell, progress: &Progress) -> Result<()> {
    let check_task = progress.task("Checking existing pandoc");
    let already_installed = cmd!(shell, "pandoc --version")
        .quiet()
        .ignore_status()
        .output()
        .map(|output| output.status.success())
        .unwrap_or(false);
    if already_installed {
        check_task.finish_with_message("Using existing pandoc");
        return Ok(());
    }
    check_task.finish_with_message("pandoc not detected; installing");

    run_command(
        progress,
        "Updating apt metadata for pandoc",
        "apt metadata updated for pandoc",
        cmd!(
            shell,
            "sudo env DEBIAN_FRONTEND=noninteractive apt-get update -y -qq"
        ),
    )?;

    run_command(
        progress,
        "Installing pandoc",
        "pandoc installed",
        cmd!(
            shell,
            "sudo env DEBIAN_FRONTEND=noninteractive apt-get install -y pandoc"
        ),
    )?;

    progress.println("pandoc installation completed");

    Ok(())
}

fn ensure_tinytex(shell: &Shell, progress: &Progress) -> Result<()> {
    let check_task = progress.task("Checking existing TinyTeX");
    if tinytex_is_installed(shell) {
        check_task.finish_with_message("Using existing TinyTeX");
        return Ok(());
    }
    check_task.finish_with_message("TinyTeX not detected; installing");

    run_command(
        progress,
        "Installing TinyTeX via Quarto",
        "TinyTeX installed via Quarto",
        cmd!(
            shell,
            "quarto install tinytex --update-path --no-prompt --log-level warning"
        ),
    )?;

    if !tinytex_is_installed(shell) {
        bail!("TinyTeX installation via Quarto did not succeed");
    }

    link_tinytex_binaries(shell, progress)?;

    progress.println("TinyTeX installation completed");

    Ok(())
}

fn tinytex_is_installed(shell: &Shell) -> bool {
    let cli_available = cmd!(shell, "tlmgr --version")
        .quiet()
        .ignore_status()
        .run()
        .is_ok();
    if cli_available {
        return true;
    }

    let bin_dirs = discover_tinytex_bin_dirs(shell);
    if bin_dirs.iter().any(|dir| dir.join("tlmgr").exists()) {
        return true;
    }

    false
}

fn link_tinytex_binaries(shell: &Shell, progress: &Progress) -> Result<()> {
    let bin_dirs = discover_tinytex_bin_dirs(shell);

    if bin_dirs.is_empty() {
        progress.println("TinyTeX binaries not located; skipping symlink creation");
        return Ok(());
    }

    for binary in ["tlmgr", "pdflatex", "xelatex", "lualatex"] {
        if let Some(source) = bin_dirs
            .iter()
            .map(|dir| dir.join(binary))
            .find(|candidate| candidate.exists())
        {
            run_command(
                progress,
                format!("Linking TinyTeX {binary}"),
                format!("Linked /usr/local/bin/{binary}"),
                cmd!(shell, "sudo ln -sf {source} /usr/local/bin/{binary}"),
            )?;
        }
    }

    Ok(())
}

fn discover_tinytex_bin_dirs(shell: &Shell) -> Vec<PathBuf> {
    let mut bin_dirs = Vec::new();

    if let Ok(path_output) = cmd!(shell, "command -v tlmgr")
        .quiet()
        .ignore_status()
        .read()
    {
        let path_output = path_output.trim();
        if !path_output.is_empty() && path_output.contains('/') {
            let path = Path::new(path_output);
            if path.is_file() {
                if let Some(parent) = path.parent() {
                    bin_dirs.push(parent.to_path_buf());
                }
            }
        }
    }

    if let Some(home_dir) = env::var_os("HOME").map(PathBuf::from) {
        let bin_root = home_dir.join(".TinyTeX").join("bin");
        if let Ok(entries) = std::fs::read_dir(&bin_root) {
            for entry in entries.flatten() {
                let path = entry.path();
                if path.is_dir() && !bin_dirs.iter().any(|existing| existing == &path) {
                    bin_dirs.push(path);
                }
            }
        }
    }

    bin_dirs
}

fn ensure_curl(shell: &Shell, progress: &Progress) -> Result<()> {
    if cmd!(shell, "curl --version")
        .quiet()
        .ignore_status()
        .output()
        .map(|output| output.status.success())
        .unwrap_or(false)
    {
        return Ok(());
    }

    run_command(
        progress,
        "Updating apt metadata for curl",
        "apt metadata updated for curl",
        cmd!(
            shell,
            "sudo env DEBIAN_FRONTEND=noninteractive apt-get update -y -qq"
        ),
    )?;

    run_command(
        progress,
        "Installing curl",
        "curl installed",
        cmd!(
            shell,
            "sudo env DEBIAN_FRONTEND=noninteractive apt-get install -y curl"
        ),
    )
}

fn http_client() -> Result<Client> {
    Client::builder()
        .user_agent(format!("revdeprun/{}", env!("CARGO_PKG_VERSION")))
        .build()
        .context("failed to construct HTTP client")
}

fn latest_quarto_version(client: &Client) -> Result<String> {
    #[derive(Deserialize)]
    struct QuartoDownloadInfo {
        version: String,
    }

    let response = client
        .get(QUARTO_DOWNLOAD_INFO_URL)
        .send()
        .context("failed to request Quarto download metadata")?
        .error_for_status()
        .context("Quarto download metadata request returned error status")?;

    let metadata: QuartoDownloadInfo = response
        .json()
        .context("failed to deserialize Quarto download metadata")?;

    Ok(metadata.version)
}

fn file_name_from_url(url: &str) -> Result<String> {
    let parsed =
        reqwest::Url::parse(url).with_context(|| format!("failed to parse download URL {url}"))?;
    parsed
        .path_segments()
        .and_then(|mut segments| segments.next_back())
        .filter(|segment| !segment.is_empty())
        .map(|segment| segment.to_string())
        .ok_or_else(|| anyhow::anyhow!("failed to extract file name from {url}"))
}
