use clap::{Parser, Subcommand};
use indieweb_cli_common::{Config, ConfigArgs, OutputFormat, print_json, TokenStore};
use indieweb::http::reqwest::Client as HttpClient;
use indieweb::standards::indieauth::AccessToken;
use miette::IntoDiagnostic;
use secrecy::{ExposeSecret, SecretString};
use serde::Serialize;
use url::Url;

mod commands;

use commands::{config as config_cmd, list, create, read, update, delete};

#[derive(Parser)]
#[command(name = "micropub")]
#[command(about = "Micropub 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,

    #[arg(long, short, global = true)]
    endpoint: Option<Url>,

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

#[derive(Subcommand)]
enum Commands {
    Config,
    List {
        #[arg(long)]
        post_type: Option<Vec<String>>,
        #[arg(long)]
        limit: Option<usize>,
        #[arg(long)]
        offset: Option<usize>,
        #[arg(long)]
        url: Option<Url>,
    },
    Create {
        #[arg(long)]
        content: String,
        #[arg(long, default_value = "entry")]
        post_type: String,
        #[arg(long)]
        category: Option<Vec<String>>,
        #[arg(long)]
        name: Option<String>,
    },
    Read {
        url: Url,
    },
    Update {
        url: Url,
        #[arg(long)]
        replace: Option<Vec<String>>,
        #[arg(long)]
        add: Option<Vec<String>>,
        #[arg(long)]
        delete_prop: Option<Vec<String>>,
    },
    Delete {
        url: Url,
    },
    Logout,
}

#[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 }
    }
}

fn get_token(cli_token: Option<&String>, config_args: &ConfigArgs) -> miette::Result<Option<SecretString>> {
    let store = TokenStore::micropub();
    
    if let Some(token) = cli_token {
        return Ok(Some(SecretString::new(token.clone().into_boxed_str())));
    }
    
    if let Some(ref token) = config_args.token {
        return Ok(Some(SecretString::new(token.clone().into_boxed_str())));
    }
    
    store.load().into_diagnostic()
}

fn require_token(cli_token: Option<&String>, config_args: &ConfigArgs) -> miette::Result<SecretString> {
    get_token(cli_token, config_args)?
        .ok_or_else(|| miette::miette!("No token found. Run 'indieauth auth' first or provide --token"))
}

#[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()?;
    let http_client = HttpClient::default();

    let endpoint = cli.endpoint.clone()
        .or(config.micropub.endpoint.clone())
        .ok_or_else(|| miette::miette!("No Micropub endpoint specified. Use --endpoint or configure in config.toml"))?;

    match cli.command {
        Commands::Config => {
            let token = get_token(None, &cli.config_args)?;
            let access_token = token.map(|t| AccessToken::new(t.expose_secret()));
            let result = config_cmd::run(&http_client, &endpoint, access_token.as_ref()).await?;
            print_json(&CommandResult::success(result)).into_diagnostic()?;
        }
        Commands::List { post_type, limit, offset, url } => {
            let token = require_token(None, &cli.config_args)?;
            let access_token = AccessToken::new(token.expose_secret());
            let result = list::run(&http_client, &endpoint, &access_token, post_type, limit, offset, url).await?;
            print_json(&CommandResult::success(result)).into_diagnostic()?;
        }
        Commands::Create { content, post_type, category, name } => {
            let token = require_token(None, &cli.config_args)?;
            let access_token = AccessToken::new(token.expose_secret());
            let result = create::run(&http_client, &endpoint, &access_token, &content, &post_type, category, name).await?;
            print_json(&CommandResult::success(result)).into_diagnostic()?;
        }
        Commands::Read { url } => {
            let token = require_token(None, &cli.config_args)?;
            let access_token = AccessToken::new(token.expose_secret());
            let result = read::run(&http_client, &endpoint, &access_token, &url).await?;
            print_json(&CommandResult::success(result)).into_diagnostic()?;
        }
        Commands::Update { url, replace, add, delete_prop } => {
            let token = require_token(None, &cli.config_args)?;
            let access_token = AccessToken::new(token.expose_secret());
            let result = update::run(&http_client, &endpoint, &access_token, &url, replace, add, delete_prop).await?;
            print_json(&CommandResult::success(result)).into_diagnostic()?;
        }
        Commands::Delete { url } => {
            let token = require_token(None, &cli.config_args)?;
            let access_token = AccessToken::new(token.expose_secret());
            let result = delete::run(&http_client, &endpoint, &access_token, &url).await?;
            print_json(&CommandResult::success(result)).into_diagnostic()?;
        }
        Commands::Logout => {
            let store = TokenStore::micropub();
            store.delete().into_diagnostic()?;
            println!("{}", serde_json::to_string(&serde_json::json!({"success": true, "message": "Token removed"})).into_diagnostic()?);
        }
    }

    Ok(())
}
