// Copyright (C) 2026 Ordinary Labs, LLC.
//
// SPDX-License-Identifier: AGPL-3.0-only

use crate::{ProvisionMode, traverse};
use getrandom::SysRng;
use getrandom::rand_core::Rng;
use hickory_client::client::Client;
use hickory_client::proto::runtime::TokioRuntimeProvider;
use hickory_client::proto::udp::UdpClientStream;
use ordinary_app::server::OrdinaryAppServer;
use ordinary_auth::Auth;
use ordinary_config::{OrdinaryApiLimits, OrdinaryConfig};
use ordinary_storage::saferlmdb::EnvBuilder;
use ordinary_storage::{Storage, saferlmdb};
use ordinary_utils::shutdown_signal;
use rand_chacha::rand_core::SeedableRng;
use std::fs::File;
use std::io::Write;
use std::net::{Ipv6Addr, SocketAddr};
use std::path::Path;
use std::sync::Arc;
use tokio::net::TcpListener;

#[allow(
    clippy::fn_params_excessive_bools,
    clippy::too_many_arguments,
    clippy::too_many_lines,
    clippy::missing_panics_doc
)]
pub async fn run(
    project_dir: &str,
    domain_override: &Option<String>,
    data_dir: &str,
    log_size: bool,
    insecure: bool,
    insecure_cookies: bool,
    log_headers: bool,
    log_ips: bool,
    port: Option<u16>,
    redirect_port: Option<u16>,
    provision: &ProvisionMode,
) -> anyhow::Result<()> {
    tracing::debug!("installing standalone project...");

    let project_path = Path::new(&project_dir);

    tracing::debug!("reading config...");
    let config_str = std::fs::read_to_string(project_path.join("ordinary.json"))?;
    let mut config: OrdinaryConfig = serde_json::from_str(&config_str)?;

    // todo: make public dns IP a CLI flag
    let address = SocketAddr::from(([8, 8, 8, 8], 53));
    let conn = UdpClientStream::builder(address, TokioRuntimeProvider::default()).build();

    let (client, bg) = Client::connect(conn).await?;
    let bg = tokio::spawn(bg);

    OrdinaryAppServer::validate_cnames(&config, &Arc::new(tokio::sync::Mutex::new(client))).await?;
    bg.abort();

    if let Some(domain_override) = &domain_override {
        config.domain = domain_override.clone();
    }

    let data_dir = Path::new(data_dir).join("apps").join(&config.domain);

    tracing::debug!("setting up DB environment...");
    let store_path = data_dir.join("store");
    std::fs::create_dir_all(&store_path)?;

    let ps = page_size::get() as u64;

    // round up to full OS page
    let remainder = config.storage_size % ps;
    let mapsize = (config.storage_size - remainder) + ps;

    tracing::info!(mapsize = %bytesize::ByteSize(mapsize).display().si_short());

    let env = Arc::new(unsafe {
        let mut env_builder = EnvBuilder::new()?;
        env_builder.set_maxreaders(126)?;
        env_builder.set_mapsize(usize::try_from(mapsize)?)?;
        env_builder.set_maxdbs(13)?;
        env_builder.open(
            store_path.to_str().expect("store_path not a str"),
            &saferlmdb::open::Flags::empty(),
            0o600,
        )?
    });

    tracing::debug!("setting up auth...");

    let keys_dir = data_dir.join("keys");
    std::fs::create_dir_all(&keys_dir)?;

    let auth_key_path = keys_dir.join("auth");

    let auth_key: [u8; 32] = if auth_key_path.exists() && auth_key_path.is_file() {
        let auth_key = std::fs::read(&auth_key_path)?;
        let auth_key: [u8; 32] = auth_key[..].try_into()?;
        auth_key
    } else {
        let mut auth_key = [0u8; 32];
        let mut rng = rand_chacha::ChaCha20Rng::try_from_rng(&mut SysRng)?;

        rng.fill_bytes(&mut auth_key[..]);

        let mut auth_key_file = File::create(auth_key_path)?;
        auth_key_file.write_all(&auth_key)?;

        auth_key
    };

    tracing::debug!("initializing auth...");

    let auth = Arc::new(Auth::new(
        config.domain.clone(),
        config.auth.clone(),
        auth_key,
        env.clone(),
    )?);

    tracing::debug!("initializing storage...");

    let storage_key_path = keys_dir.join("storage");

    let storage_key: [u8; 32] = if storage_key_path.exists() && storage_key_path.is_file() {
        let storage_key = std::fs::read(&storage_key_path).expect("failed to read storage key");
        let storage_key: [u8; 32] = storage_key[..]
            .try_into()
            .expect("failed to get fixed storage key");
        storage_key
    } else {
        let mut storage_key = [0u8; 32];
        let mut rng = rand_chacha::ChaCha20Rng::try_from_rng(&mut SysRng)?;

        rng.fill_bytes(&mut storage_key[..]);

        let mut storage_key_file =
            File::create(storage_key_path).expect("failed to create storage key file");
        storage_key_file
            .write_all(&storage_key)
            .expect("failed to write storage key");

        storage_key
    };

    let limits = OrdinaryApiLimits::default();

    let storage = Arc::new(Storage::new(
        limits.storage.clone(),
        if let Some(models) = &config.models {
            models.clone()
        } else {
            vec![]
        },
        if let Some(content) = &config.content {
            content.definitions.clone()
        } else {
            vec![]
        },
        storage_key,
        &env,
        store_path.join("search"),
        log_size,
    )?);

    tracing::debug!("instantiating app...");
    let app = Arc::new(OrdinaryAppServer::new(
        config.clone(),
        limits,
        auth,
        storage,
        !insecure,
        !insecure_cookies,
        log_headers,
        log_ips,
        None,
        None,
        None,
    )?);

    let app_clone = app.clone();

    tracing::debug!("copying content...");
    if let Some(content) = config.content {
        let content_path = Path::new(&content.file_path);
        let objects_json = std::fs::read_to_string(project_path.join(content_path))?;

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

        app.update_content(&objects)?;
    }

    tracing::debug!("copying assets...");
    if let Some(assets) = config.assets {
        let parent_path = &project_path.join(&assets.dir_path);

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

            if path.ends_with(".DS_Store") {
                tracing::warn!("ignoring .DS_Store");
            } else if let Ok(content) = std::fs::read(&path)
                && let Ok(path) = path.strip_prefix(parent_path)
                && let Some(path) = path.to_str()
                && let Err(err) = app_clone.put_asset(path, &content)
            {
                tracing::error!(%err);
            }
        })?;
    }

    if config.auth.is_some() || config.client_events == Some(true) {
        let core_js =
            std::fs::read_to_string(project_path.join("./.ordinary/gen/client/js/core.js"))?;

        app.put_asset(
            &format!("{}/js/core.js", config.version),
            core_js.replace("{{ version }}", &config.version).as_bytes(),
        )?;
    }

    if config.auth.is_some() {
        let js_client_js =
            std::fs::read_to_string(project_path.join("./.ordinary/gen/client/js/client.js"))?;

        app.put_asset(
            &format!("{}/js/client.js", config.version),
            js_client_js
                .replace("{{ version }}", &config.version)
                .as_bytes(),
        )?;
    }

    if config.auth.is_some()
        || config.obfuscation == Some(true)
        || config.client_rendering == Some(true)
    {
        let client_js =
            std::fs::read_to_string(project_path.join("./.ordinary/gen/client/wasm/client.js"))?;
        let client_wasm = std::fs::read(
            Path::new(project_path).join(".ordinary/gen/client/wasm/client_bg_opt.wasm"),
        )
        .or_else(|_| {
            std::fs::read(Path::new(project_path).join(".ordinary/gen/client/wasm/client_bg.wasm"))
        })?;

        app.put_asset(
            &format!("{}/wasm/client.js", config.version),
            client_js
                .replace("{{ version }}", &config.version)
                .as_bytes(),
        )?;
        app.put_asset(
            &format!("{}/wasm/client_bg.wasm", config.version),
            &client_wasm,
        )?;
    }

    if let Some(actions) = config.actions {
        if !actions.is_empty() {
            tracing::debug!("installing actions...");
        }

        for config in actions {
            let path = project_path
                .join(&config.dir_path)
                .join("target/wasm32-wasip1/release/action.wasm");
            let action_bytes = std::fs::read(path)?;

            app.set_action(config.idx, action_bytes.as_slice())?;
        }
    }

    if let Some(templates) = config.templates {
        if !templates.is_empty() {
            tracing::debug!("installing templates...");
        }

        for config in templates {
            let path = project_path
                .join(".ordinary")
                .join("gen")
                .join("server")
                .join(&config.name)
                .join("target/wasm32-wasip1/release/template.wasm");
            let template_bytes = std::fs::read(path)?;

            let path = project_path
                .join(".ordinary")
                .join("gen")
                .join("hashes")
                .join(&config.name);

            let style_path = path.join("style-src.json");
            let script_path = path.join("script-src.json");

            let csp_style_hashes = if style_path.exists() {
                let csp_style_hashes: Vec<String> =
                    serde_json::from_str(&std::fs::read_to_string(style_path)?)?;
                Some(csp_style_hashes)
            } else {
                None
            };
            let csp_script_hashes = if script_path.exists() {
                let csp_script_hashes: Vec<String> =
                    serde_json::from_str(&std::fs::read_to_string(script_path)?)?;
                Some(csp_script_hashes)
            } else {
                None
            };

            app.set_template(
                config.idx,
                template_bytes.as_slice(),
                csp_style_hashes,
                csp_script_hashes,
                !insecure,
            )?;
        }
    }

    let port = if insecure {
        port.unwrap_or(config.port.unwrap_or(80))
    } else {
        port.unwrap_or(config.port.unwrap_or(443))
    };

    let redirect_listener = if insecure {
        None
    } else {
        let redirect_port = redirect_port.unwrap_or(config.redirect_port.unwrap_or(80));

        Some(TcpListener::bind((Ipv6Addr::UNSPECIFIED, redirect_port)).await?)
    };

    let listener = TcpListener::bind((Ipv6Addr::UNSPECIFIED, port)).await?;

    app.start(
        listener,
        redirect_listener,
        if insecure {
            ordinary_app::server::SecurityMode::Insecure
        } else {
            ordinary_app::server::SecurityMode::Secure(
                Path::new(&data_dir).join(&config.domain).join("certs"),
                match provision {
                    ProvisionMode::Localhost => ordinary_app::server::ProvisionMode::Localhost,
                    ProvisionMode::Staging => ordinary_app::server::ProvisionMode::Staging,
                    ProvisionMode::Production => ordinary_app::server::ProvisionMode::Production,
                },
            )
        },
        None,
        shutdown_signal,
    )
    .await
}
