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

use crate::{Permission, add_http};
use anyhow::bail;
use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD as b64};
use clap::Subcommand;
use clap_complete::{ArgValueCompleter, CompletionCandidate};
use ordinary_api::client::{AccountMeta, OrdinaryApiClient};
use qrcodegen::{QrCode, QrCodeEcc};
use std::env::home_dir;
use time::UtcDateTime;
use time::format_description::BorrowedFormatItem;
use time::macros::format_description;

pub static GMT_FORMAT: &[BorrowedFormatItem<'static>] = format_description!(
    "[weekday repr:short], [day] [month repr:short] [year] [hour]:[minute]:[second] GMT"
);

/// account management subcommands
#[derive(Subcommand, Debug)]
pub enum Accounts {
    /// register a new account
    Register {
        /// url where `orindaryd` is running
        host: String,
        /// name of your account with the host
        account: String,

        /// password for your new account
        #[arg(long)]
        password: Option<String>,

        /// base64 encoded invite token
        #[arg(long)]
        invite: Option<String>,
    },
    /// log in to an existing account
    Login {
        /// url where `orindaryd` is running
        host: String,
        /// name of your account with the host
        account: String,

        /// password for your existing account
        #[arg(long)]
        password: Option<String>,

        /// 6 digit TOTP MFA code
        #[arg(long)]
        mfa: Option<String>,
    },
    /// log out of a logged in account
    Logout,
    Access {
        #[command(subcommand)]
        access: Access,
    },
    /// password management
    Password {
        #[command(subcommand)]
        password: Password,
    },
    /// MFA management
    Mfa {
        #[command(subcommand)]
        mfa: Mfa,
    },
    /// recovery code management
    RecoveryCodes {
        #[command(subcommand)]
        recovery_codes: RecoveryCodes,
    },
    /// delete account
    Delete {
        /// url where `orindaryd` is running
        host: String,
        /// name of your account with the host
        account: String,

        /// password for your existing account
        #[arg(long)]
        password: Option<String>,

        /// 6 digit TOTP MFA code
        #[arg(long)]
        mfa: Option<String>,
    },
    /// invite another user to a project
    Invite {
        /// name of the account to be invited & registered
        account: String,
        /// domain for the invited application
        domain: String,
        /// which permissions to include in their set
        permissions: Vec<Permission>,
    },
    /// display info for current account
    ///
    /// format: <host> <account> <project> <permissions> <session exp>
    Current,
    /// list all logged in accounts
    List,
    /// switch to a different logged in account
    Switch {
        /// API domain where `orindaryd` is running
        #[arg(add = ArgValueCompleter::new(list_domains))]
        domain: String,
        /// name of the account you'd like to switch to
        account: String,
    },
}

/// access management subcommands
#[derive(Subcommand, Debug)]
pub enum Access {
    /// get access
    Get {
        /// how long the client signature is valid for
        #[arg(short, long)]
        min: Option<u16>,
    },
}

/// password management subcommands
#[derive(Subcommand, Debug)]
pub enum Password {
    /// reset your password
    Reset {
        /// existing password
        #[arg(long)]
        password: Option<String>,

        /// new password to be set
        #[arg(long)]
        new_password: Option<String>,

        /// 6 digit TOTP MFA code
        #[arg(long)]
        mfa: Option<String>,
    },
    /// recover your password
    Forgot {
        /// new password to be set
        #[arg(long)]
        new_password: Option<String>,

        /// 11 character recovery code
        #[arg(long)]
        recovery_code: Option<String>,
    },
}

/// MFA management subcommands
#[derive(Subcommand, Debug)]
pub enum Mfa {
    /// reset MFA TOTP secret
    Reset {
        /// account password
        #[arg(long)]
        password: Option<String>,

        /// 6 digit TOTP MFA code
        #[arg(long)]
        mfa: Option<String>,
    },
    /// recovery MFA TOTP secret
    Lost {
        /// account password
        #[arg(long)]
        password: Option<String>,

        /// 11 character recovery code
        #[arg(long)]
        recovery_code: Option<String>,
    },
}

/// recovery code management subcommands
#[derive(Subcommand, Debug)]
pub enum RecoveryCodes {
    /// reset recovery codes
    Reset {
        /// account password
        #[arg(long)]
        password: Option<String>,

        /// 6 digit TOTP MFA code
        #[arg(long)]
        mfa: Option<String>,
    },
}

fn switch_account(host: &str, domain: &str, account: &str) -> anyhow::Result<()> {
    tracing::debug!("switching accounts");

    let cli_path = home_dir()
        .expect("failed to get home dir")
        .join(".ordinary")
        .join("cli");

    if !cli_path.exists() {
        fs_err::create_dir_all(&cli_path)?;
    }

    let host = if let Some(stripped) = host.strip_prefix("https://") {
        stripped
    } else if let Some(stripped) = host.strip_prefix("http://") {
        stripped
    } else {
        host
    };

    let domain = if let Some(stripped) = domain.strip_prefix("https://") {
        stripped
    } else if let Some(stripped) = domain.strip_prefix("http://") {
        stripped
    } else {
        domain
    };

    let account_path = cli_path.join("account");
    fs_err::write(account_path, format!("{account}@{domain}@{host}"))?;

    Ok(())
}

