use std::num::NonZeroUsize;

use camino::{Utf8Path, Utf8PathBuf};
use libpijul::pristine::sanakirja::Pristine;
use libpijul::{Base32, RecordBuilder, TxnT, TxnTExt};
use quote::quote;

#[derive(Debug)]
pub struct Build {
    pub package: Package,
    pub toolchain: Toolchain,
    pub compilation: Compilation,
    pub repository: Repository,
    pub timestamp: jiff::Timestamp,
}

#[derive(Debug)]
pub struct Package {
    pub package_name: &'static str,
    pub package_version: &'static str,
}

impl Package {
    fn export() -> Result<proc_macro2::TokenStream, anyhow::Error> {
        let package_name = std::env::var("CARGO_PKG_NAME")?;
        let package_version = &std::env::var("CARGO_PKG_VERSION")?;

        Ok(quote! {
            extension_build_info::Package {
                package_name: #package_name,
                package_version: #package_version,
            }
        })
    }
}

#[derive(Debug)]
pub struct Toolchain {
    pub cargo_version: &'static str,
    pub rustc_version: &'static str,
}

impl Toolchain {
    fn get_version(program_location: String) -> Result<String, anyhow::Error> {
        let version_output = std::process::Command::new(program_location)
            .arg("--version")
            .output()?;
        let version_string = String::from_utf8(version_output.stdout)?;

        Ok(match version_string.strip_suffix('\n') {
            Some(version) => version.to_string(),
            None => version_string,
        })
    }
    fn export() -> Result<proc_macro2::TokenStream, anyhow::Error> {
        let cargo_version = Self::get_version(std::env::var("CARGO")?)?;
        let rustc_version = Self::get_version(std::env::var("RUSTC")?)?;

        Ok(quote! {
            extension_build_info::Toolchain {
                cargo_version: #cargo_version,
                rustc_version: #rustc_version,
            }
        })
    }
}

#[derive(Debug)]
pub struct Compilation {
    pub target: &'static str,
    pub profile: &'static str,
    pub opt_level: &'static str,
    pub debug: &'static str,
    pub rustflags: &'static [&'static str],
}

impl Compilation {
    fn export() -> Result<proc_macro2::TokenStream, anyhow::Error> {
        let target = std::env::var("TARGET")?;
        let profile = std::env::var("PROFILE")?;
        let opt_level = std::env::var("OPT_LEVEL")?;
        let debug = std::env::var("DEBUG")?;
        let encoded_rustflags = std::env::var("CARGO_ENCODED_RUSTFLAGS")?;
        let rustflags = encoded_rustflags.split('\u{1f}');

        Ok(quote! {
            extension_build_info::Compilation {
                target: #target,
                profile: #profile,
                opt_level: #opt_level,
                debug: #debug,
                rustflags: &[#(#rustflags),*],
            }
        })
    }
}

#[derive(Debug)]
pub struct Repository {
    pub channel_name: &'static str,
    pub channel_state: &'static str,
    pub has_unrecorded_changes: bool,
}

impl Repository {
    fn export() -> Result<proc_macro2::TokenStream, anyhow::Error> {
        // Path: editors/{EDITOR}/
        let extension_manifest_directory = Utf8PathBuf::from(std::env::var("CARGO_MANIFEST_DIR")?);
        let repository_root = extension_manifest_directory.ancestors().nth(2).unwrap();
        // Path: .pijul/pristine/db
        let pristine_db = repository_root
            .join(libpijul::DOT_DIR)
            .join("pristine")
            .join("db");

        let pristine = Pristine::new(pristine_db.as_str())?;
        let change_store =
            libpijul::changestore::filesystem::FileSystem::from_root(repository_root.as_str(), 256);
        let working_copy = libpijul::working_copy::FileSystem::from_root(repository_root.as_str());

        // Recorded changes
        let transaction = pristine.arc_txn_begin()?;
        let read_transaction = transaction.read();
        let channel_name = read_transaction
            .current_channel()
            .unwrap_or(libpijul::DEFAULT_CHANNEL);
        let channel = read_transaction.load_channel(channel_name)?.unwrap();
        let channel_merkle = read_transaction.current_state(&*channel.read())?;
        let channel_state = channel_merkle.to_base32();

        // Unrecorded changes
        let mut record_builder = RecordBuilder::new();
        record_builder.record(
            transaction.clone(),
            libpijul::Algorithm::default(),
            true,
            &libpijul::DEFAULT_SEPARATOR,
            channel.clone(),
            &working_copy,
            &change_store,
            "",
            std::thread::available_parallelism()
                .unwrap_or(NonZeroUsize::MIN)
                .get(),
        )?;
        let unrecorded_changes = record_builder.finish();
        let has_unrecorded_changes = !unrecorded_changes.actions.is_empty();

        Ok(quote! {
            extension_build_info::Repository {
                channel_name: #channel_name,
                channel_state: #channel_state,
                has_unrecorded_changes: #has_unrecorded_changes,
            }
        })
    }
}

#[macro_export]
macro_rules! include_build_info {
    () => {
        include!(concat!(env!("OUT_DIR"), "/build_info.rs"))
    };
}

pub fn export() -> Result<(), anyhow::Error> {
    let timestamp = jiff::Timestamp::now();
    let timestamp_seconds = timestamp.as_second();
    let timestamp_nanoseconds = timestamp.subsec_nanosecond();

    let package = Package::export()?;
    let toolchain = Toolchain::export()?;
    let compilation = Compilation::export()?;
    let repository = Repository::export()?;

    let token_stream = quote! {
        extension_build_info::Build {
            package: #package,
            toolchain: #toolchain,
            compilation: #compilation,
            repository: #repository,
            timestamp: jiff::Timestamp::constant(#timestamp_seconds, #timestamp_nanoseconds),
        }
    };

    // Example path when building with `dev` profile:
    // target/debug/build/{EXTENSION NAME}-{HASH}/build-script-build/
    let out_dir = std::env::var("OUT_DIR")?;
    // target/debug/build/{EXTENSION NAME}-{HASH}/build-script-build/build_info.rs
    let file_path = Utf8Path::new(&out_dir).join("build_info.rs");
    std::fs::write(file_path, token_stream.to_string().as_bytes())?;

    Ok(())
}