use brk_error::{Error, Result};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use vecdb::{Bytes, Formattable, unlikely};

use crate::{Cents, CentsSats, CentsSquaredSats, EmptyAddrData, OutputType, Sats, SupplyState};

/// Snapshot of cost basis related state.
/// Uses CentsSats (u64) for single-UTXO values, CentsSquaredSats (u128) for capitalized cap.
#[derive(Clone, Debug)]
pub struct CostBasisSnapshot {
    pub realized_price: Cents,
    pub supply_state: SupplyState,
    /// price × sats (fits u64 for individual UTXOs)
    pub price_sats: CentsSats,
    /// price² × sats (needs u128)
    pub capitalized_cap_raw: CentsSquaredSats,
}

impl CostBasisSnapshot {
    /// Create from a single UTXO (computes caps from price × value)
    #[inline]
    pub fn from_utxo(price: Cents, supply: &SupplyState) -> Self {
        let price_sats = CentsSats::from_price_sats(price, supply.value);
        Self {
            realized_price: price,
            supply_state: *supply,
            price_sats,
            capitalized_cap_raw: price_sats.to_capitalized_cap(price),
        }
    }
}

/// Data for a funded (non-empty) address with current balance.
///
/// Kept compact because one value is stored for every funded address.
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)]
#[repr(C)]
pub struct FundedAddrData {
    /// Total transaction count
    pub tx_count: u32,
    /// Number of transaction outputs funded to this address
    pub funded_txo_count: u32,
    /// Number of transaction outputs spent by this address
    pub spent_txo_count: u32,
    #[serde(skip)]
    padding: u32,
    /// Satoshis received by this address
    pub received: Sats,
    /// Satoshis sent by this address
    pub sent: Sats,
    /// The realized capitalization: Σ(price × sats)
    pub realized_cap_raw: CentsSats,
}

impl FundedAddrData {
    pub fn balance(&self) -> Sats {
        (u64::from(self.received) - u64::from(self.sent)).into()
    }

    pub fn realized_price(&self) -> Cents {
        self.realized_cap_raw.realized_price(self.balance())
    }

    #[inline]
    pub fn has_0_sats(&self) -> bool {
        self.balance() == Sats::ZERO
    }

    #[inline]
    pub fn utxo_count(&self) -> u32 {
        self.funded_txo_count
            .checked_sub(self.spent_txo_count)
            .unwrap_or_else(|| {
                panic!(
                    "FundedAddrData corruption: spent_txo_count ({}) > funded_txo_count ({}). \
                Addr data: {:?}",
                    self.spent_txo_count, self.funded_txo_count, self
                )
            })
    }

    #[inline]
    pub fn has_1_utxos(&self) -> bool {
        self.utxo_count() == 1
    }

    #[inline]
    pub fn has_0_utxos(&self) -> bool {
        self.funded_txo_count == self.spent_txo_count
    }

    /// Whether this address currently holds at least one UTXO.
    #[inline]
    pub fn is_funded(&self) -> bool {
        !self.has_0_utxos()
    }

    /// Whether this address has received more than one output over its
    /// lifetime: the receive-side proxy for address reuse (close to but
    /// not exactly "received in 2+ distinct transactions"; over-counts
    /// the rare case of multi-output funding to the same address in one
    /// tx). Matches the industry-standard "address reuse" signal.
    #[inline]
    pub fn is_reused(&self) -> bool {
        self.funded_txo_count > 1
    }

    /// Whether this address has spent more than one output over its
    /// lifetime: the spend-side counterpart to `is_reused`. Captures
    /// "demonstrated reuse via actual spending" and excludes addresses
    /// that received multiple outputs but have not yet been drawn from
    /// more than once.
    #[inline]
    pub fn is_respent(&self) -> bool {
        self.spent_txo_count > 1
    }

    /// Whether this address's public key has been revealed in the chain.
    /// For P2PK33/P2PK65/P2TR the pubkey is in the locking script of any
    /// funding output; for other types it's only revealed when spending.
    #[inline]
    pub fn is_pubkey_exposed(&self, output_type: OutputType) -> bool {
        output_type.pubkey_exposed_at_funding() || self.spent_txo_count > 0
    }

    /// Whether this address currently holds funds AND its pubkey is exposed.
    /// True iff the address contributes to the "funds at quantum risk" set.
    #[inline]
    pub fn is_funded_with_exposed_pubkey(&self, output_type: OutputType) -> bool {
        self.is_funded() && self.is_pubkey_exposed(output_type)
    }

    /// This address's contribution (in sats) to the "funds at quantum risk"
    /// supply: its balance if currently in the funded-exposed set, else 0.
    #[inline]
    pub fn exposed_supply_contribution(&self, output_type: OutputType) -> Sats {
        if self.is_funded_with_exposed_pubkey(output_type) {
            self.balance()
        } else {
            Sats::ZERO
        }
    }

    /// This address's contribution (in sats) to the funded-reused supply:
    /// its balance if currently funded AND reused (received ≥ 2), else 0.
    #[inline]
    pub fn reused_supply_contribution(&self) -> Sats {
        if self.is_funded() && self.is_reused() {
            self.balance()
        } else {
            Sats::ZERO
        }
    }