pub fn get_current_account(insecure: bool) -> anyhow::Result<AccountMeta> {
    tracing::debug!("getting current account");

    let account_path = home_dir()
        .expect("failed to get home dir")
        .join(".ordinary")
        .join("cli")
        .join("account");

    if !account_path.exists() {
        bail!("not logged in")
    }

    let pair = fs_err::read_to_string(account_path)?;

    let split_pair = pair.split('@').collect::<Vec<&str>>();
    let mut account_meta =
        OrdinaryApiClient::get_account(split_pair[2], split_pair[1], split_pair[0])?;

    account_meta.host = add_http(&account_meta.host, insecure);
    Ok(account_meta)
}

impl Accounts {
    #[allow(clippy::too_many_lines)]
    pub async fn handle(
        &self,
        api_domain: Option<&str>,
        accept_invalid_certs: bool,
        insecure: bool,
    ) -> anyhow::Result<()> {
        match self {
            Self::Register {
                host,
                account,
                password,
                invite,
            } => {
                let client = OrdinaryApiClient::new(
                    host,
                    account,
                    api_domain,
                    accept_invalid_certs,
                    crate::USER_AGENT,
                    false,
                )?;

                // todo: pull from elsewhere if None
                let password = password.to_owned().unwrap_or_default();
                let invite = invite.to_owned().unwrap_or_default();

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

                Self::print_mfa(&totp.get_url())?;
                Self::print_recovery_codes(&recovery_codes_str);

                println!("\nlog in: `ordinary accounts login --help`");
            }
            Self::Login {
                host,
                account,
                password,
                mfa,
            } => {
                let client = OrdinaryApiClient::new(
                    host,
                    account,
                    api_domain,
                    accept_invalid_certs,
                    crate::USER_AGENT,
                    false,
                )?;

                // todo: pull from elsewhere if None
                let password = password.to_owned().unwrap_or_default();
                let mfa = mfa.to_owned().unwrap_or_default();

                client.login(&password, &mfa).await?;
                switch_account(host, api_domain.unwrap_or(host), account)?;
            }
            Self::Logout => {
                println!("todo: logout");
            }
            Self::Access { access } => match access {
                Access::Get { min } => {
                    let AccountMeta {
                        host,
                        name: account,
                        ..
                    } = get_current_account(insecure)?;
                    let client = OrdinaryApiClient::new(
                        &host,
                        &account,
                        api_domain,
                        accept_invalid_certs,
                        crate::USER_AGENT,
                        false,
                    )?;
                    let access_token = client
                        .get_access(min.map(|m| u32::from(m) * 60), None)
                        .await?;
                    let b64_token = b64.encode(access_token);

                    println!("{b64_token}");
                }
            },
            Self::Password { password } => {
                let AccountMeta {
                    host,
                    name: account,
                    ..
                } = get_current_account(insecure)?;
                let client = OrdinaryApiClient::new(
                    &host,
                    &account,
                    api_domain,
                    accept_invalid_certs,
                    crate::USER_AGENT,
                    false,
                )?;

                match password {
                    Password::Reset {
                        password,
                        new_password,
                        mfa,
                    } => {
                        // todo: pull from elsewhere if None
                        let password = password.to_owned().unwrap_or_default();
                        let new_password = new_password.to_owned().unwrap_or_default();
                        let mfa = mfa.to_owned().unwrap_or_default();

                        client
                            .reset_password(&password, &mfa, &new_password)
                            .await?;
                    }
                    Password::Forgot {
                        new_password,
                        recovery_code,
                    } => {
                        let AccountMeta {
                            host,
                            name: account,
                            ..
                        } = get_current_account(insecure)?;
                        let client = OrdinaryApiClient::new(
                            &host,
                            &account,
                            api_domain,
                            accept_invalid_certs,
                            crate::USER_AGENT,
                            false,
                        )?;

                        // todo: pull from elsewhere if None
                        let new_password = new_password.to_owned().unwrap_or_default();
                        let recovery_code = recovery_code.to_owned().unwrap_or_default();

                        client
                            .forgot_password(&new_password, &recovery_code)
                            .await?;
                    }
                }
            }
            Self::Mfa { mfa } => {
                let AccountMeta {
                    host,
                    name: account,
                    ..
                } = get_current_account(insecure)?;
                let client = OrdinaryApiClient::new(
                    &host,
                    &account,
                    api_domain,
                    accept_invalid_certs,
                    crate::USER_AGENT,
                    false,
                )?;

                match mfa {
                    Mfa::Reset { password, mfa } => {
                        let password = password.to_owned().unwrap_or_default();
                        let mfa = mfa.to_owned().unwrap_or_default();

                        let totp = client.mfa_totp_reset(&password, &mfa).await?;

                        Self::print_mfa(&totp.get_url())?;
                    }
                    Mfa::Lost {
                        password,
                        recovery_code,
                    } => {
                        let password = password.to_owned().unwrap_or_default();
                        let recovery_code = recovery_code.to_owned().unwrap_or_default();

                        let totp = client.mfa_totp_reset(&password, &recovery_code).await?;

                        Self::print_mfa(&totp.get_url())?;
                    }
                }
            }
            Self::RecoveryCodes { recovery_codes } => {
                let AccountMeta {
                    host,
                    name: account,
                    ..
                } = get_current_account(insecure)?;
                let client = OrdinaryApiClient::new(
                    &host,
                    &account,
                    api_domain,
                    accept_invalid_certs,
                    crate::USER_AGENT,
                    false,
                )?;

                match recovery_codes {
                    RecoveryCodes::Reset { password, mfa } => {
                        let password = password.to_owned().unwrap_or_default();
                        let mfa = mfa.to_owned().unwrap_or_default();

                        let recovery_codes_str =
                            client.recovery_codes_reset(&password, &mfa).await?;

                        Self::print_recovery_codes(&recovery_codes_str);
                    }
                }
            }
            Self::Delete {
                host,
                account,
                password,
                mfa,
            } => {
                let client = OrdinaryApiClient::new(
                    host,
                    account,
                    api_domain,
                    accept_invalid_certs,
                    crate::USER_AGENT,
                    false,
                )?;

                // todo: pull from elsewhere if None
                let password = password.to_owned().unwrap_or_default();
                let mfa = mfa.to_owned().unwrap_or_default();

                client.delete_account(&password, &mfa).await?;
            }
            Self::Invite {
                account: their_account,
                domain,
                permissions,
            } => {
                let AccountMeta {
                    host,
                    name: account,
                    project,
                    ..
                } = get_current_account(insecure)?;
                let client = OrdinaryApiClient::new(
                    &host,
                    &account,
                    api_domain,
                    accept_invalid_certs,
                    crate::USER_AGENT,
                    false,
                )?;

                if !project.is_empty() && &project != domain {
                    bail!("projects do not match");
                }

                let invite_token = client
                    .invite_api_account(
                        domain,
                        their_account,
                        permissions
                            .iter()
                            .map(Permission::as_u8)
                            .collect::<Vec<u8>>(),
                    )
                    .await?;

                println!("INVITE TOKEN: {invite_token}");
            }
            Self::Current => {
                let curr = get_current_account(insecure)?;
                let accounts = OrdinaryApiClient::list_accounts()?
                    .iter()
                    .filter(|&account| account.name == curr.name && account.domain == curr.domain)
                    .cloned()
                    .collect::<Vec<AccountMeta>>();

                Self::account_meta_to_readable(accounts)?;
            }
            Self::List => {
                Self::account_meta_to_readable(OrdinaryApiClient::list_accounts()?)?;
            }
            Self::Switch { domain, account } => {
                let host = OrdinaryApiClient::get_host(domain, account)?;
                switch_account(&host, domain, account)?;
            }
        }

        Ok(())
    }

