use harp::call::r_expr_quote;
use harp::environment::r_ns_env;
use harp::environment::Binding;
use harp::exec::RFunction;
use harp::exec::RFunctionExt;
use harp::object::r_length;
use harp::object::RObject;
use harp::r_symbol;
use harp::utils::r_typeof;
use libr::*;

use crate::console::Console;
use crate::modules::ARK_ENVS;
use crate::r_task;
use crate::r_task::RTask;
use crate::variables::variable::is_binding_fancy;
use crate::variables::variable::plain_binding_force_with_rollback;

#[tracing::instrument(level = "trace", skip_all)]
pub(crate) fn resource_namespaces(pkgs: Vec<String>) -> anyhow::Result<()> {
    // Generate only one task and loop inside it to preserve the order of `pkgs`
    r_task::spawn(RTask::idle(async move |_| {
        for pkg in pkgs.into_iter() {
            if let Err(err) = ns_populate_srcref(pkg.clone()).await {
                log::error!("Can't populate srcrefs for `{pkg}`: {err:?}");
            }
        }
    }));

    Ok(())
}

pub(crate) fn resource_loaded_namespaces() -> anyhow::Result<()> {
    let loaded = RFunction::new("base", "loadedNamespaces").call()?;
    let loaded: Vec<String> = loaded.try_into()?;
    resource_namespaces(loaded)
}

pub(crate) async fn ns_populate_srcref(ns_name: String) -> anyhow::Result<()> {
    if let Some((uri, contents)) = ns_populate_srcref_without_vdoc_insertion(ns_name).await? {
        // Register the virtual document for the namespace
        Console::get_mut().insert_virtual_document(uri, contents);
    };

    Ok(())
}

/// Returns `None` if namespace vdoc was already generated. Otherwise returns
/// `(uri, contents)`.
async fn ns_populate_srcref_without_vdoc_insertion(
    ns_name: String,
) -> anyhow::Result<Option<(String, String)>> {
    let span = tracing::trace_span!("ns_populate_srcref", ns = ns_name);

    // Don't redo the work if we already did it. We don't expect a namespace to change.
    #[cfg(not(test))]
    if Console::get().has_virtual_document(&ark_ns_uri(&ns_name)) {
        return Ok(None);
    }

    let mut tick = std::time::Instant::now();

    let ns = r_ns_env(&ns_name)?;
    let uri = ark_ns_uri(&ns_name);

    let mut vdoc: Vec<String> = vec![
        format!("# Virtual namespace of package {ns_name}."),
        format!("# This source is generated by Ark and is approximate."),
        format!(""),
        format!("declare(ark(diagnostics(enable = FALSE)))"),
        String::from(""),
    ];

    let mut n_ok = 0;
    let mut n_bad = 0;
    let mut n_skipped = 0;

    for b in ns.iter().filter_map(Result::ok) {
        span.in_scope(|| {
            match generate_source(&b, ns.inner.sexp, vdoc.len(), &uri) {
                Ok(Some(mut lines)) => {
                    n_ok += 1;

                    vdoc.append(&mut lines);

                    // Add some separation
                    vdoc.push(String::from(""));
                },
                Err(_err) => {
                    n_bad += 1;

                    // log::error!(
                    //     "Can't populate srcref for {} in namespace {ns_name}: {_err}",
                    //     b.name
                    // )
                },
                _ => {
                    n_skipped += 1;
                },
            }
        });

        if tick.elapsed() > std::time::Duration::from_millis(10) {
            tick = std::time::Instant::now();
            tokio::task::yield_now().await;
        }
    }

    log::trace!(
        "Populated virtual namespace for {ns_name}: \
         {} lines, {n_ok} ok, {n_bad} bad, {n_skipped} skipped",
        vdoc.len()
    );

    let contents = vdoc.join("\n");
    Ok(Some((uri, contents)))
}

fn ark_ns_uri(ns_name: &str) -> String {
    ark_uri(&format!("namespace/{ns_name}.R"))
}

