#![warn(clippy::all, clippy::pedantic)]
#![allow(clippy::missing_errors_doc)]

use clap::{Parser, Subcommand};
use ordinary_admin::client::AdminClient;

use tracing_subscriber::fmt::format::FmtSpan;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;

use qrcodegen::{QrCode, QrCodeEcc};

#[derive(Parser)]
#[command(version, about, long_about = None)]
#[command(propagate_version = true)]
pub struct Cli {
    #[command(subcommand)]
    command: Commands,

    /// whether to print events to stdio
    #[arg(long, default_value_t = false)]
    verbose: bool,

    /// whether to pretty print events to stdio
    #[arg(long, default_value_t = false)]
    pretty: bool,

    /// DANGER: only use when working with self-signed localhost certs
    #[arg(long, default_value_t = false)]
    danger_accept_invalid_certs: bool,

    /// only necessary for localhost
    #[arg(long, default_value_t = String::new())]
    admin_domain: String,
}

#[derive(Subcommand)]
pub enum Commands {
    /// create a new project
    New {
        /// project name
        name: String,
        /// project domain
        domain: String,
    },
    /// add component
    Add {
        #[command(subcommand)]
        add_commands: AddCommands,
    },
    /// build a project
    Build {
        /// project path
        project: String,

        /// rebuild whole project even if nothing has changed
        #[arg(short, long, default_value_t = false)]
        ignore_cache: bool,
    },
    /// download the static template files
    Download {
        /// project path
        project: String,
        /// url for the application server
        url: String,
        /// destination for downloaded files
        dest: String,
    },

    /// register as admin account on the Ordinary server
    Register {
        domain: String,

        account: String,
        password: String,
        invite_code: String,
    },
    /// log in as admin account on the Ordinary server
    Login {
        domain: String,

        account: String,
        password: String,
        mfa_code: String,
    },
    /// log out of the Ordinary server
    Logout { domain: String },
    /// reset your account password
    ResetPassword {
        domain: String,
        account: String,
        old_password: String,
        new_password: String,
        mfa_code: String,
    },

    /// generate an invite code for an admin account
    InviteAdminAccount {
        domain: String,
        account: String,

        /// domain the invited account will be assigned to
        invite_domain: String,
        /// name of the new admin account
        invite_account: String,

        permissions: Vec<Permission>,
    },

    /// deploy an app config to an Ordinary Server
    Deploy {
        /// Ordinary Server domain
        domain: String,

        account: String,

        /// app dir
        app: String,

        /// for applications that need to consume stdio
        #[arg(long, default_value_t = false)]
        clean_port_to_stdio: bool,
    },
    /// patch an app config to an Ordinary Server
    Patch {
        /// Ordinary Server domain
        domain: String,

        account: String,

        /// app dir
        app: String,

        /// for applications that need to consume stdio
        #[arg(long, default_value_t = false)]
        clean_port_to_stdio: bool,
    },
    /// redeploy a running app (typically used for config updates)
    Migrate {
        domain: String,
        account: String,
        app: String,
    },

    /// bridge an app on this Ordinary Server to an instance
    /// running on another Ordinary Server.
    Bridge {
        domain: String,
        account: String,

        app: String,
        remote: String,
    },
    /// stop the running app's thread
    Kill {
        domain: String,
        account: String,
        app: String,
    },
    /// erase all app data from the Ordinary Server
    Erase {
        domain: String,
        account: String,
        app: String,
    },