    fn print_recovery_codes(recovery_codes_str: &str) {
        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}");
        }
    }

    fn print_mfa(mfa_url: &str) -> anyhow::Result<()> {
        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}");
        Ok(())
    }

    fn account_meta_to_readable(accounts: Vec<AccountMeta>) -> anyhow::Result<()> {
        use comfy_table::Table;

        let mut table = Table::new();

        table.set_header(vec![
            "API account",
            "API domain",
            "Host",
            "App domain",
            "App permissions",
            "Token expiration",
            "Token ID",
        ]);

        for account in accounts {
            let mut perms_str = String::new();

            for permission in account.permissions {
                perms_str.push_str(Permission::from_u8(permission).as_str());
                perms_str.push(' ');
            }

            perms_str.pop();

            let exp = UtcDateTime::from_unix_timestamp(account.refresh_exp.cast_signed())?;
            let exp_fmt = exp.format(&GMT_FORMAT)?;

            table.add_row(vec![
                account.name,
                account.domain,
                format!("http(s)://{}", account.host),
                account.project,
                perms_str,
                exp_fmt,
                account.id,
            ]);
        }

        println!("{table}");

        Ok(())
    }
}
fn list_domains(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
    let Some(current) = current.to_str() else {
        return vec![];
    };

    let accounts = OrdinaryApiClient::list_accounts();
    match accounts {
        Ok(accounts) => accounts
            .iter()
            .filter(|&a| a.domain.starts_with(current))
            .map(|a| CompletionCandidate::new(a.domain.clone()))
            .collect::<Vec<CompletionCandidate>>(),
        Err(_) => {
            vec![]
        }
    }
}
