use std::{
    collections::BTreeMap,
    env::{current_dir, set_current_dir},
    fs::{DirEntry, File, create_dir_all},
    io::Write,
    path::Path,
    process::Command,
    sync::Arc,
};

mod generate;
mod js;

use configinator::{ActionLang, AppConfig};
use parking_lot::Mutex;
use swc_common::{FileName, FilePathMapping, SourceMap};
use swc_html_codegen::{Emit, writer::basic::BasicHtmlWriter};
use swc_html_minifier::{minify_document, option::MinifyOptions};
use swc_html_parser::parse_file_as_document;

use crate::generate::action::generate_action_bindings;

const BASE_CLIENT: &str = include_str!(concat!(
    env!("CARGO_MANIFEST_DIR"),
    "/static/client_template.rs"
));

const BASE_SERVER_TOML: &str = include_str!(concat!(
    env!("CARGO_MANIFEST_DIR"),
    "/static/ServerTemplate.toml"
));
const BASE_CLIENT_TOML: &str = include_str!(concat!(
    env!("CARGO_MANIFEST_DIR"),
    "/static/ClientTemplate.toml"
));

const BASE_ACTION_TOML: &str = include_str!(concat!(
    env!("CARGO_MANIFEST_DIR"),
    "/static/ServerAction.toml"
));

// js

const APPEND_WASM_JS: &str = include_str!(concat!(
    env!("CARGO_MANIFEST_DIR"),
    "/static/append_wasm.js"
));

const JS_ONLY_JS: &str = include_str!(concat!(
    env!("CARGO_MANIFEST_DIR"),
    "/static/javascript_only.js"
));

const CORE_JS: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/static/core.js"));

