use crate::{cache_storage::CacheStorage, ESPLORA_API_URL};
use anyhow::{anyhow, bail, Context, Result};
use elements::{
    encode::{deserialize, serialize_hex},
    Address, BlockHash, Transaction, Txid,
};
use reqwest::StatusCode;
use wasm_bindgen::UnwrapThrowExt;

/// Fetch the UTXOs of an address.
///
/// UTXOs change over time and as such, this function never uses a cache.
pub async fn fetch_utxos(address: &Address) -> Result<Vec<Utxo>> {
    let esplora_url = {
        let guard = ESPLORA_API_URL.lock().expect_throw("can get lock");
        guard.clone()
    };

    let path = format!("address/{}/utxo", address);
    let esplora_url = esplora_url.join(path.as_str())?;
    let response = reqwest::get(esplora_url.clone())
        .await
        .context("failed to fetch UTXOs")?;

    if response.status() == StatusCode::NOT_FOUND {
        log::debug!(
            "GET {} returned 404, defaulting to empty UTXO set",
            esplora_url
        );

        return Ok(Vec::new());
    }

    if !response.status().is_success() {
        let error_body = response.text().await?;
        return Err(anyhow!(
            "failed to fetch utxos, esplora returned '{}'",
            error_body
        ));
    }

    response
        .json::<Vec<Utxo>>()
        .await
        .context("failed to deserialize response")
}

/// Fetch transaction history for the specified address.
///
/// Returns up to 50 mempool transactions plus the first 25 confirmed
/// transactions. See
/// https://github.com/blockstream/esplora/blob/master/API.md#get-addressaddresstxs
/// for more information.
pub async fn fetch_transaction_history(address: &Address) -> Result<Vec<Txid>> {
    let esplora_url = {
        let guard = ESPLORA_API_URL.lock().expect_throw("can get lock");
        guard.clone()
    };
    let path = format!("address/{}/txs", address);
    let url = esplora_url.join(path.as_str())?;
    let response = reqwest::get(url.clone())
        .await
        .context("failed to fetch transaction history")?;

    if !response.status().is_success() {
        let error_body = response.text().await?;
        return Err(anyhow!(
            "failed to fetch transaction history, esplora returned '{}' from '{}'",
            error_body,
            url
        ));
    }

    #[derive(serde::Deserialize)]
    struct HistoryElement {
        txid: Txid,
    }

    let response = response
        .json::<Vec<HistoryElement>>()
        .await
        .context("failed to deserialize response")?;

    Ok(response.iter().map(|elem| elem.txid).collect())
}

/// Fetches a transaction.
///
/// This function makes use of the browsers local storage to avoid spamming the underlying source.
/// Transaction never change after they've been mined, hence we can cache those indefinitely.
pub async fn fetch_transaction(txid: Txid) -> Result<Transaction> {
    let esplora_url = {
        let guard = ESPLORA_API_URL.lock().expect_throw("can get lock");
        guard.clone()
    };
    let cache = CacheStorage::new()?;
    let body = cache
        .match_or_add(&format!("{}tx/{}/hex", esplora_url, txid))
        .await?
        .text()
        .await?;

    Ok(deserialize(&hex::decode(body.as_bytes())?)?)
}

pub async fn broadcast(tx: Transaction) -> Result<Txid> {
    let esplora_url = {
        let guard = ESPLORA_API_URL.lock().expect_throw("can get lock");
        guard.clone()
    };
    let esplora_url = esplora_url.join("tx")?;
    let client = reqwest::Client::new();

    let response = client
        .post(esplora_url.clone())
        .body(serialize_hex(&tx))
        .send()
        .await?;

    let code = response.status();

    if !code.is_success() {
        bail!("failed to successfully publish transaction");
    }

    let txid = response
        .text()
        .await?
        .parse()
        .context("failed to parse response body as txid")?;

    Ok(txid)
}

pub async fn get_fee_estimates() -> Result<FeeEstimatesResponse> {
    let esplora_url = {
        let guard = ESPLORA_API_URL.lock().expect_throw("can get lock");
        guard.clone()
    };
    let esplora_url = esplora_url.join("fee-estimates")?;

    let fee_estimates = reqwest::get(esplora_url.clone())
        .await
        .with_context(|| format!("failed to GET {}", esplora_url))?
        .json()
        .await
        .context("failed to deserialize fee estimates")?;

    Ok(fee_estimates)
}