    /// install an action onto an app
    Install {
        domain: String,
        account: String,

        /// App path
        app: String,
        /// action name
        action: String,
    },
    /// install all actions from local project onto an app
    InstallAll {
        domain: String,
        account: String,

        /// App path
        app: String,
    },
    /// upload a template to an app
    Upload {
        /// Ordinary Server
        domain: String,
        account: String,

        /// App path
        app: String,
        /// template name
        template: String,
    },
    /// upload all templates from local project to an app
    UploadAll {
        /// Ordinary Server
        domain: String,
        account: String,

        /// App path
        app: String,
    },
    /// write an asset to an app
    Write {
        /// Ordinary Server
        domain: String,
        account: String,

        /// App path
        app: String,

        /// Asset path relative to app
        asset: String,
    },
    /// write all assets from local to an app
    WriteAll {
        /// Ordinary Server
        domain: String,
        account: String,

        /// App path
        app: String,
    },
    /// update the content
    Update {
        /// Ordinary Server
        domain: String,
        account: String,

        /// App path
        app: String,
    },
    ListItems {
        /// Ordinary Server
        domain: String,
        account: String,

        /// App path
        app: String,

        #[arg(long)]
        /// Model idx
        model: u8,

        /// for applications that need to consume stdio
        #[arg(long, default_value_t = false)]
        clean_json_to_stdio: bool,
    },
    ListAppAccounts {
        /// Ordinary Server
        domain: String,
        account: String,

        /// App path
        app: String,

        /// for applications that need to consume stdio
        #[arg(long, default_value_t = false)]
        clean_json_to_stdio: bool,
    },
    AppLogs {
        /// Ordinary Server
        domain: String,
        account: String,

        /// App path
        app: String,

        #[arg(long)]
        /// query
        query: String,

        #[arg(long)]
        /// format
        format: String,

        #[arg(long)]
        /// limit
        limit: Option<usize>,

        /// for applications that need to consume stdio
        #[arg(long, default_value_t = false)]
        clean_json_to_stdio: bool,
    },

    /// list the admin accounts
    ListAdminAccounts {
        /// Ordinary server
        domain: String,
        account: String,

        /// App domain
        app_domain: String,

        /// for applications that need to consume stdio
        #[arg(long, default_value_t = false)]
        clean_json_to_stdio: bool,
    },
    /// delete an admin account
    DeleteAdminAccount {
        /// Ordinary server
        domain: String,
        account: String,

        /// App domain
        app_domain: String,

        /// Account name
        delete_account: String,

        /// for applications that need to consume stdio
        #[arg(long, default_value_t = false)]
        clean_json_to_stdio: bool,
    },
    /// logs for an application admin operations
    AdminLogs {
        /// Ordinary server
        domain: String,
        account: String,

        /// App domain
        app_domain: Option<String>,

        /// for applications that need to consume stdio
        #[arg(long, default_value_t = false)]
        clean_json_to_stdio: bool,
    },

    /// list the apps running on an Ordinary Server
    RootListApps { domain: String },
    /// all Ordinary server logs
    RootLogs {
        /// Ordinary server
        domain: String,

        /// for applications that need to consume stdio
        #[arg(long, default_value_t = false)]
        clean_json_to_stdio: bool,
    },
}

#[derive(Subcommand)]
pub enum AddCommands {
    Action {
        project: String,
        name: String,
        lang: String,
        transactional: Option<bool>,
    },
    Template {
        project: String,
        name: String,
        route: String,
        mime: String,
    },
    Integration {
        project: String,
        name: String,
        endpoint: String,
        protocol: String,
    },
    ContentDef {
        project: String,
        name: String,
    },
    ContentObj {
        project: String,
        object_json: String,
    },
    Model {
        project: String,
        name: String,
        uuid_version: Option<String>,
    },
}

/// permission for app operations on the Ordinary Server
#[derive(Clone)]
pub enum Permission {
    /// all
    Admin,
    /// logs, list items, list accounts, list apps
    Read,
    /// write assets
    Write,
    /// update content
    Update,
    /// upload templates
    Upload,
    /// install actions
    Install,
    /// deploy, patch and migrate applications
    Deploy,
    /// bridge applications on two separate instances
    Bridge,
    /// kill application
    Kill,
    /// erase the application
    Erase,
}

impl Permission {
    fn as_u8(&self) -> u8 {
        match self {
            Self::Admin => 0,
            Self::Read => 1,
            Self::Write => 2,
            Self::Update => 3,
            Self::Upload => 4,
            Self::Install => 5,
            Self::Deploy => 6,
            Self::Bridge => 7,
            Self::Kill => 8,
            Self::Erase => 9,
        }
    }
}

