use anyhow::{anyhow, Result};
use multihash::{Code, Hasher, Multihash, MultihashDigest};
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Entry {
    pub entity: String,
    pub attribute: String,
    pub value: EntryValue,
}

#[derive(Debug, Clone)]
pub struct ImmutableEntry(pub Entry);

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InvariantEntry {
    pub attribute: String,
    pub value: EntryValue,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "t", content = "c")]
pub enum EntryValue {
    String(String),
    Number(f64),
    Address(String),
    Null,
    Invalid,
}

impl std::str::FromStr for EntryValue {
    type Err = std::convert::Infallible;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        if s.len() < 2 {
            match s.chars().next() {
                Some('S') => Ok(EntryValue::String("".into())),
                Some('X') => Ok(EntryValue::Null),
                _ => Ok(EntryValue::Invalid),
            }
        } else {
            let (type_char, content) = s.split_at(1);
            match (type_char, content) {
                ("S", content) => Ok(EntryValue::String(String::from(content))),
                ("N", content) => {
                    if let Ok(n) = content.parse::<f64>() {
                        Ok(EntryValue::Number(n))
                    } else {
                        Ok(EntryValue::Invalid)
                    }
                }
                ("O", content) => {
                    Ok(EntryValue::Address(String::from(content)))
                }
                _ => Ok(EntryValue::Invalid),
            }
        }
    }
}

#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct Hash(pub Vec<u8>);

impl AsRef<[u8]> for Hash {
    fn as_ref(&self) -> &[u8] {
        self.0.as_ref()
    }
}

pub fn hash<T: AsRef<[u8]>>(input: T) -> Hash {
    let mut hasher = multihash::Sha2_256::default();
    hasher.update(input.as_ref());
    Hash(Vec::from(hasher.finalize()))
}

fn b58_encode<T: AsRef<[u8]>>(vec: T) -> String {
    multibase::encode(multibase::Base::Base58Btc, vec.as_ref())
}

#[derive(Clone, Eq, PartialEq, Hash)]
pub enum Address {
    Hash(Hash),
    Attribute(String),
    Url(String),
}

// multihash SHA2-256
const SHA2_256: u64 = 0x12;
// multihash identity
const IDENTITY: u64 = 0x00;

impl Address {
    pub fn encode(&self) -> Result<Vec<u8>> {
        let hash = match self {
            Self::Hash(hash) => Multihash::wrap(SHA2_256, &hash.0).map_err(|err| anyhow!(err))?,
            Self::Attribute(attribute) => {
                Code::Identity.digest(&[&[b'A'], attribute.as_bytes()].concat())
            }
            Self::Url(url) => Code::Identity.digest(&[&[b'X'], url.as_bytes()].concat()),
        };

        Ok(hash.to_bytes())
    }
}

impl std::fmt::Display for Address {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "{}",
            b58_encode(self.encode().map_err(|_| std::fmt::Error)?)
        )
    }
}