#[tracing::instrument(level = "trace", skip_all, fields(name = %binding.name))]
fn generate_source(
    binding: &Binding,
    ns_env: SEXP,
    line: usize,
    uri: &str,
) -> anyhow::Result<Option<Vec<String>>> {
    if is_binding_fancy(binding) {
        return Ok(None);
    }

    // Only update regular functions
    let old = plain_binding_force_with_rollback(binding)?;
    if old.kind() != CLOSXP {
        return Ok(None);
    }

    // These don't deparse to a `function` call!
    if harp::utils::r_is_s4(old.sexp) {
        return Ok(None);
    }

    // Ignore functions that already have sources
    if old.get_attribute("srcref").is_some() {
        return Ok(None);
    }

    let reparsed = RFunction::new("", "reparse_with_srcref")
        .add(old.clone())
        .param("name", r_expr_quote(binding.name.sexp))
        .param("uri", uri)
        .param("line", (line + 1) as i32)
        .call_in(ARK_ENVS.positron_ns)?;

    let (new, text) = (
        harp::list_get(reparsed.sexp, 0),
        harp::list_get(reparsed.sexp, 1),
    );

    // Inject source references in functions. This is slightly unsafe but we
    // couldn't think of a dire failure mode.
    unsafe {
        // First replace the body which contains expressions tagged with srcrefs
        // such as calls to `{`. Compiled functions are a little more tricky.
        let body = harp::fn_body(old.sexp);

        if r_typeof(body) == BCODESXP {
            // This is a compiled function. We could recompile the fresh
            // function we just created but the compiler is very slow. Instead,
            // update the expression stored in the bytecode. This expression is
            // used by `eval()` when stepping with the debugger.

            // Get the constant pool: BCODE_CONSTS = CDR
            let consts = CDR(body);

            // The original body expression is stored as first element
            // of the constant pool
            if r_length(consts) > 0 {
                // Inject new body instrumented with source references
                SET_VECTOR_ELT(consts, 0, R_ClosureExpr(new));
            }

            harp::attrib_poke(
                old.sexp,
                r_symbol!("srcref"),
                harp::attrib_get(new, r_symbol!("srcref")),
            );
        } else {
            let new_body = harp::fn_body(new);
            let out = RObject::new(harp::new_function(
                harp::fn_formals(old.sexp),
                new_body,
                harp::fn_env(old.sexp),
            ));

            harp::attrib_poke_from(out.sexp, old.sexp);
            harp::attrib_poke(
                out.sexp,
                r_symbol!("srcref"),
                harp::attrib_get(new, r_symbol!("srcref")),
            );

            harp::env_bind_force(ns_env, binding.name.sexp, out.sexp);
        }
    }

    let text: Vec<String> = RObject::view(text).try_into()?;
    Ok(Some(text))
}

/// Creates a URI with scheme `ark:`. These URIs are routed from the frontend by
/// a text content provider to our LSP via a custom request.
pub(crate) fn ark_uri(path: &str) -> String {
    // The URI includes the process ID to disambiguate in case multiple sessions
    // are open on the frontend side. We're not worried about pid reuse issues
    // for our modest use cases with virtual documents.
    //
    // Currently if the wrong session is active when a vdoc is opened you just
    // get an error but we could route them to the correct kernel via a Jupyter
    // comm (Control socket for async RPC) in the future. Unlike the LSP, the
    // Jupyter comm persists session changes. Note that we currently don't have
    // important use cases that require opening a vdoc for a background session.
    // It's possible that a user could call `View()` after running a long
    // computation, but it'd be peculiar to view a function that way, we mostly
    // expect interactive gestures.
    let id = std::process::id();
    format!("ark:ark-{id}/{path}")
}

#[harp::register]
pub extern "C-unwind" fn ark_zap_srcref(x: SEXP) -> anyhow::Result<SEXP> {
    Ok(harp::attrib::zap_srcref(x).sexp)
}

#[harp::register]
pub extern "C-unwind" fn ps_ark_uri(path: SEXP) -> anyhow::Result<SEXP> {
    let path: String = RObject::view(path).try_into()?;
    let uri: RObject = ark_uri(&path).into();
    Ok(uri.sexp)
}

#[harp::register]
pub extern "C-unwind" fn ps_ark_ns_uri(path: SEXP) -> anyhow::Result<SEXP> {
    let path: String = RObject::view(path).try_into()?;
    let uri: RObject = ark_ns_uri(&path).into();
    Ok(uri.sexp)
}

#[harp::register]
unsafe extern "C-unwind" fn ps_resource_namespaces(pkgs: SEXP) -> anyhow::Result<SEXP> {
    let pkgs: Vec<String> = RObject::view(pkgs).try_into()?;
    resource_namespaces(pkgs)?;
    Ok(harp::r_null())
}

#[harp::register]
unsafe extern "C-unwind" fn ps_ns_populate_srcref(ns_name: SEXP) -> anyhow::Result<SEXP> {
    let ns_name: String = RObject::view(ns_name).try_into()?;
    futures::executor::block_on(ns_populate_srcref(ns_name))?;
    Ok(harp::r_null())
}

#[harp::register]
unsafe extern "C-unwind" fn ps_ns_populate_srcref_without_vdoc_insertion(
    ns_name: SEXP,
) -> anyhow::Result<SEXP> {
    let ns_name: String = RObject::view(ns_name).try_into()?;

    let Some((uri, contents)) =
        futures::executor::block_on(ns_populate_srcref_without_vdoc_insertion(ns_name))?
    else {
        return Ok(RObject::null().sexp);
    };

    // Would ideally be a named list but currently inconvenient to create
    let uri: RObject = uri.into();
    let contents: RObject = contents.into();
    let out: RObject = vec![uri, contents].try_into()?;

    Ok(out.sexp)
}