impl clap::ValueEnum for Permission {
    fn value_variants<'a>() -> &'a [Self] {
        &[
            Self::Admin,
            Self::Read,
            Self::Write,
            Self::Update,
            Self::Upload,
            Self::Install,
            Self::Deploy,
            Self::Bridge,
            Self::Kill,
            Self::Erase,
        ]
    }

    fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> {
        match self {
            Self::Admin => Some(clap::builder::PossibleValue::new("admin")),
            Self::Read => Some(clap::builder::PossibleValue::new("read")),
            Self::Write => Some(clap::builder::PossibleValue::new("write")),
            Self::Update => Some(clap::builder::PossibleValue::new("update")),
            Self::Upload => Some(clap::builder::PossibleValue::new("upload")),
            Self::Install => Some(clap::builder::PossibleValue::new("install")),
            Self::Deploy => Some(clap::builder::PossibleValue::new("deploy")),
            Self::Bridge => Some(clap::builder::PossibleValue::new("bridge")),
            Self::Kill => Some(clap::builder::PossibleValue::new("kill")),
            Self::Erase => Some(clap::builder::PossibleValue::new("erase")),
        }
    }
}

#[derive(Subcommand)]
pub enum UserSubcommands {
    /// list users
    List,
    /// create a user
    Create {
        app: String,
        account: String,
        password: String,
        permissions: Vec<Permission>,
    },
    /// delete a user
    Delete { app: String, account: String },
}

