use core::fmt;
use core::fmt::Display;
use core::fmt::Formatter;
use core::fmt::Write as _;
use core::hash::Hash;

use beancount_price_spec::PriceSpec;
use tap::Tap as _;
use time::format_description::well_known::Rfc3339;
use time::Date;
use time::OffsetDateTime;

use crate::Acc;
use crate::Account;
use crate::Amount;
use crate::CostSpec;
use crate::Link;
use crate::LinkSet;
use crate::MetadataKey;
use crate::MetadataMap;
use crate::MetadataValue;

#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct Posting {
    pub flag: Option<Flag>,

    pub account: Account,

    pub amount: Option<Amount>,

    pub cost: Option<CostSpec>,

    pub price: Option<PriceSpec>,

    pub meta: MetadataMap,
}

impl Posting {
    pub fn on(account: impl Into<Account>) -> Self {
        let account = account.into();
        let (flag, amount, cost, price, meta) = Default::default();

        Self {
            flag,
            account,
            amount,
            cost,
            price,
            meta,
        }
    }
}

impl Posting {
    #[inline]
    pub fn add_meta(
        &mut self,
        key: impl Into<MetadataKey>,
        value: impl Into<MetadataValue>,
    ) -> &mut Self {
        self.meta.insert(key.into(), value.into());
        self
    }

    #[inline]
    pub fn clear_amount(&mut self) -> &mut Self {
        self.amount = None;
        self
    }

    #[inline]
    pub fn clear_cost(&mut self) -> &mut Self {
        self.cost = None;
        self
    }

    #[inline]
    pub fn clear_flag(&mut self) -> &mut Self {
        self.flag = None;
        self
    }

    #[inline]
    pub fn clear_meta(&mut self) -> &mut Self {
        self.meta.clear();
        self
    }

    #[inline]
    pub fn clear_price(&mut self) -> &mut Self {
        self.price = None;
        self
    }

    #[inline]
    pub fn complete(&mut self) -> &mut Self {
        self.set_flag(Flag::Complete)
    }

    #[inline]
    pub fn incomplete(&mut self) -> &mut Self {
        self.set_flag(Flag::Incomplete)
    }

    #[inline]
    pub fn set_account(&mut self, account: impl Into<Account>) -> &mut Self {
        self.account = account.into();
        self
    }

    #[inline]
    pub fn set_amount(&mut self, amount: Amount) -> &mut Self {
        self.amount = Some(amount);
        self
    }

    #[inline]
    pub fn set_cost(&mut self, cost: impl Into<CostSpec>) -> &mut Self {
        self.cost = Some(cost.into());
        self
    }

    #[inline]
    pub fn set_flag(&mut self, flag: Flag) -> &mut Self {
        self.flag = Some(flag);
        self
    }

    #[inline]
    pub fn set_meta(&mut self, meta: impl Into<MetadataMap>) -> &mut Self {
        self.meta = meta.into();
        self
    }

    #[inline]
    pub fn set_price(&mut self, price: impl Into<PriceSpec>) -> &mut Self {
        self.price = Some(price.into());
        self
    }
}

impl Display for Posting {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        let Self {
            flag,
            account,
            amount,
            cost,
            price,
            meta,
        } = self;
        if let Some(flag) = flag {
            write!(f, "{flag} ")?;
        }

        write!(f, "{account}")?;

        if let Some(amount) = amount {
            write!(f, " {amount}")?;

            if let Some(cost) = cost {
                write!(f, " {cost}")?;
            }

            if let Some(price) = price {
                write!(f, " {price}")?;
            }
        }

        for (key, value) in meta {
            write!(f, "\n    {key}: {value}")?;
        }

        Ok(())
    }
}

#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct Transaction {
    pub date: Date,

    pub flag: Flag,

    // TODO consider using Cow
    pub payee: Option<String>,

    // TODO consider using Cow
    pub narration: Option<String>,

    pub links: LinkSet,

    pub meta: MetadataMap,

    // TODO consider using smallvecs
    pub postings: Vec<Posting>,
}

