use clap::{Parser, Subcommand};
use indieweb_cli_common::{Config, ConfigArgs, OutputFormat, print_json, TokenStore};
use indieweb_cli_common::auth;
use miette::IntoDiagnostic;
use secrecy::SecretString;
use serde::Serialize;
use url::Url;

#[derive(Parser)]
#[command(name = "indieauth")]
#[command(about = "IndieAuth CLI client for the IndieWeb", long_about = None)]
#[command(version)]
struct Cli {
    #[command(flatten)]
    config_args: ConfigArgs,

    #[arg(long, short, global = true, default_value = "json")]
    output: OutputFormat,

    #[command(subcommand)]
    command: Commands,
}

#[derive(Subcommand)]
enum Commands {
    Auth {
        me: Url,
        #[arg(long)]
        client_id: Option<Url>,
        #[arg(long, default_value = "create update delete")]
        scope: String,
    },
    Revoke {
        me: Url,
    },
    Info {
        me: Url,
    },
}

#[derive(Serialize)]
struct CommandResult<T: Serialize> {
    success: bool,
    #[serde(flatten)]
    data: T,
}

impl<T: Serialize> CommandResult<T> {
    fn success(data: T) -> Self {
        Self { success: true, data }
    }
}

#[derive(Serialize)]
struct AuthOutput {
    me: String,
    scope: Option<String>,
    token_stored: bool,
}

#[derive(Serialize)]
struct RevokeOutput {
    revoked: bool,
}

fn get_token() -> miette::Result<Option<SecretString>> {
    let store = TokenStore::indieauth();
    store.load().into_diagnostic()
}

fn require_token() -> miette::Result<SecretString> {
    get_token()?
        .ok_or_else(|| miette::miette!("No token found. Run 'indieauth auth <me>' first"))
}

#[tokio::main]
async fn main() -> miette::Result<()> {
    let cli = Cli::parse();

    if cli.config_args.verbose {
        tracing_forest::init();
    }

    let config = Config::load(&cli.config_args).into_diagnostic()?;

    match cli.command {
        Commands::Auth { me, client_id, scope } => {
            let mut auth_config = config.indieauth.clone();
            if let Some(cid) = client_id {
                auth_config.client_id = Some(cid);
            }

            let result = auth::authenticate(&me, &auth_config, &scope).await
                .into_diagnostic()?;

            let store = TokenStore::indieauth();
            let token = SecretString::new(result.access_token.clone().into_boxed_str());
            store.save(&token).into_diagnostic()?;

            let micropub_store = TokenStore::micropub();
            micropub_store.save(&token).into_diagnostic()?;

            print_json(&CommandResult::success(AuthOutput {
                me: result.me,
                scope: result.scope,
                token_stored: true,
            })).into_diagnostic()?;
        }
        Commands::Revoke { me } => {
            let token = require_token()?;

            match auth::revoke_token(&token, &me).await {
                Ok(()) => {
                    let store = TokenStore::indieauth();
                    store.delete().into_diagnostic()?;
                    
                    let micropub_store = TokenStore::micropub();
                    micropub_store.delete().into_diagnostic()?;

                    print_json(&CommandResult::success(RevokeOutput { revoked: true })).into_diagnostic()?;
                }
                Err(_) => {
                    let store = TokenStore::indieauth();
                    store.delete().into_diagnostic()?;
                    
                    let micropub_store = TokenStore::micropub();
                    micropub_store.delete().into_diagnostic()?;

                    print_json(&CommandResult::success(RevokeOutput { revoked: true })).into_diagnostic()?;
                }
            }
        }
        Commands::Info { me } => {
            let token = require_token()?;

            let result = auth::introspect_token(&token, &me).await.into_diagnostic()?;

            print_json(&CommandResult::success(result)).into_diagnostic()?;
        }
    }

    Ok(())
}