#[allow(clippy::too_many_lines)]
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let cli = Cli::parse();

    let pretty_layer = if cli.pretty {
        Some(
            tracing_subscriber::fmt::layer()
                .pretty()
                .with_span_events(FmtSpan::CLOSE),
        )
    } else {
        None
    };

    let ugly_layer = if cli.pretty {
        None
    } else {
        Some(
            tracing_subscriber::fmt::layer()
                .with_span_events(FmtSpan::CLOSE)
                .with_target(false),
        )
    };

    if cli.verbose || cli.pretty {
        tracing_subscriber::registry()
            .with(
                tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| {
                    "itsalive=debug,godfather=debug,cbwaw=debug,certifiably=debug,hammertime=debug,stewball=debug,stencilr=debug,ordinary=debug,skyhook=debug,startmeup=debug,tower_http=debug,axum::rejection=trace"
                        .into()
                }),
            )
            .with(pretty_layer)
            .with(ugly_layer)
            .init();
    }

    std::panic::set_hook(Box::new(|info| {
        if let Some(msg) = info.payload_as_str()
            && let Some(loc) = info.location()
        {
            tracing::error!(%loc, msg, "panic");
        } else if let Some(loc) = info.location() {
            tracing::error!(%loc, "panic");
        }
    }));

    let admin_domain = if cli.admin_domain.contains('.') {
        Some(cli.admin_domain.as_str())
    } else {
        None
    };

    match &cli.command {
        // commands for creating and modifying projects, locally
        Commands::New { name, domain } => ordinary_modify::create_project(name, domain)?,
        Commands::Add { add_commands } => match add_commands {
            AddCommands::Action {
                project,
                name,
                lang,
                transactional,
            } => {
                ordinary_modify::add_action(project, name, lang, (), (), (), transactional, ())?;
            }
            AddCommands::Template {
                project,
                name,
                route,
                mime,
            } => {
                ordinary_modify::add_template(project, name, route, mime)?;
            }
            AddCommands::Integration {
                project,
                name,
                endpoint,
                protocol,
            } => {
                ordinary_modify::add_integration(project, name, endpoint, protocol)?;
            }
            AddCommands::ContentDef { project, name } => {
                ordinary_modify::add_content_def(project, name)?;
            }
            AddCommands::ContentObj {
                project,
                object_json,
            } => {
                ordinary_modify::add_content_def(project, object_json)?;
            }
            AddCommands::Model {
                project,
                name,
                uuid_version,
            } => {
                ordinary_modify::add_model(project, name, uuid_version)?;
            }
        },
        Commands::Build {
            project,
            ignore_cache,
        } => ordinary_build::build(project, *ignore_cache)?,
        Commands::Download { project, url, dest } => {
            ordinary_download::download(project, url, dest).await?;
        }

        Commands::Register {
            domain,
            account,
            password,
            invite_code,
        } => {
            let client = AdminClient::new(
                domain,
                account,
                admin_domain,
                cli.danger_accept_invalid_certs,
            );

            let (totp, recovery_codes_str) = client.register(password, invite_code).await?;

            let mfa_url = totp.get_url();
            let qr: QrCode = QrCode::encode_text(&mfa_url, QrCodeEcc::Medium)?;

            let border: i32 = 4;
            for y in -border..qr.size() + border {
                for x in -border..qr.size() + border {
                    let c: char = if qr.get_module(x, y) { '█' } else { ' ' };
                    print!("{c}{c}");
                }
                println!();
            }
            println!("{mfa_url}");

            let mut recovery_code = String::new();
            let mut recovery_codes: Vec<String> = vec![];

            for (i, c) in recovery_codes_str.chars().enumerate() {
                if i > 0 && i % 11 == 0 {
                    recovery_codes.push(recovery_code.clone());
                    recovery_code = c.to_string();
                } else if i == recovery_codes_str.len() - 1 {
                    recovery_code.push(c);
                    recovery_codes.push(recovery_code.clone());
                } else {
                    recovery_code.push(c);
                }
            }

            println!("recovery codes:");

            for code in recovery_codes {
                println!("- {code}");
            }
        }

        // commands for interacting with `ordinaryd` (Ordinary Server)
        Commands::Login {
            domain,
            account,
            password,
            mfa_code,
        } => {
            let client = AdminClient::new(
                domain,
                account,
                admin_domain,
                cli.danger_accept_invalid_certs,
            );

            client.login(password, mfa_code).await?;
        }
        Commands::Logout { domain: _ } => {
            println!("todo: logout");
        }
        Commands::ResetPassword {
            domain,
            account,
            old_password,
            new_password,
            mfa_code,
        } => {
            let client = AdminClient::new(
                domain,
                account,
                admin_domain,
                cli.danger_accept_invalid_certs,
            );

            client
                .reset_password(old_password, mfa_code, new_password)
                .await?;
        }

        Commands::InviteAdminAccount {
            domain,
            account,
            invite_domain,
            invite_account,
            permissions,
        } => {
            let client = AdminClient::new(
                domain,
                account,
                admin_domain,
                cli.danger_accept_invalid_certs,
            );

            let permissions = permissions
                .iter()
                .map(Permission::as_u8)
                .collect::<Vec<u8>>();

            let invite_token = client
                .invite_admin_account(invite_domain, invite_account, permissions)
                .await?;

            println!("invite token: {invite_token}");
        }

        Commands::Deploy {
            domain,
            account,
            app,
            clean_port_to_stdio,
        } => {
            let client = AdminClient::new(
                domain,
                account,
                admin_domain,
                cli.danger_accept_invalid_certs,
            );

            let port = client.deploy(app).await?;

            if clean_port_to_stdio == &true {
                print!("{port}");
            } else {
                tracing::info!("running on port: {port}");
            }
        }
        Commands::Patch {
            domain,
            account,
            app,
            clean_port_to_stdio,
        } => {
            let client = AdminClient::new(
                domain,
                account,
                admin_domain,
                cli.danger_accept_invalid_certs,
            );

            let port = client.patch(app).await?;

            if clean_port_to_stdio == &true {
                print!("{port}");
            } else {
                tracing::info!("running on port: {port}");
            }
        }
        Commands::Migrate {
            domain: _,
            account: _,
            app: _,
        } => {
            println!("todo: migrate");
        }
        Commands::Bridge {
            domain: _,
            account: _,
            app,
            remote,
        } => {
            tracing::info!("bridge {app} to {remote}");
        }
        Commands::Kill {
            domain,
            account,
            app,
        } => {
            let client = AdminClient::new(
                domain,
                account,
                admin_domain,
                cli.danger_accept_invalid_certs,
            );

            client.kill(app).await?;
        }
        Commands::Erase {
            domain: _,
            account: _,
            app,
        } => {
            tracing::info!("erase {app}");
        }
        Commands::Install {
            domain,
            account,
            app,
            action,
        } => {
            let client = AdminClient::new(
                domain,
                account,
                admin_domain,
                cli.danger_accept_invalid_certs,
            );

            client.install(app, action).await?;
        }
        Commands::InstallAll {
            domain,
            account,
            app,
        } => {
            let client = AdminClient::new(
                domain,
                account,
                admin_domain,
                cli.danger_accept_invalid_certs,
            );

            client.install_all(app).await?;
        }
        Commands::Upload {
            domain,
            account,
            app,
            template,
        } => {
            let client = AdminClient::new(
                domain,
                account,
                admin_domain,
                cli.danger_accept_invalid_certs,
            );

            client.upload(app, template).await?;
        }
        Commands::UploadAll {
            domain,
            account,
            app,
        } => {
            let client = AdminClient::new(
                domain,
                account,
                admin_domain,
                cli.danger_accept_invalid_certs,
            );

            client.upload_all(app).await?;
        }
        Commands::Write {
            domain,
            account,
            app,
            asset,
        } => {
            let client = AdminClient::new(
                domain,
                account,
                admin_domain,
                cli.danger_accept_invalid_certs,
            );

            client.write(app, asset).await?;
        }
        Commands::WriteAll {
            domain,
            account,
            app,
        } => {
            let client = AdminClient::new(
                domain,
                account,
                admin_domain,
                cli.danger_accept_invalid_certs,
            );

            client.write_all(app).await?;
        }
        Commands::Update {
            domain,
            account,
            app,
        } => {
            let client = AdminClient::new(
                domain,
                account,
                admin_domain,
                cli.danger_accept_invalid_certs,
            );

            client.update(app).await?;
        }
        Commands::ListAppAccounts {
            domain,
            account,
            app,
            clean_json_to_stdio,
        } => {
            let client = AdminClient::new(
                domain,
                account,
                admin_domain,
                cli.danger_accept_invalid_certs,
            );

            let json = client.app_accounts_list(app).await?;

            if clean_json_to_stdio == &true {
                print!("{json}");
            } else {
                let accounts: Vec<serde_json::Value> = serde_json::from_str(&json)?;

                for account in accounts {
                    tracing::info!(%account);
                }
            }
        }
        Commands::ListItems {
            domain,
            account,
            app,
            model,
            clean_json_to_stdio,
        } => {
            let client = AdminClient::new(
                domain,
                account,
                admin_domain,
                cli.danger_accept_invalid_certs,
            );

            let json = client.items_list(app, *model).await?;

            if clean_json_to_stdio == &true {
                print!("{json}");
            } else {
                let items: Vec<serde_json::Value> = serde_json::from_str(&json)?;

                for item in items {
                    tracing::info!(%item);
                }
            }
        }
        Commands::AppLogs {
            domain,
            account,
            app,
            query,
            format,
            limit,
            clean_json_to_stdio,
        } => {
            let client = AdminClient::new(
                domain,
                account,
                admin_domain,
                cli.danger_accept_invalid_certs,
            );

            let json = client.app_logs(app, query, format, limit).await?;

            if clean_json_to_stdio == &true {
                print!("{json}");
            } else if matches!(format.as_str(), "all" | "top") {
                let mut lines: Vec<serde_json::Value> = serde_json::from_str(&json)?;

                lines.sort_by(|a, b| {
                    let a = a
                        .get("timestamp")
                        .expect("no timestamp")
                        .as_str()
                        .expect("timestamp not string");
                    let b = b
                        .get("timestamp")
                        .expect("no timestamp")
                        .as_str()
                        .expect("timestamp not string");

                    b.cmp(a)
                });

                for line in lines {
                    tracing::info!(%line);
                }
            } else {
                tracing::info!(count = %json);
            }
        }
        Commands::ListAdminAccounts { .. } => {
            println!("todo: list admin accounts");
        }
        Commands::DeleteAdminAccount { .. } => {
            println!("todo: delete admin account");
        }
        Commands::AdminLogs { .. } => {
            println!("todo: admin logs");
        }
        Commands::RootListApps { domain: _ } => {
            println!("todo: root list apps");
        }
        Commands::RootLogs { .. } => {
            println!("todo: root logs");
        }
    }

    Ok(())
}
