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

use std::error::Error;
use std::{io::Write, path::Path};

use configinator::{
    ActionConfig, AppConfig, Content, ContentDefinition, IntegrationConfig, IntegrationProtocol,
    ModelConfig, TemplateCache, TemplateConfig, UuidVersion,
};
use heck::ToTitleCase;
use notatype::ContentObject;
use tracing::instrument;

#[instrument(err)]
pub fn create_project(path: &String, domain: &String) -> Result<(), Box<dyn Error>> {
    let path = Path::new(path).join(domain);

    tracing::info!("creating project at {:?}", path);
    std::fs::create_dir_all(&path)?;

    let app_config = AppConfig {
        domain: domain.clone(),
        version: "0.1.0".into(),
        contacts: vec![],

        hide_schema: None,

        client_e2ee: None,
        client_rendering: None,
        client_tracing: None,

        port: None,
        redirect_port: None,
        logging: None,
        // todo: set this on last arm of path
        globals: None,
        secrets: None,

        // todo: all of these should be set with defaults
        assets: None,
        content: None,
        error: None,
        // todo /
        flags: None,

        templates: None,
        auth: None,
        actions: None,
        models: None,
        integrations: None,
    };

    let ordinary_json = serde_json::to_string_pretty(&app_config)?;

    let mut file = std::fs::File::create(path.join("ordinary.json"))?;
    file.write_all(ordinary_json.as_bytes())?;

    if let Some(path) = path.to_str() {
        add_template(
            &path.into(),
            &"index".into(),
            &"/".into(),
            &"text/html".into(),
        )?;
    }

    Ok(())
}

#[instrument(err)]
#[allow(
    clippy::too_many_arguments,
    clippy::used_underscore_binding,
    clippy::too_many_lines
)]
pub fn add_action(
    path: &String,
    name: &String,
    lang: &String,
    _access: (),
    _accepts: (),
    _returns: (),
    transactional: &Option<bool>,
    _protected: (),
) -> Result<(), Box<dyn Error>> {
    let proj_path = Path::new(path);
    let ordinary_json = std::fs::read_to_string(proj_path.join("ordinary.json"))?;

    let mut app_config: AppConfig = serde_json::from_str(&ordinary_json)?;

    let mut actions = app_config.actions.unwrap_or_default();

    if actions.len() <= 225 {
        let next_idx = actions.len();

        let language = match lang.as_str() {
            "Rust" | "rs" => configinator::ActionLang::Rust,
            "JavaScript" | "JS" | "js" => configinator::ActionLang::JavaScript,
            _ => return Err(format!("language {lang} not yet supported").into()),
        };

        let action_dir_path = proj_path.join("actions").join(name);
        std::fs::create_dir_all(&action_dir_path)?;

        match language {
            configinator::ActionLang::Rust => {
                let gitignore = "/target";

                let cargo_toml = format!(
                    r#"[workspace]

[package]
name = "action"
version = "0.1.0"
edition = "2024"

[dependencies]
ordinary = {{ path = "../../.ordinary/gen/actions/{name}" }}

[profile.release]
strip = "symbols"
lto = "fat"
opt-level = "z"
codegen-units = 1
panic = "abort"
"#
                );

                let main_rs = r#"use std::error::Error;
use ordinary::{recv_in, send_out};

fn main() -> Result<(), Box<dyn Error>> {
    let _input = recv_in()?;
    
    ordinary::trace(2, "Hello, Ordinary!")?;

    send_out(())
}
"#;

                let mut gitignore_file = std::fs::File::create(action_dir_path.join(".gitignore"))?;
                gitignore_file.write_all(gitignore.as_bytes())?;

                let mut cargo_toml_file =
                    std::fs::File::create(action_dir_path.join("Cargo.toml"))?;
                cargo_toml_file.write_all(cargo_toml.as_bytes())?;

                std::fs::create_dir_all(action_dir_path.join("src"))?;
                let mut main_rs_file =
                    std::fs::File::create(action_dir_path.join("src").join("main.rs"))?;
                main_rs_file.write_all(main_rs.as_bytes())?;
            }
            configinator::ActionLang::JavaScript => {
                let gitignore = r"/target
node_modules";

                let package_json = r#"{
  "name": "action",
  "version": "0.1.0",
  "main": "index.js",
  "files": [
    "index.js",
    "index.d.ts"
  ],
  "types": "client.d.ts",
  "scripts": {
    "build": "esbuild main.js --bundle --minify --outfile=index.js --target=es2020 --format=iife --global-name=action"
  },
  "devDependencies": {
    "esbuild": "0.25.10"
  }
}
                "#;

                let main_js = r#"export function main(input) {
    console.log("Hello, Ordinary!");                
}"#;

                let mut gitignore_file = std::fs::File::create(action_dir_path.join(".gitignore"))?;
                gitignore_file.write_all(gitignore.as_bytes())?;

                let mut package_json_file =
                    std::fs::File::create(action_dir_path.join("package.json"))?;
                package_json_file.write_all(package_json.as_bytes())?;

                let mut main_js_file = std::fs::File::create(action_dir_path.join("main.js"))?;
                main_js_file.write_all(main_js.as_bytes())?;
            }
        }

        actions.push(ActionConfig {
            idx: u8::try_from(next_idx)?,
            dir_path: format!("./actions/{name}"),
            name: name.clone(),
            lang: language,
            access: vec![],
            accepts: notatype::Kind::Void,
            returns: notatype::Kind::Void,
            triggered_by: vec![],
            transactional: *transactional,
            protected: None,
        });

        app_config.actions = Some(actions);

        let ordinary_json = serde_json::to_string_pretty(&app_config)?;

        let mut file = std::fs::File::create(proj_path.join("ordinary.json"))?;
        file.write_all(ordinary_json.as_bytes())?;
    } else {
        tracing::error!("cannot support more than 255 actions for a single project.");
    }

    Ok(())
}

