use crate::common::OVERWRITTEN_MSG;
use crate::common::ToCase as _;
use crate::schema::ConfigDef;
use crate::schema::Joinable;
use crate::schema::{_to_ident_name, GroupsDef};
use crate::{SEPARATED_BASE_FILES, filters};
use crate::{
    common::fs_write,
    schema::{FieldDef, ModelDef, VALUE_OBJECTS, set_domain_mode, to_id_name},
};
use anyhow::{Result, ensure};
use askama::Template;
use regex::Regex;
use std::{
    collections::{BTreeMap, HashSet},
    ffi::OsString,
    fs,
    path::Path,
    sync::Arc,
};

pub fn write_value_objects_rs(
    base_domain_src_dir: &Path,
    remove_files: &mut HashSet<OsString>,
    clean: bool,
    force: bool,
) -> Result<()> {
    let file_path = base_domain_src_dir.join("value_objects.rs");
    let mod_names: BTreeMap<_, _> = VALUE_OBJECTS
        .read()
        .unwrap()
        .as_ref()
        .unwrap()
        .iter()
        .map(|(name, _def)| (name.to_snake(), name.to_pascal()))
        .collect();

    #[derive(Template)]
    #[template(
        source = r###"
// Do not modify below this line. (ModStart)
@%- if SEPARATED_BASE_FILES %@
mod _base {
@%- for (mod_name, _) in mod_names %@
    pub mod _@{ mod_name }@;
@%- endfor %@
}
@%- else %@
mod _base;
@%- endif %@
@%- for (mod_name, _) in mod_names %@
mod @{ mod_name|ident }@;
@%- endfor %@
@%- for (mod_name, name) in mod_names %@
pub use @{ mod_name|ident }@::@{ name }@;
@%- endfor %@
// Do not modify above this line. (ModEnd)"###,
        ext = "txt",
        escape = "none"
    )]
    struct DomainValueObjectModsTemplate {
        pub mod_names: BTreeMap<String, String>,
    }
    let tpl = DomainValueObjectModsTemplate { mod_names }.render()?;
    let tpl = tpl.trim_start();
    if file_path.exists() {
        let content = fs::read_to_string(&file_path)?.replace("\r\n", "\n");
        let re = Regex::new(r"(?s)// Do not modify below this line. \(ModStart\).+// Do not modify above this line. \(ModEnd\)").unwrap();
        ensure!(
            re.is_match(&content),
            "File contents are invalid.: {:?}",
            &file_path
        );
        let content = re.replace(&content, tpl);
        fs_write(file_path, &*content)?;
    } else {
        fs_write(file_path, tpl)?;
    }

    let value_objects_dir = base_domain_src_dir.join("value_objects");
    let value_objects_base_dir = value_objects_dir.join("_base");
    if clean && value_objects_dir.exists() {
        for entry in glob::glob(&format!("{}/**/*.rs", value_objects_dir.display()))? {
            match entry {
                Ok(path) => remove_files.insert(path.as_os_str().to_owned()),
                Err(_) => false,
            };
        }
    }

    #[allow(dead_code)]
    #[derive(Template)]
    #[template(path = "domain/base_domain/src/value_objects/base.rs", escape = "none")]
    struct DomainValueObjectBaseTemplate<'a> {
        pub mod_name: &'a str,
        pub pascal_name: &'a str,
        pub def: &'a FieldDef,
    }

    #[derive(Template)]
    #[template(
        path = "domain/base_domain/src/value_objects/wrapper.rs",
        escape = "none"
    )]
    struct DomainValueObjectWrapperTemplate<'a> {
        pub mod_name: &'a str,
        pub pascal_name: &'a str,
    }

    let mut output = String::new();
    output.push_str(OVERWRITTEN_MSG);
    for (name, def) in VALUE_OBJECTS.read().unwrap().as_ref().unwrap() {
        let mod_name = name.to_snake();
        let mod_name = &mod_name;
        let tpl = DomainValueObjectBaseTemplate {
            mod_name,
            pascal_name: &name.to_pascal(),
            def,
        }
        .render()?;
        if SEPARATED_BASE_FILES {
            let file_path = value_objects_base_dir.join(format!("_{}.rs", mod_name));
            remove_files.remove(file_path.as_os_str());
            fs_write(file_path, format!("{}{}", OVERWRITTEN_MSG, tpl))?;
        } else {
            output.push_str(&format!("pub mod _{} {{\n{}}}\n", mod_name, tpl));
        }

        let file_path = value_objects_dir.join(format!("{}.rs", mod_name));
        remove_files.remove(file_path.as_os_str());
        let tpl = DomainValueObjectWrapperTemplate {
            mod_name,
            pascal_name: &name.to_pascal(),
        }
        .render()?;
        if force || !file_path.exists() {
            fs_write(file_path, tpl)?;
        }
    }
    if !SEPARATED_BASE_FILES {
        let file_path = value_objects_dir.join("_base.rs");
        remove_files.remove(file_path.as_os_str());
        fs_write(file_path, output)?;
    }
    Ok(())
}

