#![doc = include_str!("../README.md")]
#![warn(clippy::all, clippy::pedantic)]
#![allow(clippy::missing_errors_doc)]

use std::{
    collections::BTreeMap,
    fs::{File, read_to_string},
    io::Write,
    path::{Path, PathBuf},
};

use configinator::{AppConfig, TemplateRefFieldBind};
use serde_json::Value;

#[allow(clippy::too_many_lines)]
pub async fn download(path: &str, url: &str, dest: &str) -> Result<(), Box<dyn std::error::Error>> {
    let path = Path::new(path);

    let config_str = read_to_string(path.join("./ordinary.json"))?;
    let config: AppConfig = serde_json::from_str(&config_str)?;

    // def name, field name, field value
    let mut indexed_vals: BTreeMap<String, Vec<(String, Value)>> = BTreeMap::new();

    if let Some(content) = config.content {
        let mut def_map = BTreeMap::new();

        for def in content.definitions {
            def_map.insert(def.name.clone(), def);
        }

        let objects_json = read_to_string(path.join(&content.file_path))?;

        let objects: Vec<notatype::ContentObject> = serde_json::from_str(&objects_json)?;

        for object in objects {
            if let Some(def) = def_map.get(&object.instance_of) {
                for def_field in &def.fields {
                    if def_field.indexed == Some(true) {
                        for obj_field in &object.fields {
                            if def_field.name == obj_field.name {
                                if let Some(vec) = indexed_vals.get_mut(&def.name) {
                                    vec.push((obj_field.name.clone(), obj_field.value.clone()));
                                } else {
                                    indexed_vals.insert(
                                        def.name.clone(),
                                        vec![(obj_field.name.clone(), obj_field.value.clone())],
                                    );
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    if let Some(templates) = config.templates {
        let client = reqwest::Client::new();

        for template_config in templates {
            if template_config.models.is_some() {
                tracing::info!(
                    "skipping template {} because models cannot be statically generated.",
                    template_config.name
                );
                continue;
            }

            let mut request_routes = vec![];

            if let Some(template_content) = template_config.content {
                let mut has_bindings = false;

                for content_ref in template_content {
                    for ref_field in content_ref.fields {
                        if let Some(binding) = ref_field.bind {
                            match binding {
                                TemplateRefFieldBind::Segment {
                                    name,
                                    expression: _,
                                } => {
                                    has_bindings = true;

                                    if let Some(vals) = indexed_vals.get(&content_ref.name) {
                                        for (field_name, val) in vals {
                                            if field_name == &ref_field.name
                                                && let Some(val) = val.as_str()
                                            {
                                                request_routes.push(
                                                    template_config
                                                        .route
                                                        .replace(&format!("{{{name}}}"), val),
                                                );
                                            }
                                        }
                                    }
                                }
                                TemplateRefFieldBind::Token {
                                    field: _,
                                    expression: _,
                                } => {
                                    tracing::warn!(
                                        "cannot generate for templates whose content binds to token fields"
                                    );
                                }
                            }
                        }
                    }
                }

                if !has_bindings {
                    request_routes.push(template_config.route);
                }
            }

            tracing::info!("generating files for template \"{}\"", template_config.name);

            // for request_route in &request_routes {
            // todo: create the matrix of route permutations for multi-slug
            // }

            for request_route in request_routes {
                tracing::info!("route: \"{}\"", request_route);

                let res = client
                    .get(format!("{url}{request_route}"))
                    .send()
                    .await?
                    .bytes()
                    .await?;

                let mut path: PathBuf = Path::new(&format!("{dest}{request_route}")).into();

                path.set_extension("");

                let file_path = if request_route.ends_with('/') {
                    std::fs::create_dir_all(&path)?;

                    match template_config.mime.as_str() {
                        "text/html" => path.join("index.html"),
                        "text/xml" => path.join("index.xml"),
                        "application/rss+xml" => path.join("feed.xml"),
                        "text/plain" => path.join("index.txt"),
                        _ => path.join("unknown"),
                    }
                } else if let Some(file_name) = path.file_name()
                    && let Some(file_name) = file_name.to_str()
                    && let Some(path) = path.parent()
                {
                    std::fs::create_dir_all(path)?;

                    match template_config.mime.as_str() {
                        "text/html" => path.join(format!("{file_name}.html")),
                        "text/xml" | "application/rss+xml" => path.join(format!("{file_name}.xml")),
                        "text/plain" => path.join(format!("{file_name}.txt")),
                        _ => path.join(file_name),
                    }
                } else {
                    return Err("no file_name or path".into());
                };

                tracing::info!("output: {:?}", file_path);

                let mut file = File::create(file_path)?;
                file.write_all(&res)?;
            }
        }
    }

    Ok(())
}