#[instrument(err)]
pub fn add_template(
    path: &String,
    name: &String,
    route: &String,
    mime: &String,
) -> Result<(), Box<dyn Error>> {
    let proj_path = Path::new(path);
    let ordinary_json = std::fs::read_to_string(proj_path.join("ordinary.json"))?;

    let mut app_config: AppConfig = serde_json::from_str(&ordinary_json)?;

    let mut templates = app_config.templates.unwrap_or_default();

    if templates.len() <= 255 {
        let next_idx = templates.len();

        let cache = Some(TemplateCache {
            stored: None,
            http: None,
        });

        let file_ext = match mime.as_str() {
            "text/html" => "html",
            "text/xml" => "xml",
            "text/plain" => "txt",
            _ => "",
        };

        std::fs::create_dir_all(proj_path.join("templates"))?;

        let file_name = format!("{name}.{file_ext}");

        let mut file = std::fs::File::create(proj_path.join("templates").join(&file_name))?;
        // todo: have a default file for each extension/mime type

        match mime.as_str() {
            "text/html" => {
                let default_html = format!(
                    r#"<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>

<body>
    <header><a href="/">{{{{ domain }}}}</a></header>
    <h1>{}</h1>
</body>
"#,
                    name.to_title_case()
                );
                file.write_all(default_html.as_bytes())?;
            }
            _ => {
                file.write_all(&[][..])?;
            }
        }

        templates.push(TemplateConfig {
            idx: u8::try_from(next_idx)?,
            name: name.clone(),
            // todo: verify that route begins with "/" and is valid HTTP route
            route: route.clone(),
            // todo: check against valid mime types
            mime: mime.clone(),
            cache,
            path: Some(format!("./templates/{file_name}")),
            protected: None,
            globals: None,
            flags: None,
            fields: None,
            params: None,
            content: None,
            models: None,
            actions: None,
            minify: None,
            compression_precedence: None,
        });

        app_config.templates = Some(templates);

        let ordinary_json = serde_json::to_string_pretty(&app_config)?;

        let mut file = std::fs::File::create(proj_path.join("ordinary.json"))?;
        file.write_all(ordinary_json.as_bytes())?;
    } else {
        tracing::error!("cannot support more than 255 templates for a single project.");
    }

    Ok(())
}