pub fn write_models_rs(base_domain_src_dir: &Path, db: &str) -> Result<()> {
    #[derive(Template)]
    #[template(
        source = r###"
pub mod @{ db|snake|ident }@;
// Do not modify this line. (Mod)"###,
        ext = "txt",
        escape = "none"
    )]
    pub struct ModTemplate<'a> {
        pub db: &'a str,
    }

    let file_path = base_domain_src_dir.join("models.rs");
    let mut content = if !file_path.exists() {
        #[derive(Template)]
        #[template(path = "domain/base_domain/src/models.rs", escape = "none")]
        struct DomainModelsTemplate;

        DomainModelsTemplate.render()?
    } else {
        fs::read_to_string(&file_path)?.replace("\r\n", "\n")
    };
    let chk = format!("\npub mod {};\n", _to_ident_name(&db.to_snake()));
    if !content.contains(&chk) {
        let tpl = ModTemplate { db }.render()?;
        content = content.replace("// Do not modify this line. (Mod)", tpl.trim_start());
    }

    fs_write(&file_path, &*content)?;
    Ok(())
}

pub fn write_models_db_rs(
    domain_models_dir: &Path,
    db: &str,
    groups: &GroupsDef,
    force: bool,
) -> Result<()> {
    let file_path = domain_models_dir.join(format!("{}.rs", &db.to_snake()));
    let content = if force || !file_path.exists() {
        #[derive(Template)]
        #[template(path = "domain/base_domain/src/models/db.rs", escape = "none")]
        pub struct DomainDbTemplate;

        DomainDbTemplate.render()?
    } else {
        fs::read_to_string(&file_path)?.replace("\r\n", "\n")
    };

    let re = Regex::new(r"(?s)// Do not modify below this line. \(ModStart\).+// Do not modify above this line. \(ModEnd\)").unwrap();
    ensure!(
        re.is_match(&content),
        "File contents are invalid.: {:?}",
        &file_path
    );

    #[derive(Template)]
    #[template(
        source = r###"
// Do not modify below this line. (ModStart)
@%- for (name, defs) in groups %@
pub mod @{ name|snake|ident }@;
@%- endfor %@
// Do not modify above this line. (ModEnd)"###,
        ext = "txt",
        escape = "none"
    )]
    pub struct ModTemplate<'a> {
        pub groups: &'a GroupsDef,
    }

    let tpl = ModTemplate { groups }.render()?;
    let tpl = tpl.trim_start();
    let content = re.replace(&content, tpl);

    fs_write(file_path, &*content)?;
    Ok(())
}

