///|
/// A `LEDGERBAL` or `AVAILBAL` aggregate: an amount and the instant it was
/// true.
///
/// `kind` is the tag the balance came from, so a report can say which balance
/// it is printing. The amount stays text for the same reason a transaction
/// amount does: the caller knows the scale the ledger stores.
pub struct Balance {
  kind : String
  amount : String
  as_of : String
}

///|
/// The balance amount at a scale, or `None` when it cannot be represented.
pub fn Balance::amount_minor_units(self : Balance, scale : Int) -> Int64? {
  amount_to_minor_units(self.amount, scale)
}

///|
/// The instant the balance was true, or `None` when the document's `DTASOF` is
/// not a date this importer accepts.
pub fn Balance::as_of_date(self : Balance) -> PostedDate? {
  parse_posted_date(self.as_of)
}

///|
/// The balance as `KIND amount as of DATE`, for a report line.
pub fn Balance::to_string(self : Balance) -> String {
  self.kind + " " + self.amount + " as of " + self.as_of
}

///|
/// One collected balance aggregate before it has been validated.
priv struct RawBalance {
  kind : String
  amount : String
  as_of : String
  amount_line : Int
  as_of_line : Int
}

///|
/// Collect every `LEDGERBAL`/`AVAILBAL`/`BAL` aggregate in the token stream.
///
/// A new aggregate of the same family starts a new record, so both dialects
/// work: XML closes the previous one explicitly, and SGML simply starts the
/// next one. Validation happens afterwards, in [`validate_balance`], because a
/// record is only complete once its last field has been seen.
fn collect_balances(tokens : Array[TagToken]) -> Array[RawBalance] {
  let collected : Array[RawBalance] = []
  let mut kind = ""
  let mut amount = ""
  let mut as_of = ""
  let mut amount_line = 0
  let mut as_of_line = 0
  for token in tokens {
    let tag = token.name
    match close_target(tag) {
      Some(target) =>
        if target == kind {
          collected.push({ kind, amount, as_of, amount_line, as_of_line, })
          kind = ""
          amount = ""
          as_of = ""
        }
      None =>
        match tag {
          "LEDGERBAL" | "AVAILBAL" | "BAL" => {
            if kind != "" {
              collected.push({ kind, amount, as_of, amount_line, as_of_line, })
            }
            kind = tag
            amount = ""
            as_of = ""
            amount_line = token.line
            as_of_line = token.line
          }
          "BALAMT" =>
            if kind != "" {
              amount = token.value
              amount_line = token.line
            }
          "DTASOF" =>
            if kind != "" {
              as_of = token.value
              as_of_line = token.line
            }
          _ => ()
        }
    }
  }
  if kind != "" {
    collected.push({ kind, amount, as_of, amount_line, as_of_line, })
  }
  collected
}

///|
/// Check one collected balance and keep it only if both fields are usable.
///
/// A balance is an amount and a time; half of one is worse than none, because a
/// caller that reconciles against it would be comparing against a value the
/// document never stated.
fn validate_balance(
  raw : RawBalance,
  diagnostics : Array[Diagnostic],
) -> Balance? {
  if raw.amount == "" {
    diagnostics.push(
      error_at("invalid-balance-amount", at_text("missing", raw.amount_line)),
    )
    return None
  }
  if !is_valid_amount(raw.amount) {
    diagnostics.push(
      error_at("invalid-balance-amount", at_text(raw.amount, raw.amount_line)),
    )
    return None
  }
  if raw.as_of == "" {
    diagnostics.push(
      error_at("invalid-balance-date", at_text("missing", raw.as_of_line)),
    )
    return None
  }
  if !valid_date(raw.as_of) {
    diagnostics.push(
      error_at("invalid-balance-date", at_text(raw.as_of, raw.as_of_line)),
    )
    return None
  }
  Some({ kind: raw.kind, amount: raw.amount, as_of: raw.as_of, })
}

///|
/// Read the ledger and available balances out of a token stream.
///
/// The first usable balance of each kind is kept: a document that states the
/// same balance twice is not more correct for having said it twice.
fn parse_balances(
  tokens : Array[TagToken],
  diagnostics : Array[Diagnostic],
) -> (Balance?, Balance?) {
  let mut ledger : Balance? = None
  let mut available : Balance? = None
  for raw in collect_balances(tokens) {
    match validate_balance(raw, diagnostics) {
      Some(balance) =>
        if balance.kind == "AVAILBAL" {
          if available is None {
            available = Some(balance)
          }
        } else if ledger is None {
          ledger = Some(balance)
        }
      None => ()
    }
  }
  (ledger, available)
}

///|
/// The result of checking a closing balance against an opening balance plus the
/// transactions between them.
///
/// `difference` is `declared - computed`, so a positive value means the document
/// claims more money than the transaction list accounts for. `matched` is the
/// answer to the only question that matters before writing to a ledger.
pub struct Reconciliation {
  opening : Int64
  declared_closing : Int64
  computed_closing : Int64
  difference : Int64
  matched : Bool
}

///|
/// Whether the declared closing balance equals the computed one.
pub fn Reconciliation::matched(self : Reconciliation) -> Bool {
  self.matched
}

///|
/// The signed difference `declared - computed`, in minor units.
pub fn Reconciliation::difference(self : Reconciliation) -> Int64 {
  self.difference
}

///|
/// The reconciliation as one line, with the difference spelled out.
pub fn Reconciliation::to_string(self : Reconciliation) -> String {
  let verdict = if self.matched { "reconciled" } else { "MISMATCH" }
  verdict +
  ": opening " +
  self.opening.to_string() +
  " + transactions = " +
  self.computed_closing.to_string() +
  ", declared " +
  self.declared_closing.to_string() +
  ", difference " +
  self.difference.to_string()
}

///|
/// Check that an opening balance plus every transaction equals a closing
/// balance, all at the same scale.
///
/// `None` means the arithmetic cannot be done honestly: an amount that will not
/// convert at this scale, or a sum that overflows `Int64`. Returning `None` is
/// deliberate — reporting "reconciled" because the addition wrapped around is
/// exactly the failure this function exists to prevent.
pub fn reconcile(
  opening : Int64,
  closing : Int64,
  transactions : Array[Transaction],
  scale : Int,
) -> Reconciliation? {
  if !valid_scale(scale) {
    return None
  }
  let mut total = opening
  for transaction in transactions {
    let minor = match transaction.amount_minor_units(scale) {
      Some(value) => value
      None => return None
    }
    total = match checked_add(total, minor) {
      Some(sum) => sum
      None => return None
    }
  }
  let difference = match checked_sub(closing, total) {
    Some(value) => value
    None => return None
  }
  Some({
    opening,
    declared_closing: closing,
    computed_closing: total,
    difference,
    matched: difference == 0L,
  })
}

///|
/// Reconcile a statement's own ledger balance against an opening balance the
/// caller supplies plus the transactions the statement holds.
///
/// `None` when the statement declares no ledger balance, when an amount cannot
/// be converted at `scale`, or when the arithmetic overflows.
pub fn Statement::reconcile(
  self : Statement,
  opening : Int64,
  scale : Int,
) -> Reconciliation? {
  let declared = match self.ledger_balance {
    Some(balance) => balance
    None => return None
  }
  let closing = match declared.amount_minor_units(scale) {
    Some(value) => value
    None => return None
  }
  reconcile(opening, closing, self.transactions, scale)
}