/// The response object for the `/fee-estimates` endpoint.
///
/// The key is the confirmation target (in number of blocks) and the value is the estimated feerate (in sat/vB).
/// The available confirmation targets are 1-25, 144, 504 and 1008 blocks.
#[derive(serde::Deserialize, Debug)]
pub struct FeeEstimatesResponse {
    #[serde(rename = "1")]
    pub b_1: Option<f32>,
    #[serde(rename = "2")]
    pub b_2: Option<f32>,
    #[serde(rename = "3")]
    pub b_3: Option<f32>,
    #[serde(rename = "4")]
    pub b_4: Option<f32>,
    #[serde(rename = "5")]
    pub b_5: Option<f32>,
    #[serde(rename = "6")]
    pub b_6: Option<f32>,
    #[serde(rename = "7")]
    pub b_7: Option<f32>,
    #[serde(rename = "8")]
    pub b_8: Option<f32>,
    #[serde(rename = "9")]
    pub b_9: Option<f32>,
    #[serde(rename = "10")]
    pub b_10: Option<f32>,
    #[serde(rename = "11")]
    pub b_11: Option<f32>,
    #[serde(rename = "12")]
    pub b_12: Option<f32>,
    #[serde(rename = "13")]
    pub b_13: Option<f32>,
    #[serde(rename = "14")]
    pub b_14: Option<f32>,
    #[serde(rename = "15")]
    pub b_15: Option<f32>,
    #[serde(rename = "16")]
    pub b_16: Option<f32>,
    #[serde(rename = "17")]
    pub b_17: Option<f32>,
    #[serde(rename = "18")]
    pub b_18: Option<f32>,
    #[serde(rename = "19")]
    pub b_19: Option<f32>,
    #[serde(rename = "20")]
    pub b_20: Option<f32>,
    #[serde(rename = "21")]
    pub b_21: Option<f32>,
    #[serde(rename = "22")]
    pub b_22: Option<f32>,
    #[serde(rename = "23")]
    pub b_23: Option<f32>,
    #[serde(rename = "24")]
    pub b_24: Option<f32>,
    #[serde(rename = "25")]
    pub b_25: Option<f32>,
    #[serde(rename = "144")]
    pub b_144: Option<f32>,
    #[serde(rename = "504")]
    pub b_504: Option<f32>,
    #[serde(rename = "1008")]
    pub b_1008: Option<f32>,
}

/// Represents a UTXO as it is modeled by esplora.
///
/// We ignore the commitments and asset IDs because we need to fetch the full transaction anyway.
/// Hence, we don't even bother with deserializing it here.
#[derive(serde::Deserialize, Debug, PartialEq, Clone, Copy)]
pub struct Utxo {
    pub txid: Txid,
    pub vout: u32,
    pub status: UtxoStatus,
}

#[derive(serde::Deserialize, Debug, PartialEq, Clone, Copy)]
pub struct UtxoStatus {
    pub confirmed: bool,
    pub block_height: Option<u64>,
    pub block_hash: Option<BlockHash>,
    pub block_time: Option<u64>,
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn can_deserialize_confidential_utxo() {
        let utxos = r#"[
  {
    "txid": "26ad78aca6db29fa6ca37337fcfb23498dc1a01ee274614970097ab7ca6b6a19",
    "vout": 0,
    "status": {
      "confirmed": true,
      "block_height": 1099688,
      "block_hash": "e0dd686b1a3334e941512a0e08dda69c9db71cd642d8b219f6063fb81838d86b",
      "block_time": 1607042939
    },
    "valuecommitment": "0959edffa4326a255a15925a5a8eeda37e27fb80a62b1f1792dcd98bb8e29b7496",
    "assetcommitment": "0b7b0f23047a44d6145fb4754f218807c1a3f0acc811221f7ba35e44dfc3a31795",
    "noncecommitment": "039b1feace0413efc144298bc462a90bbf8f269cf68e3dfa65088f84f381921261"
  }
]
"#;

        let utxos = serde_json::from_str::<Vec<Utxo>>(utxos).unwrap();

        assert_eq!(utxos.len(), 1);
    }

    #[test]
    fn can_deserialize_explicit_utxo() {
        let utxos = r#"[
  {
    "txid": "58035633e6391fd08955f9f73b710efe3835a7975baaf1267aa4fcb3c738c1ba",
    "vout": 0,
    "status": {
      "confirmed": true,
      "block_height": 1099644,
      "block_hash": "58d573591f8920b225512bb209b5d75f2ae9260f107c306b87a53c4cc4d42d7e",
      "block_time": 1607040059
    },
    "value": 99958,
    "asset": "6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d"
  }
]
"#;

        let utxos = serde_json::from_str::<Vec<Utxo>>(utxos).unwrap();

        assert_eq!(utxos.len(), 1);
    }
}