#![allow(
    unreachable_patterns,
    reason = "P2PK65 and P2PK33 both serialize as 'p2pk'"
)]

use bitcoin::{
    AddressType, ScriptBuf,
    opcodes::all::{
        OP_CHECKSIG, OP_DUP, OP_EQUAL, OP_EQUALVERIFY, OP_HASH160, OP_PUSHBYTES_0, OP_PUSHBYTES_2,
        OP_PUSHBYTES_20, OP_PUSHBYTES_32, OP_PUSHBYTES_33, OP_PUSHBYTES_65, OP_PUSHNUM_1,
        OP_RETURN,
    },
};
use brk_error::Error;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use strum::Display;
use vecdb::{Bytes, Formattable, Pco, TransparentPco};

use crate::AddrBytes;

#[derive(
    Debug,
    Clone,
    Copy,
    Display,
    PartialEq,
    Eq,
    PartialOrd,
    Ord,
    Serialize,
    Deserialize,
    JsonSchema,
    Hash,
)]
#[serde(rename_all = "lowercase")]
#[strum(serialize_all = "lowercase")]
#[repr(u8)]
/// Type (P2PKH, P2WPKH, P2SH, P2TR, etc.)
pub enum OutputType {
    #[serde(rename = "p2pk")]
    #[strum(serialize = "p2pk")]
    P2PK65,
    #[serde(rename = "p2pk")]
    #[strum(serialize = "p2pk")]
    P2PK33,
    P2PKH,
    #[serde(rename = "multisig")]
    #[strum(serialize = "multisig")]
    P2MS,
    P2SH,
    #[serde(rename = "op_return")]
    #[strum(serialize = "op_return")]
    OpReturn,
    #[serde(rename = "v0_p2wpkh")]
    #[strum(serialize = "v0_p2wpkh")]
    P2WPKH,
    #[serde(rename = "v0_p2wsh")]
    #[strum(serialize = "v0_p2wsh")]
    P2WSH,
    #[serde(rename = "v1_p2tr")]
    #[strum(serialize = "v1_p2tr")]
    P2TR,
    P2A,
    Empty,
    Unknown,
}

impl OutputType {
    pub const ADDR_TYPES: [Self; 8] = [
        Self::P2PK65,
        Self::P2PK33,
        Self::P2PKH,
        Self::P2SH,
        Self::P2WPKH,
        Self::P2WSH,
        Self::P2TR,
        Self::P2A,
    ];

    fn is_valid(value: u8) -> bool {
        value <= Self::Unknown as u8
    }

    pub fn is_spendable(&self) -> bool {
        match self {
            Self::P2PK65 => true,
            Self::P2PK33 => true,
            Self::P2PKH => true,
            Self::P2MS => true,
            Self::P2SH => true,
            Self::OpReturn => false,
            Self::P2WPKH => true,
            Self::P2WSH => true,
            Self::P2TR => true,
            Self::P2A => true,
            Self::Empty => true,
            Self::Unknown => true,
        }
    }

    pub fn is_addr(&self) -> bool {
        match self {
            Self::P2PK65 => true,
            Self::P2PK33 => true,
            Self::P2PKH => true,
            Self::P2MS => false,
            Self::P2SH => true,
            Self::OpReturn => false,
            Self::P2WPKH => true,
            Self::P2WSH => true,
            Self::P2TR => true,
            Self::P2A => true,
            Self::Empty => false,
            Self::Unknown => false,
        }
    }

    pub fn is_not_addr(&self) -> bool {
        !self.is_addr()
    }

    pub fn is_unspendable(&self) -> bool {
        !self.is_spendable()
    }

    /// Whether the address type's public key is revealed at funding time
    /// (vs. only at spending time). For P2PK33/P2PK65 the pubkey is directly
    /// in the locking script; for P2TR the tweaked output key is in the
    /// locking script. All other address types hash the pubkey/script and
    /// only reveal it on spending.
    pub fn pubkey_exposed_at_funding(&self) -> bool {
        matches!(self, Self::P2PK65 | Self::P2PK33 | Self::P2TR)
    }

    pub fn as_vec() -> Vec<Self> {
        vec![
            Self::P2PK65,
            Self::P2PK33,
            Self::P2PKH,
            Self::P2MS,
            Self::P2SH,
            Self::OpReturn,
            Self::P2WPKH,
            Self::P2WSH,
            Self::P2TR,
            Self::P2A,
            Self::Empty,
            Self::Unknown,
        ]
    }
}

