//! ngemsite

use clap::Parser;
use etcetera::{AppStrategy, AppStrategyArgs, choose_app_strategy};
use fluffer::App;
use nostr_lmdb::NostrLMDB;
use nostr_sdk::prelude::*;
use qpidfile::Pidfile;
use url_builder::URLBuilder;

use colored::*;
use confique::{Config, toml};
use std::fs;
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;
use std::sync::Arc;
use terminal_prompt::Terminal;
use tokio::sync::Mutex;

mod certgen;
mod cli;
mod config;
mod error;
mod gateway;
mod site;

use cli::{Args, Command};
use config::{GatewayConfig, SiteConfig};
use error::ServiceError;
use gateway::route_catchall;

pub(crate) type FlufferClient = fluffer::Client<Arc<Mutex<GnContext>>>;

pub(crate) struct GnContext {
    client: Arc<Client>,
}

async fn setup_client(client: Arc<Client>, config: GatewayConfig) {
    for relay in config.relays {
        if let Err(_) = client.add_relay(&relay).await {
            eprintln!("Failed to add relay: {relay}");
        }
    }

    client.connect().await;

    if let Err(_) = client
        .subscribe(
            Filter::new()
                .kind(Kind::Custom(15128))
                .kind(Kind::Custom(35128)),
            None,
        )
        .await
    {}
}

async fn run_gateway(
    config: GatewayConfig,
    data_dir: PathBuf,
    pid_file_path: PathBuf,
    cert: PathBuf,
    key: PathBuf,
) -> Result<(), fluffer::AppErr> {
    let db_path = data_dir.join("nostr_db");
    let _pidfile = Pidfile::new(pid_file_path);

    let Ok(database) = NostrLMDB::open(db_path.display().to_string()) else {
        panic!("Cannot open database");
    };

    let client = Arc::new(Client::builder().database(database).build());
    let context = Arc::new(Mutex::new(GnContext {
        client: client.clone(),
    }));

    tokio::spawn(setup_client(client.clone(), config.clone()));

    let mut gateway_url = URLBuilder::new();
    gateway_url
        .set_protocol("gemini")
        .set_host(&config.address.to_string())
        .set_port(config.port);

    println!(
        "{}: {}",
        "Gateway URL".green(),
        gateway_url.build().yellow()
    );

    App::default()
        .state(context)
        .path_to_cert(cert)
        .path_to_key(key)
        .address(format!("{}:{}", config.address, config.port))
        .route("/", route_catchall)
        .route("/*path", route_catchall)
        .run()
        .await
}

#[tokio::main]
async fn main() -> Result<(), ServiceError> {
    let Ok(strategy) = choose_app_strategy(AppStrategyArgs {
        top_level_domain: "org.codeberg".to_string(),
        author: "cipres".to_string(),
        app_name: "ngemsite".to_string(),
    }) else {
        panic!(
            "Failed to determine locations of config and data files on your system"
        );
    };

    let main_cfg_dir = strategy.config_dir();
    let main_data_dir = strategy.data_dir();
    let gateways_cfg_dir = main_cfg_dir.join("gateways");
    let args = Args::parse();

    std::fs::create_dir_all(&main_data_dir)?;

    match args.command {
        Command::GwRun { id, pid_file } => {
            let gw_cfg_dir = gateways_cfg_dir.join(id);
            std::fs::create_dir_all(&gw_cfg_dir)?;

            let cfg_path = gw_cfg_dir.join("config.toml");
            let key_path = gw_cfg_dir.join("key.pem");
            let cert_path = gw_cfg_dir.join("cert.pem");

            let Ok(config) =
                GatewayConfig::builder().env().file(cfg_path).load()
            else {
                panic!("Cannot load gw configuration");
            };

            run_gateway(
                config,
                main_data_dir,
                pid_file.unwrap_or(PathBuf::from("ngemsite-gateway.pid")),
                cert_path,
                key_path,
            )
            .await
            .map_err(|fluffer_err| ServiceError::FlufferAppError(fluffer_err))
        }
        Command::GwInit { id, hostname } => {
            let gw_cfg_dir = gateways_cfg_dir.join(id);
            let cfg_path = gw_cfg_dir.join("config.toml");
            let key_path = gw_cfg_dir.join("key.pem");
            let cert_path = gw_cfg_dir.join("cert.pem");

            std::fs::create_dir_all(&gw_cfg_dir)?;

            if !cfg_path.exists() {
                let mut file = File::create(&cfg_path)?;

                let template = toml::template::<GatewayConfig>(
                    toml::FormatOptions::default(),
                );
                file.write_all(&template.into_bytes())?;
            }

            if (!key_path.exists() || !cert_path.exists())
                && let Err(err) = certgen::gen_cert(
                    vec![format!("*.{}", hostname)],
                    &cert_path,
                    &key_path,
                )
            {
                panic!("Failed to create self-signed certificate: {err}");
            }

            println!("Gateway configuration file: {}", cfg_path.display());

            Ok(())
        }
        Command::GwList => {
            let entries = fs::read_dir(gateways_cfg_dir)?;
            for entry in entries {
                let entry = entry?;
                let Ok(file_type) = entry.file_type() else {
                    continue;
                };

                if file_type.is_dir() {
                    println!("{}", entry.file_name().to_string_lossy());
                }
            }

            Ok(())
        }
        Command::Create { path } => {
            let public_path = path.join("public");
            let cfg_path = path.join(".ngemsite.toml");

            std::fs::create_dir_all(&path)?;
            std::fs::create_dir_all(&public_path)?;

            if !cfg_path.exists() {
                let mut file = File::create(&cfg_path)?;

                let template = toml::template::<SiteConfig>(
                    toml::FormatOptions::default(),
                );
                file.write_all(&template.into_bytes())?;
            }

            Ok(())
        }
        Command::Deploy { path, sec, force } => {
            let cfg_path = path.join(".ngemsite.toml");
            let Ok(config) = SiteConfig::builder().env().file(cfg_path).load()
            else {
                panic!("Cannot load gw configuration");
            };

            let pk = match sec {
                Some(key) => key,
                None => {
                    let mut terminal = Terminal::open()?;
                    terminal.prompt_sensitive(
                        "Copy-paste your nostr private key: ",
                    )?
                }
            };

            let Ok(keys) = Keys::parse(&pk) else {
                panic!("Invalid private key");
            };

            let npub = keys.public_key().to_bech32().unwrap();

            let mut nsite_https_url = URLBuilder::new();
            nsite_https_url
                .set_protocol("https")
                .set_host(&format!("{}.nsite.lol", npub));

            let mut nsite_gemini_url = URLBuilder::new();
            nsite_gemini_url
                .set_protocol("gemini")
                .set_host(&format!("{}.localhost", npub));

            let signer = Arc::new(keys);

            match site::site_deploy(path, config, signer.clone(), force).await {
                Ok(_) => {
                    println!("=> {}", "Success".green());

                    println!("==> {}", nsite_https_url.build().cyan().bold());
                    println!("==> {}", nsite_gemini_url.build().green().bold());
                }
                Err(err) => println!(
                    "{}: {}",
                    "Error deploying site".red(),
                    format!("{err}").red().bold()
                ),
            }

            Ok(())
        }
    }
}
