use anyhow::anyhow;
use clap::{Parser, Subcommand, ValueEnum};
use filebuffer::FileBuffer;
use log::{debug, error, trace};
use reqwest::Url;
use serde_json::json;
use std::path::PathBuf;

use crate::common::EntryValue;

mod common;

/// Command-line client for UpEnd
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None, arg_required_else_help=true)]
struct Args {
    #[command(subcommand)]
    command: Option<Commands>,

    /// Output format
    #[arg(short, long)]
    format: Option<OutputFormat>,
}

#[derive(Subcommand, Debug)]
enum Commands {
    Query {
        /// URL of the UpEnd instance to query.
        #[arg(short, long)]
        url: Option<Url>,
        /// The query itself, in L-expression format.
        query: String,
    },
    /// Insert an entry.
    Insert {
        /// URL of the UpEnd instance to query.
        #[arg(short, long)]
        url: Option<Url>,
        /// The address of the entity.
        entity: String,
        // The attribute.
        attribute: String,
        // The value.
        value: common::EntryValue,
    },
    /// Get the address of a file, attribute or URL.
    Address {
        /// Type of input to be addressed
        _type: AddressType,
        /// Path to a file, hash...
        input: String,
    },
}

#[derive(Copy, Clone, PartialEq, Eq, Debug, ValueEnum)]
enum OutputFormat {
    Json,
    Tsv,
    Raw,
}

#[derive(Copy, Clone, PartialEq, Eq, Debug, ValueEnum)]
enum AddressType {
    /// Hash a file and output its address.
    File,
    /// Compute an address from the output of `sha256sum`
    Sha256sum,
}

fn main() -> anyhow::Result<()> {
    env_logger::init();
    let args = Args::parse();

    let format = args.format.unwrap_or(OutputFormat::Tsv);

    match args.command {
        Some(Commands::Query { url, query }) => {
            let url = url.unwrap_or("http://localhost:8093".parse().unwrap());
            let api_url = url.join("/api/query")?;

            debug!("Querying \"{}\"", api_url);
            let client = reqwest::blocking::Client::new();
            let response = client.post(api_url).body(query).send()?;

            match response.error_for_status_ref() {
                Ok(_) => match format {
                    OutputFormat::Json | OutputFormat::Raw => println!("{}", response.text()?),
                    OutputFormat::Tsv => todo!(),
                },
                Err(err) => {
                    error!("{}", response.text()?);
                    return Err(err.into());
                }
            }
        }
        Some(Commands::Insert {
            url,
            entity,
            attribute,
            value,
        }) => {
            let url = url.unwrap_or("http://localhost:8093".parse().unwrap());
            let api_url = url.join("/api/obj")?;

            if let EntryValue::Invalid = value {
                return Err(anyhow!("Invalid entry value."));
            }

            let body = json!({
               "entity": entity,
               "attribute": attribute,
               "value": value
            });

            debug!("Inserting {:?} at \"{}\"", body, api_url);
            let client = reqwest::blocking::Client::new();
            let response = client.put(api_url).json(&body).send()?;

            match response.error_for_status_ref() {
                Ok(_) => {
                    let data: Vec<String> = response.json()?;
                    println!("{}", data[0]);
                }
                Err(err) => {
                    error!("{}", response.text()?);
                    return Err(err.into());
                }
            }
        }
        Some(Commands::Address { _type, input }) => {
            let address = match _type {
                AddressType::File => {
                    let filepath = PathBuf::from(input);
                    debug!("Hashing {:?}...", filepath);
                    let fbuffer = FileBuffer::open(&filepath)?;
                    let digest = common::hash(&fbuffer);
                    trace!("Finished hashing {:?}...", &filepath);
                    common::Address::Hash(digest)
                }
                AddressType::Sha256sum => {
                    let digest = multibase::Base::Base16Lower.decode(input)?;
                    common::Address::Hash(common::Hash(digest))
                }
            };

            match format {
                OutputFormat::Json => println!("\"{}\"", address),
                OutputFormat::Tsv | OutputFormat::Raw => println!("{}", address),
            }
        }
        None => {}
    }

    Ok(())
}