impl From<&ScriptBuf> for OutputType {
    #[inline]
    fn from(script: &ScriptBuf) -> Self {
        let bytes = script.as_bytes();

        match bytes.len() {
            0 => Self::Empty,
            4 if bytes == [OP_PUSHNUM_1.to_u8(), OP_PUSHBYTES_2.to_u8(), 78, 115] => Self::P2A,
            22 if bytes[0] == OP_PUSHBYTES_0.to_u8() && bytes[1] == OP_PUSHBYTES_20.to_u8() => {
                Self::P2WPKH
            }
            23 if bytes[0] == OP_HASH160.to_u8()
                && bytes[1] == OP_PUSHBYTES_20.to_u8()
                && bytes[22] == OP_EQUAL.to_u8() =>
            {
                Self::P2SH
            }
            25 if bytes[0] == OP_DUP.to_u8()
                && bytes[1] == OP_HASH160.to_u8()
                && bytes[2] == OP_PUSHBYTES_20.to_u8()
                && bytes[23] == OP_EQUALVERIFY.to_u8()
                && bytes[24] == OP_CHECKSIG.to_u8() =>
            {
                Self::P2PKH
            }
            34 if bytes[0] == OP_PUSHBYTES_0.to_u8() && bytes[1] == OP_PUSHBYTES_32.to_u8() => {
                Self::P2WSH
            }
            34 if bytes[0] == OP_PUSHNUM_1.to_u8() && bytes[1] == OP_PUSHBYTES_32.to_u8() => {
                Self::P2TR
            }
            35 if bytes[0] == OP_PUSHBYTES_33.to_u8() && bytes[34] == OP_CHECKSIG.to_u8() => {
                Self::P2PK33
            }
            67 if bytes[0] == OP_PUSHBYTES_65.to_u8() && bytes[66] == OP_CHECKSIG.to_u8() => {
                Self::P2PK65
            }
            _ if bytes[0] == OP_RETURN.to_u8() => Self::OpReturn,
            _ if script.is_multisig() => Self::P2MS,
            _ => Self::Unknown,
        }
    }
}

impl From<AddressType> for OutputType {
    #[inline]
    fn from(value: AddressType) -> Self {
        match value {
            AddressType::P2a => Self::P2A,
            AddressType::P2pkh => Self::P2PKH,
            AddressType::P2sh => Self::P2SH,
            AddressType::P2tr => Self::P2TR,
            AddressType::P2wpkh => Self::P2WPKH,
            AddressType::P2wsh => Self::P2WSH,
            _ => unreachable!(),
        }
    }
}

impl From<&AddrBytes> for OutputType {
    #[inline]
    fn from(bytes: &AddrBytes) -> Self {
        match bytes {
            AddrBytes::P2PK65(_) => Self::P2PK65,
            AddrBytes::P2PK33(_) => Self::P2PK33,
            AddrBytes::P2PKH(_) => Self::P2PKH,
            AddrBytes::P2SH(_) => Self::P2SH,
            AddrBytes::P2WPKH(_) => Self::P2WPKH,
            AddrBytes::P2WSH(_) => Self::P2WSH,
            AddrBytes::P2TR(_) => Self::P2TR,
            AddrBytes::P2A(_) => Self::P2A,
        }
    }
}

impl TryFrom<OutputType> for AddressType {
    type Error = Error;
    fn try_from(value: OutputType) -> Result<Self, Self::Error> {
        Ok(match value {
            OutputType::P2A => Self::P2a,
            OutputType::P2PKH => Self::P2pkh,
            OutputType::P2SH => Self::P2sh,
            OutputType::P2TR => Self::P2tr,
            OutputType::P2WPKH => Self::P2wpkh,
            OutputType::P2WSH => Self::P2wsh,
            _ => return Err(Error::UnsupportedType(format!("{:?}", value))),
        })
    }
}

impl Formattable for OutputType {
    fn write_to(&self, buf: &mut Vec<u8>) {
        use std::fmt::Write;
        let mut s = String::new();
        write!(s, "{}", self).unwrap();
        buf.extend_from_slice(s.as_bytes());
    }

    fn fmt_json(&self, buf: &mut Vec<u8>) {
        buf.push(b'"');
        self.write_to(buf);
        buf.push(b'"');
    }
}

impl Bytes for OutputType {
    type Array = [u8; size_of::<Self>()];

    #[inline]
    fn to_bytes(&self) -> Self::Array {
        [*self as u8]
    }

    #[inline]
    fn from_bytes(bytes: &[u8]) -> vecdb::Result<Self> {
        if bytes.len() != size_of::<Self>() {
            return Err(vecdb::Error::WrongLength {
                expected: size_of::<Self>(),
                received: bytes.len(),
            });
        };
        let value = bytes[0];
        if !Self::is_valid(value) {
            return Err(vecdb::Error::InvalidArgument("invalid OutputType"));
        }
        // SAFETY: We validated that value is a valid variant
        let s: Self = unsafe { std::mem::transmute(value) };
        Ok(s)
    }
}

impl Pco for OutputType {
    type NumberType = u8;
}

impl TransparentPco<u8> for OutputType {}