#[instrument(err)]
pub fn add_integration(
    path: &String,
    name: &String,
    endpoint: &String,
    protocol: &String,
) -> Result<(), Box<dyn Error>> {
    let proj_path = Path::new(path);
    let ordinary_json = std::fs::read_to_string(proj_path.join("ordinary.json"))?;

    let mut app_config: AppConfig = serde_json::from_str(&ordinary_json)?;

    let mut integrations = app_config.integrations.unwrap_or_default();

    if integrations.len() <= 255 {
        let next_idx = integrations.len();

        integrations.push(IntegrationConfig {
            idx: u8::try_from(next_idx)?,
            name: name.clone(),
            protocol: match protocol.as_str() {
                "REST" | "rest" => IntegrationProtocol::Http {
                    method: "GET".to_string(),
                    headers: vec![],
                    send_encoding: configinator::IntegrationProtocolHttpEncoding::Text,
                    recv_encoding: configinator::IntegrationProtocolHttpEncoding::Text,
                },
                _ => return Err("invalid protocol".into()),
            },
            endpoint: endpoint.clone(),
            send: notatype::Kind::Json,
            recv: notatype::Kind::Json,
            secrets: None,
        });

        app_config.integrations = Some(integrations);

        let ordinary_json = serde_json::to_string_pretty(&app_config)?;

        let mut file = std::fs::File::create(proj_path.join("ordinary.json"))?;
        file.write_all(ordinary_json.as_bytes())?;
    } else {
        tracing::error!("cannot support more than 255 integrations for a single project.");
    }

    Ok(())
}

#[instrument(err)]
pub fn add_content_def(path: &String, name: &String) -> Result<(), Box<dyn Error>> {
    let proj_path = Path::new(path);
    let ordinary_json = std::fs::read_to_string(proj_path.join("ordinary.json"))?;

    let mut app_config: AppConfig = serde_json::from_str(&ordinary_json)?;

    let mut file_path = "./content.json".to_string();

    let mut content_defs = if let Some(d) = app_config.content {
        file_path = d.file_path;
        d.definitions
    } else {
        let mut file = std::fs::File::create(proj_path.join("content.json"))?;
        file.write_all(b"[]")?;

        vec![]
    };

    if content_defs.len() < 255 {
        let next_idx = content_defs.len();

        content_defs.push(ContentDefinition {
            idx: u8::try_from(next_idx)?,
            name: name.clone(),
            fields: vec![],
        });

        app_config.content = Some(Content {
            definitions: content_defs,
            file_path,
        });

        let ordinary_json = serde_json::to_string_pretty(&app_config)?;

        let mut file = std::fs::File::create(proj_path.join("ordinary.json"))?;
        file.write_all(ordinary_json.as_bytes())?;
    } else {
        tracing::error!("cannot support more than 255 content definitions for a single project.");
    }

    Ok(())
}

#[instrument(err)]
pub fn add_content_obj(path: &String, object_json: &String) -> Result<(), Box<dyn Error>> {
    let proj_path = Path::new(path);
    let ordinary_json = std::fs::read_to_string(proj_path.join("ordinary.json"))?;

    let app_config: AppConfig = serde_json::from_str(&ordinary_json)?;

    if let Some(content) = app_config.content {
        let content_json = std::fs::read_to_string(proj_path.join(&content.file_path))?;

        let mut content_objects: Vec<ContentObject> = serde_json::from_str(&content_json)?;

        let new_obj: ContentObject = serde_json::from_str(object_json)?;

        // todo: do more to verify the structure of the object is compatible with its instance_of

        content_objects.push(new_obj);

        let new_objects = serde_json::to_string_pretty(&content_objects)?;

        let mut file = std::fs::File::create(proj_path.join(&content.file_path))?;
        file.write_all(new_objects.as_bytes())?;
    }

    Ok(())
}

#[instrument(err)]
pub fn add_model(
    path: &String,
    name: &String,
    uuid: &Option<String>,
) -> Result<(), Box<dyn Error>> {
    let proj_path = Path::new(path);
    let ordinary_json = std::fs::read_to_string(proj_path.join("ordinary.json"))?;

    let mut app_config: AppConfig = serde_json::from_str(&ordinary_json)?;

    let mut models = app_config.models.unwrap_or_default();

    if models.len() <= 255 {
        let next_idx = models.len();

        models.push(ModelConfig {
            idx: u8::try_from(next_idx)?,
            name: name.clone(),
            fields: vec![],
            uuid: match uuid {
                Some(uuid) => match uuid.as_str() {
                    "v4" | "4" => Some(UuidVersion::V4),
                    "v7" | "7" => Some(UuidVersion::V7),
                    _ => return Err("invalid UUID version".into()),
                },
                None => None,
            },
        });

        app_config.models = Some(models);

        let ordinary_json = serde_json::to_string_pretty(&app_config)?;

        let mut file = std::fs::File::create(proj_path.join("ordinary.json"))?;
        file.write_all(ordinary_json.as_bytes())?;
    } else {
        tracing::error!("cannot support more than 255 models for a single project.");
    }

    Ok(())
}
