use crate::conventions::{Sat, Usd};
use crate::event::{DisposeKind, LedgerEvent, LotPick};
use crate::identity::{EventId, SourceRef, WalletId};
use crate::price::{fmv_of, PriceProvider};
use crate::project::fold::fold;
use crate::project::resolve::{resolve, Eff, Op};
use crate::state::{Blocker, BlockerKind, DisposalLeg, Lot, Term};
use crate::{ProjectionConfig, TaxDate};
use time::UtcOffset;
#[derive(Debug, Clone)]
pub struct CandidateDisposal {
pub existing_event: Option<EventId>,
pub wallet: WalletId,
pub date: TaxDate,
pub sat: Sat,
pub kind: DisposeKind,
pub proceeds: Option<Usd>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EvaluateOutcome {
pub legs: Vec<DisposalLeg>,
pub st_gain: Usd,
pub lt_gain: Usd,
pub lots_after: Vec<Lot>,
pub blockers: Vec<Blocker>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EvaluateError {
ProceedsRequired,
UnknownExistingDisposal,
}
fn honoring_sat(op: &Op) -> Option<Sat> {
match op {
Op::Dispose { sat, .. }
| Op::GiftOut { sat, .. }
| Op::Donate { sat, .. }
| Op::SelfTransfer { sat, .. } => Some(*sat),
_ => None,
}
}
pub fn evaluate_disposal(
events: &[LedgerEvent],
prices: &dyn PriceProvider,
config: &ProjectionConfig,
candidate: &CandidateDisposal,
selection: Option<&[LotPick]>,
) -> Result<EvaluateOutcome, EvaluateError> {
let mut res = resolve(events, prices, config);
let (target_id, principal): (EventId, Sat) = match &candidate.existing_event {
Some(id) => {
let sat = res
.timeline
.iter()
.find(|e| &e.id == id)
.and_then(|e| honoring_sat(&e.op))
.ok_or(EvaluateError::UnknownExistingDisposal)?;
(id.clone(), sat)
}
None => {
let proceeds = match candidate.proceeds {
Some(p) => p,
None => fmv_of(prices, candidate.date, candidate.sat)
.ok_or(EvaluateError::ProceedsRequired)?,
};
let id = EventId::Decision { seq: u64::MAX };
let utc = candidate.date.midnight().assume_utc();
res.timeline.push(Eff {
id: id.clone(),
utc,
tz: UtcOffset::UTC,
src_priority: 0,
src_ref: SourceRef::new("__synthetic__"),
wallet: Some(candidate.wallet.clone()),
op: Op::Dispose {
sat: candidate.sat,
proceeds,
fee_usd: Usd::ZERO,
fee_sat: None,
kind: candidate.kind,
},
});
(id, candidate.sat)
}
};
let mut extra: Vec<Blocker> = Vec::new();
if let Some(picks) = selection {
let picked: Sat = picks.iter().map(|p| p.sat).sum();
if picked != principal {
extra.push(Blocker {
kind: BlockerKind::LotSelectionInvalid,
event: Some(target_id.clone()),
detail: format!(
"candidate selection must conserve principal: {picked} != {principal}"
),
});
} else {
res.selections.insert(target_id.clone(), picks.to_vec());
}
}
let state = fold(res, prices, config);
let legs: Vec<DisposalLeg> = state
.disposals
.iter()
.filter(|d| d.event == target_id)
.flat_map(|d| d.legs.clone())
.collect();
let st_gain: Usd = legs
.iter()
.filter(|l| l.term == Term::ShortTerm)
.map(|l| l.gain)
.sum();
let lt_gain: Usd = legs
.iter()
.filter(|l| l.term == Term::LongTerm)
.map(|l| l.gain)
.sum();
let mut blockers: Vec<Blocker> = state
.blockers
.iter()
.filter(|b| b.event.as_ref() == Some(&target_id))
.cloned()
.collect();
blockers.extend(extra);
Ok(EvaluateOutcome {
legs,
st_gain,
lt_gain,
lots_after: state.lots,
blockers,
})
}