impl Transaction {
    #[must_use]
    pub fn on(date: Date) -> Self {
        let (flag, links, meta, payee, narration, postings) = Default::default();

        Self {
            date,
            flag,
            payee,
            narration,
            links,
            meta,
            postings,
        }
    }
}

impl Transaction {
    #[inline]
    pub fn add_posting(&mut self, posting: Posting) -> &mut Self {
        self.postings.push(posting);
        self
    }

    #[inline]
    pub fn add_meta(
        &mut self,
        key: impl Into<MetadataKey>,
        value: impl Into<MetadataValue>,
    ) -> &mut Self {
        self.meta.insert(key.into(), value.into());
        self
    }

    #[inline]
    pub fn add_link(&mut self, link: impl Into<Link>) -> &mut Self {
        self.links.insert(link.into());
        self
    }

    #[inline]
    pub fn build_posting(
        &mut self,
        on: impl Into<Account>,
        block: impl FnOnce(&mut Posting),
    ) -> &mut Self {
        self.add_posting(Posting::on(on).tap_mut(block))
    }

    #[inline]
    pub fn clear_meta(&mut self) -> &mut Self {
        self.meta.clear();
        self
    }

    #[inline]
    pub fn clear_narration(&mut self) -> &mut Self {
        self.narration = None;
        self
    }

    #[inline]
    pub fn clear_payee(&mut self) -> &mut Self {
        self.payee = None;
        self
    }

    #[inline]
    pub fn clear_postings(&mut self) -> &mut Self {
        self.postings.clear();
        self
    }

    #[inline]
    pub fn complete(&mut self) -> &mut Self {
        self.set_flag(Flag::Complete)
    }

    #[inline]
    pub fn incomplete(&mut self) -> &mut Self {
        self.set_flag(Flag::Incomplete)
    }

    #[inline]
    pub fn set_date(&mut self, date: Date) -> &mut Self {
        self.date = date;
        self
    }

    #[inline]
    pub fn set_flag(&mut self, flag: Flag) -> &mut Self {
        self.flag = flag;
        self
    }

    #[inline]
    pub fn set_meta(&mut self, meta: impl Into<MetadataMap>) -> &mut Self {
        self.meta = meta.into();
        self
    }

    #[inline]
    pub fn set_narration(&mut self, narration: impl Into<String>) -> &mut Self {
        self.narration = Some(narration.into());
        self
    }

    #[inline]
    pub fn set_payee(&mut self, payee: impl Into<String>) -> &mut Self {
        self.payee = Some(payee.into());
        self
    }

    #[inline]
    pub fn set_postings(&mut self, postings: impl Into<Vec<Posting>>) -> &mut Self {
        self.postings = postings.into();
        self
    }
}

impl Transaction {
    #[must_use]
    pub fn main_account(&self) -> Option<&Acc> {
        self.postings
            .first()
            .map(|posting| posting.account.as_ref())
    }

    #[inline]
    pub fn timestamp(&self) -> Option<OffsetDateTime> {
        self.meta
            .get("timestamp")
            .and_then(MetadataValue::as_str)
            .and_then(|timestamp| OffsetDateTime::parse(timestamp, &Rfc3339).ok())
    }
}

impl Display for Transaction {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        let Self {
            date,
            flag,
            payee,
            links,
            meta,
            narration,
            postings,
        } = self;

        write!(f, "{date} {flag}")?;

        match (payee, narration) {
            (None, None) => {}
            (None, Some(narration)) => write!(f, r#" "{narration}""#)?,
            (Some(payee), None) => write!(f, r#" "{payee}" """#)?,
            (Some(payee), Some(narration)) => write!(f, r#" "{payee}" "{narration}""#)?,
        }

        for link in links {
            write!(f, "\n  {link}")?;
        }

        for (key, value) in meta {
            write!(f, "\n  {key}: {value}")?;
        }

        for posting in postings {
            write!(f, "\n  {posting}")?;
        }

        Ok(())
    }
}

#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum Flag {
    #[default]
    Complete,
    Incomplete,
}

impl Display for Flag {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        f.write_char(match self {
            Self::Complete => '*',
            Self::Incomplete => '!',
        })
    }
}