    /// This address's contribution (in sats) to the funded-respent supply:
    /// its balance if currently funded AND respent (spent ≥ 2), else 0.
    #[inline]
    pub fn respent_supply_contribution(&self) -> Sats {
        if self.is_funded() && self.is_respent() {
            self.balance()
        } else {
            Sats::ZERO
        }
    }

    pub fn receive(&mut self, amount: Sats, price: Cents) {
        self.receive_outputs(amount, price, 1);
    }

    /// Applies received outputs and returns their exact realized-cap delta.
    pub fn receive_outputs(&mut self, amount: Sats, price: Cents, output_count: u32) -> CentsSats {
        self.received += amount;
        self.funded_txo_count += output_count;
        let ps = CentsSats::from_price_sats(price, amount);
        self.realized_cap_raw += ps;
        ps
    }

    /// Applies a spent output and returns its exact realized-cap delta.
    pub fn send(&mut self, amount: Sats, previous_price: Cents) -> Result<CentsSats> {
        if unlikely(self.balance() < amount) {
            return Err(Error::Internal("Previous amount smaller than sent amount"));
        }
        self.sent += amount;
        self.spent_txo_count += 1;
        let ps = CentsSats::from_price_sats(previous_price, amount);
        self.realized_cap_raw -= ps;
        Ok(ps)
    }
}

impl From<EmptyAddrData> for FundedAddrData {
    #[inline]
    fn from(value: EmptyAddrData) -> Self {
        Self::from(&value)
    }
}

impl From<&EmptyAddrData> for FundedAddrData {
    #[inline]
    fn from(value: &EmptyAddrData) -> Self {
        Self {
            tx_count: value.tx_count,
            funded_txo_count: value.funded_txo_count,
            spent_txo_count: value.funded_txo_count,
            padding: 0,
            received: value.transfered,
            sent: value.transfered,
            realized_cap_raw: CentsSats::ZERO,
        }
    }
}

impl std::fmt::Display for FundedAddrData {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "tx_count: {}, funded_txo_count: {}, spent_txo_count: {}, received: {}, sent: {}, realized_cap_raw: {}",
            self.tx_count,
            self.funded_txo_count,
            self.spent_txo_count,
            self.received,
            self.sent,
            self.realized_cap_raw,
        )
    }
}

impl Formattable for FundedAddrData {
    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_csv(&self, f: &mut String) -> std::fmt::Result {
        let start = f.len();
        self.fmt_into(f);
        if f.as_bytes()[start..].contains(&b',') {
            f.insert(start, '"');
            f.push('"');
        }
        Ok(())
    }

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

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

    fn to_bytes(&self) -> Self::Array {
        let mut arr = [0u8; size_of::<Self>()];
        arr[0..4].copy_from_slice(self.tx_count.to_bytes().as_ref());
        arr[4..8].copy_from_slice(self.funded_txo_count.to_bytes().as_ref());
        arr[8..12].copy_from_slice(self.spent_txo_count.to_bytes().as_ref());
        arr[12..16].copy_from_slice(self.padding.to_bytes().as_ref());
        arr[16..24].copy_from_slice(self.received.to_bytes().as_ref());
        arr[24..32].copy_from_slice(self.sent.to_bytes().as_ref());
        arr[32..48].copy_from_slice(self.realized_cap_raw.to_bytes().as_ref());
        arr
    }

    fn from_bytes(bytes: &[u8]) -> vecdb::Result<Self> {
        Ok(Self {
            tx_count: u32::from_bytes(&bytes[0..4])?,
            funded_txo_count: u32::from_bytes(&bytes[4..8])?,
            spent_txo_count: u32::from_bytes(&bytes[8..12])?,
            padding: u32::from_bytes(&bytes[12..16])?,
            received: Sats::from_bytes(&bytes[16..24])?,
            sent: Sats::from_bytes(&bytes[24..32])?,
            realized_cap_raw: CentsSats::from_bytes(&bytes[32..48])?,
        })
    }
}

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

    #[test]
    fn funded_addr_data_stays_compact_and_roundtrips() {
        assert_eq!(size_of::<FundedAddrData>(), 48);

        let mut data = FundedAddrData::default();
        data.receive_outputs(Sats::ONE_BTC, Cents::new(10_000), 2);
        data.send(Sats::new(25_000_000), Cents::new(10_000))
            .unwrap();

        let decoded = FundedAddrData::from_bytes(&data.to_bytes()).unwrap();
        assert_eq!(decoded.tx_count, data.tx_count);
        assert_eq!(decoded.funded_txo_count, data.funded_txo_count);
        assert_eq!(decoded.spent_txo_count, data.spent_txo_count);
        assert_eq!(decoded.received, data.received);
        assert_eq!(decoded.sent, data.sent);
        assert_eq!(decoded.realized_cap_raw, data.realized_cap_raw);
        assert_eq!(
            SupplyState::from(&decoded).utxo_count,
            SupplyState::from(&data).utxo_count
        );
        assert_eq!(
            SupplyState::from(&decoded).value,
            SupplyState::from(&data).value
        );
    }
}