#[allow(clippy::too_many_arguments)]
pub fn write_abstract(
    domain_db_dir: &Path,
    db: &str,
    config: &ConfigDef,
    group_name: &String,
    mod_name: &str,
    model_name: &String,
    def: &Arc<ModelDef>,
    remove_files: &mut HashSet<OsString>,
) -> Result<String, anyhow::Error> {
    set_domain_mode(true);
    let pascal_name = &model_name.to_pascal();
    #[derive(Template)]
    #[template(
        path = "domain/base_domain/src/models/entities/abstract.rs",
        escape = "none"
    )]
    pub struct DomainAbstractTemplate<'a> {
        pub db: &'a str,
        pub config: &'a ConfigDef,
        pub group_name: &'a str,
        pub mod_name: &'a str,
        pub pascal_name: &'a str,
        pub def: &'a Arc<ModelDef>,
    }

    let tpl = DomainAbstractTemplate {
        db,
        config,
        group_name,
        mod_name,
        pascal_name,
        def,
    };
    let ret = tpl.render()?;

    set_domain_mode(false);
    if SEPARATED_BASE_FILES {
        let domain_group_dir = domain_db_dir.join(group_name.to_snake());
        let file_path = domain_group_dir.join(format!("{}.rs", mod_name));
        remove_files.remove(file_path.as_os_str());
        fs_write(file_path, format!("{}{}", OVERWRITTEN_MSG, ret))?;
        Ok(";\n".to_string())
    } else {
        Ok(format!(" {{\n{}}}\n", ret))
    }
}

#[allow(clippy::too_many_arguments)]
pub fn write_entity(
    domain_db_dir: &Path,
    db: &str,
    config: &ConfigDef,
    group_name: &String,
    mod_name: &str,
    model_name: &String,
    def: &Arc<ModelDef>,
    remove_files: &mut HashSet<OsString>,
) -> Result<String, anyhow::Error> {
    set_domain_mode(true);
    let pascal_name = &model_name.to_pascal();
    let id_name = &to_id_name(model_name);
    let model_id: u64 = if let Some(model_id) = def.model_id {
        model_id
    } else {
        use crc::{CRC_64_ECMA_182, Crc};
        pub const CRC64: Crc<u64> = Crc::<u64>::new(&CRC_64_ECMA_182);
        CRC64.checksum(format!("{db}:{group_name}:{mod_name}").as_bytes())
    };

    #[derive(Template)]
    #[template(
        path = "domain/base_domain/src/models/entities/entity.rs",
        escape = "none"
    )]
    pub struct DomainEntityTemplate<'a> {
        pub db: &'a str,
        pub config: &'a ConfigDef,
        pub group_name: &'a str,
        pub mod_name: &'a str,
        pub pascal_name: &'a str,
        pub id_name: &'a str,
        pub def: &'a Arc<ModelDef>,
        pub model_id: u64,
    }

    let tpl = DomainEntityTemplate {
        db,
        config,
        group_name,
        mod_name,
        pascal_name,
        id_name,
        def,
        model_id,
    };
    let ret = tpl.render()?;
    set_domain_mode(false);
    if SEPARATED_BASE_FILES {
        let domain_group_dir = domain_db_dir.join(group_name.to_snake());
        let file_path = domain_group_dir.join(format!("{}.rs", mod_name));
        remove_files.remove(file_path.as_os_str());
        fs_write(file_path, format!("{}{}", OVERWRITTEN_MSG, ret))?;
        Ok(";\n".to_string())
    } else {
        Ok(format!(" {{\n{}}}\n", ret))
    }
}

pub fn write_group_rs(
    domain_db_dir: &Path,
    group_name: &String,
    base_domain_output: String,
    remove_files: &mut HashSet<OsString>,
) -> Result<()> {
    let file_path = domain_db_dir.join(format!("{}.rs", group_name.to_snake()));
    remove_files.remove(file_path.as_os_str());
    #[derive(Template)]
    #[template(path = "domain/base_domain/src/models/group.rs", escape = "none")]
    struct DomainGroupTemplate {
        pub base_domain_output: String,
    }

    let content = DomainGroupTemplate { base_domain_output }.render()?;

    fs_write(file_path, &*content)?;
    Ok(())
}