pub fn build(path: &str, no_cache: bool) -> Result<(), Box<dyn std::error::Error>> {
    let start_dir = current_dir()?;

    let path = Path::new(path);
    set_current_dir(path)?;
    let project_dir = current_dir()?;

    let config_str = std::fs::read_to_string("./ordinary.json")?;
    let config: AppConfig = serde_json::from_str(&config_str).unwrap();

    let gen_dir_path = project_dir.join(".ordinary/gen");

    let client_dir_path = gen_dir_path.join("client");
    let server_dir_path = gen_dir_path.join("server");
    let templates_dir_path = gen_dir_path.join("templates");

    create_dir_all(&client_dir_path)?;
    create_dir_all(&server_dir_path)?;
    create_dir_all(&templates_dir_path)?;

    let mut content_def_map = BTreeMap::new();

    if let Some(content) = config.content {
        for content_def in content.definitions {
            content_def_map.insert(content_def.name.clone(), content_def.clone());
        }
    }

    let mut model_config_map = BTreeMap::new();

    if let Some(models) = config.models {
        for model_config in models {
            model_config_map.insert(model_config.name.clone(), model_config.clone());
        }
    }

    let mut integration_config_map = BTreeMap::new();

    if let Some(integrations) = config.integrations {
        for integration_config in integrations {
            integration_config_map
                .insert(integration_config.name.clone(), integration_config.clone());
        }
    }

    if let Some(actions) = config.actions.clone() {
        for action_config in actions {
            let cache_dir = gen_dir_path
                .join("actions")
                .join("cache")
                .join(&action_config.name);

            let input_action_dir = Path::new(&action_config.dir_path);

            create_dir_all(&cache_dir)?;

            let should_build = Arc::new(Mutex::new(false));

            traverse(input_action_dir, &|entry| {
                let path = entry.path();

                if let Some(str_path) = path.to_str()
                    && (str_path.contains(".vscode")
                        || str_path.contains("target")
                        || str_path.contains("node_modules"))
                    {
                        return ;
                    }

                let curr_content = std::fs::read(&path).unwrap_or_else(|_| Vec::new());

                let child_path = path.strip_prefix(input_action_dir).unwrap();

                if let Some(parent) = child_path.parent() {
                    create_dir_all(cache_dir.join(parent)).unwrap();
                }

                let cache_path = cache_dir.join(child_path);

                let cached_content = std::fs::read(&cache_path).unwrap_or_else(|_| Vec::new());

                if curr_content != cached_content {
                    let mut content_file = File::create(&cache_path).unwrap();
                    content_file.write_all(&curr_content).unwrap();

                    let mut should_build = should_build.lock();
                    *should_build = true;
                }
            })?;

            let should_build = should_build.lock();

            if *should_build || no_cache {
                let (generated_code, extras) = generate_action_bindings(
                    action_config.clone(),
                    &model_config_map,
                    &content_def_map,
                    &integration_config_map,
                    &config.auth,
                    &config.domain,
                )?;

                if let Some((main_rs, cargo_toml, type_defs)) = extras {
                    let path = project_dir.join(&action_config.dir_path);
                    create_dir_all(path.join("src"))?;

                    let mut cargo_file = File::create(path.join("Cargo.toml"))?;
                    cargo_file.write_all(cargo_toml.as_bytes())?;

                    let mut main_file = File::create(path.join("src").join("main.rs"))?;
                    main_file.write_all(main_rs.as_bytes())?;

                    match action_config.lang {
                        ActionLang::Rust => {}
                        ActionLang::JavaScript => {
                            let mut type_file = File::create(path.join("index.d.ts"))?;
                            type_file.write_all(type_defs.as_bytes())?;

                            let curr_dir = current_dir()?;
                            set_current_dir(path)?;

                            Command::new("pnpm").args(["install"]).output()?;
                            Command::new("pnpm").args(["run", "build"]).output()?;

                            set_current_dir(curr_dir)?;
                        }
                    }
                }

                let action_dir_path = gen_dir_path.join("actions").join(&action_config.name);

                create_dir_all(action_dir_path.join("src"))?;

                let cargo = action_dir_path.join("Cargo.toml");
                let mut cargo_file = File::create(cargo)?;
                cargo_file.write_all(BASE_ACTION_TOML.as_bytes())?;

                let lib = action_dir_path.join("src").join("lib.rs");
                let mut lib_file = File::create(lib)?;
                lib_file.write_all(generated_code.as_bytes())?;

                let path = project_dir.join(action_config.dir_path);
                set_current_dir(path)?;

                let output = Command::new("cargo")
                    .args(["build", "--target", "wasm32-wasip1", "--release"])
                    .output()?;

                if !output.status.success() {
                    tracing::error!(
                        "failed for action '{}'\n\n{}",
                        action_config.name,
                        String::from_utf8_lossy(&output.stderr)
                    );
                } else {
                    let action_path = "target/wasm32-wasip1/release/action.wasm";

                    let action_bytes = std::fs::read(action_path)?;

                    tracing::info!(
                        "action '{}', lang: {:?}, bin size: {}",
                        action_config.name,
                        action_config.lang,
                        bytesize::ByteSize(action_bytes.len() as u64),
                    );
                }

                set_current_dir(&project_dir)?;
            } else {
                tracing::info!("action '{}' has not changed; skipping.", action_config.name);
            }
        }
    }

    let mut client_file = BASE_CLIENT.to_string();

    if let Some(templates) = config.templates {
        for template_config in templates {
            if let Some(template_path) = &template_config.path {
                let path = project_dir.join(template_path);

                let template_string = std::fs::read_to_string(&path)?;

                let template_final = if template_config.minify == Some(true) {
                    if template_config.mime == "text/html"
                        || template_config.mime == "text/html; charset=utf-8"
                    {
                        swc_common::GLOBALS.set(&swc_common::Globals::new(), || {
                            let cm = SourceMap::new(FilePathMapping::empty());
                            let fm =
                                cm.new_source_file(FileName::Anon.into(), template_string.clone());

                            let mut errors = Vec::new();

                            let mut document =
                                parse_file_as_document(&fm, Default::default(), &mut errors)
                                    .unwrap();

                            minify_document(
                                &mut document,
                                &MinifyOptions {
                                    // todo: may be too aggressive for minification
                                    collapse_whitespaces:
                                        swc_html_minifier::option::CollapseWhitespaces::All,
                                    ..Default::default()
                                },
                            );

                            let mut html_string = String::new();

                            let wr =
                                BasicHtmlWriter::new(&mut html_string, None, Default::default());
                            let mut generator = swc_html_codegen::CodeGenerator::new(
                                wr,
                                swc_html_codegen::CodegenConfig {
                                    minify: true,
                                    ..Default::default()
                                },
                            );

                            generator.emit(&document).unwrap();

                            html_string.as_bytes().to_vec()
                        })
                    } else {
                        // todo: support minification for additional MIME types
                        template_string.as_bytes().to_vec()
                    }
                } else {
                    template_string.as_bytes().to_vec()
                };

                let file_name = match path.extension() {
                    Some(ext) => match ext.to_str() {
                        Some(ext) => &format!("{}.{}", &template_config.name, ext),
                        None => &template_config.name,
                    },
                    None => &template_config.name,
                };

                let mut global_vars = BTreeMap::new();

                if let Some(globals) = config.globals.clone() {
                    for global_var in globals {
                        global_vars.insert(global_var.name, global_var.value);
                    }
                }

                let (server, client) = generate::template::generate_template_renderers(
                    &config.domain,
                    &config.version,
                    file_name,
                    template_config.clone(),
                    &content_def_map,
                    &model_config_map,
                    global_vars,
                    &config.error,
                    &config.auth,
                );

                client_file = format!("{}\n{}", client_file, client);

                let file_path = templates_dir_path.join(file_name);

                if !no_cache
                    && file_path.exists() && std::fs::read(&file_path)? == template_final {
                        tracing::info!(
                            "template '{}' has not changed; skipping.",
                            template_config.name
                        );
                        continue;
                    }

                // server
                let mut file = File::create(file_path)?;

                file.write_all(&template_final)?;

                create_dir_all(server_dir_path.join(&template_config.name).join("src"))?;

                let server_main_rs = server_dir_path
                    .join(&template_config.name)
                    .join("src")
                    .join("main.rs");
                let mut server_main_rs_file = File::create(server_main_rs)?;
                server_main_rs_file.write_all(server.as_bytes())?;

                let server_cargo = server_dir_path
                    .join(&template_config.name)
                    .join("Cargo.toml");
                let mut server_cargo_file = File::create(server_cargo)?;
                server_cargo_file.write_all(BASE_SERVER_TOML.as_bytes())?;

                let server_askama = server_dir_path
                    .join(&template_config.name)
                    .join("askama.toml");
                let mut server_askama_file = File::create(server_askama)?;
                server_askama_file.write_all(
                    r#"# generated
[general]
dirs = ["../../templates"]

[[escaper]]
path = "askama::filters::Text"
extensions = ["js", "json"]
"#
                    .as_bytes(),
                )?;

                let path = server_dir_path.join(&template_config.name);
                set_current_dir(path)?;

                Command::new("cargo").args(["fmt"]).output()?;

                let output = Command::new("cargo")
                    .args(["build", "--target", "wasm32-wasip1", "--release"])
                    .output()?;

                if !output.status.success() {
                    tracing::error!(
                        "failed for template '{}'\n\n{}",
                        template_config.name,
                        String::from_utf8_lossy(&output.stderr)
                    );
                } else {
                    let template_wasm_path = "target/wasm32-wasip1/release/template.wasm";

                    let template_bytes = std::fs::read(template_wasm_path)?;

                    if template_config.minify == Some(true) {
                        tracing::info!(
                            "template '{}', mime: {}, source size: {}, minified: {}, bin size: {}",
                            template_config.name,
                            template_config.mime,
                            bytesize::ByteSize(template_string.len() as u64),
                            bytesize::ByteSize(template_final.len() as u64),
                            bytesize::ByteSize(template_bytes.len() as u64),
                        );
                    } else {
                        tracing::info!(
                            "template '{}', mime: {}, source size: {}, bin size: {}",
                            template_config.name,
                            template_config.mime,
                            bytesize::ByteSize(template_final.len() as u64),
                            bytesize::ByteSize(template_bytes.len() as u64),
                        );
                    }
                }
            }

            set_current_dir(&project_dir)?;
        }
    }

    create_dir_all(client_dir_path.join("src"))?;

    let client_lib_rs = client_dir_path.join("src").join("lib.rs");
    let mut client_lib_file = File::create(client_lib_rs)?;
    client_lib_file.write_all(client_file.as_bytes())?;

    let client_cargo = client_dir_path.join("Cargo.toml");
    let mut client_cargo_file = File::create(client_cargo)?;
    client_cargo_file.write_all(BASE_CLIENT_TOML.as_bytes())?;

    let client_askama = client_dir_path.join("askama.toml");
    let mut client_askama_file = File::create(client_askama)?;
    client_askama_file.write_all(
        r#"# generated
[general]
dirs = ["../templates"]

[[escaper]]
path = "askama::filters::Text"
extensions = ["js", "json"]
"#
        .as_bytes(),
    )?;

    set_current_dir("./.ordinary/gen/client")?;

    Command::new("cargo").args(["fmt"]).output()?;

    let build_output = Command::new("cargo")
        .args([
            "build",
            "--lib",
            "--release",
            "--target",
            "wasm32-unknown-unknown",
        ])
        .output()?;

    if !build_output.status.success() {
        print!("{}", std::str::from_utf8(&build_output.stderr)?)
    }

    let bindgen_output = Command::new("wasm-bindgen")
        .args([
            "target/wasm32-unknown-unknown/release/client.wasm",
            "--out-dir",
            "wasm",
            "--typescript",
            "--target",
            "web",
        ])
        .output()?;

    if !bindgen_output.status.success() {
        print!("{}", std::str::from_utf8(&bindgen_output.stderr)?)
    }

    let opt_output = Command::new("wasm-opt")
        .args([
            "--all-features",
            "wasm/client_bg.wasm",
            "-o",
            "wasm/client_bg_opt.wasm",
            "-O4",
        ])
        .output()?;

    if !opt_output.status.success() {
        print!("{}", std::str::from_utf8(&opt_output.stderr)?)
    }

    set_current_dir(&project_dir)?;

    create_dir_all("./.ordinary/gen/client/js")?;

    let mut core_js_file = File::create("./.ordinary/gen/client/js/core.js")?;
    core_js_file.write_all(js::minify(CORE_JS)?.as_bytes())?;

    let client_js = std::fs::read_to_string("./.ordinary/gen/client/wasm/client.js")?;

    if !client_js.contains(&js::minify(APPEND_WASM_JS)?) {
        let final_js = format!("{}\n{}", client_js, APPEND_WASM_JS);

        let mut client_js_file = File::create("./.ordinary/gen/client/wasm/client.js")?;
        client_js_file.write_all(js::minify(&final_js)?.as_bytes())?;
    }

    let mut js_only_client = File::create("./.ordinary/gen/client/js/client.js")?;
    js_only_client.write_all(js::minify(JS_ONLY_JS)?.as_bytes())?;

    set_current_dir(start_dir)?;

    Ok(())
}

pub fn traverse(dir: &Path, cb: &dyn Fn(&DirEntry)) -> std::io::Result<()> {
    if dir.is_dir() {
        for entry in std::fs::read_dir(dir)? {
            let entry = entry?;
            let path = entry.path();
            if path.is_dir() {
                traverse(&path, cb)?;
            } else {
                cb(&entry);
            }
        }
    }
    Ok(())
}
