///|
pub struct StatementRecord {
  end_to_end_id : String
  amount : Decimal
  currency : String
  credit_debit : String
}

///|
pub(all) enum LifecycleState {
  Settled
  SettledNotPosted
  Rejected
  Pending
  MissingStatus
  StatementMismatch
  StatementBeforeFinalStatus
}

///|
pub fn LifecycleState::name(self : LifecycleState) -> String {
  match self {
    Settled => "SETTLED"
    SettledNotPosted => "SETTLED_NOT_POSTED"
    Rejected => "REJECTED"
    Pending => "PENDING"
    MissingStatus => "MISSING_STATUS"
    StatementMismatch => "STATEMENT_MISMATCH"
    StatementBeforeFinalStatus => "STATEMENT_BEFORE_FINAL_STATUS"
  }
}

///|
pub struct LifecycleCase {
  end_to_end_id : String
  amount : Decimal
  currency : String
  status : String?
  statement_amount : Decimal?
  statement_currency : String?
  state : LifecycleState
  action : String
}

///|
pub struct LifecycleReport {
  cases : Array[LifecycleCase]
  orphan_statuses : Array[String]
  orphan_statements : Array[String]
}

///|
pub fn statement_records(xml_text : String) -> Array[StatementRecord] raise XmlParseError {
  let fields = flatten_xml(xml_text)
  let ids = texts_by_suffix(fields, "/NtryDtls/TxDtls/Refs/EndToEndId")
  let amounts = texts_by_suffix(fields, "/Ntry/Amt")
  let amount_elements = elements_by_suffix(fields, "/Ntry/Amt")
  let indicators = texts_by_suffix(fields, "/Ntry/CdtDbtInd")
  let count = ids.length()
  let amount_count = if amounts.length() < count { amounts.length() } else { count }
  let records : Array[StatementRecord] = []
  for i = 0; i < amount_count; i = i + 1 {
    let currency = if i < amount_elements.length() {
      amount_elements[i].get_attribute("Ccy")
    } else {
      None
    }
    match Decimal::parse(amounts[i]) {
      Some(amount) =>
        match currency {
          Some(currency_code) => {
            let direction = if i < indicators.length() {
              indicators[i]
            } else {
              ""
            }
            records.push({
              end_to_end_id: ids[i],
              amount,
              currency: currency_code,
              credit_debit: direction,
            })
          }
          None => ()
        }
      None => ()
    }
  }
  records
}

///|
fn action_for_state(state : LifecycleState) -> String {
  match state {
    Settled => "No action; retain the report as audit evidence."
    SettledNotPosted => "Check statement import and bank posting."
    Rejected => "Review the rejection reason and decide whether to resend."
    Pending => "Query the payment status again later."
    MissingStatus => "Request a status report from the payment provider."
    StatementMismatch => "Open an exception and verify amount or currency."
    StatementBeforeFinalStatus => "Reconcile the statement entry with the current status."
  }
}

///|
fn classify_case(
  status : String?,
  statement : StatementRecord?,
  payment : PaymentRecord,
) -> LifecycleState {
  match statement {
    Some(entry) =>
      if entry.amount.compare(payment.amount) != 0 || entry.currency != payment.currency {
        return StatementMismatch
      }
    None => ()
  }
  match status {
    Some(value) =>
      if value == "ACSC" {
        match statement {
          Some(_) => Settled
          None => SettledNotPosted
        }
      } else if value == "RJCT" {
        Rejected
      } else {
        match statement {
          Some(_) => StatementBeforeFinalStatus
          None => Pending
        }
      }
    None => MissingStatus
  }
}

///|
pub fn lifecycle_report(
  pain_xml : String,
  status_xml : String,
  statement_xml : String,
) -> LifecycleReport raise XmlParseError {
  let payments = pain001_payment_records(pain_xml)
  let statuses = status_records(status_xml)
  let statements = statement_records(statement_xml)
  let status_map : Map[String, String] = Map([])
  for record in statuses {
    status_map.set(record.end_to_end_id, record.status)
  }
  let statement_map : Map[String, StatementRecord] = Map([])
  for record in statements {
    statement_map.set(record.end_to_end_id, record)
  }
  let payment_ids : Map[String, Bool] = Map([])
  let cases : Array[LifecycleCase] = []
  for payment in payments {
    payment_ids.set(payment.end_to_end_id, true)
    let status = status_map.get(payment.end_to_end_id)
    let statement = statement_map.get(payment.end_to_end_id)
    let state = classify_case(status, statement, payment)
    cases.push({
      end_to_end_id: payment.end_to_end_id,
      amount: payment.amount,
      currency: payment.currency,
      status,
      statement_amount: match statement {
        Some(entry) => Some(entry.amount)
        None => None
      },
      statement_currency: match statement {
        Some(entry) => Some(entry.currency)
        None => None
      },
      state,
      action: action_for_state(state),
    })
  }
  let orphan_statuses : Array[String] = []
  for record in statuses {
    if !payment_ids.contains(record.end_to_end_id) {
      orphan_statuses.push(record.end_to_end_id)
    }
  }
  let orphan_statements : Array[String] = []
  for record in statements {
    if !payment_ids.contains(record.end_to_end_id) {
      orphan_statements.push(record.end_to_end_id)
    }
  }
  { cases, orphan_statuses, orphan_statements }
}

///|
pub fn LifecycleReport::to_text(self : LifecycleReport) -> String {
  let output = StringBuilder()
  output.write_string("Payment lifecycle report\n")
  for case in self.cases {
    output.write_string("\n")
    output.write_string(case.end_to_end_id)
    output.write_string(" | ")
    output.write_string(case.amount.to_string())
    output.write_string(" ")
    output.write_string(case.currency)
    output.write_string(" | status=")
    output.write_string(match case.status {
      Some(value) => value
      None => "(missing)"
    })
    output.write_string(" | state=")
    output.write_string(case.state.name())
    output.write_string("\nAction: ")
    output.write_string(case.action)
    output.write_string("\n")
  }
  if self.orphan_statuses.length() > 0 {
    output.write_string("\nStatus entries without payment:")
    for id in self.orphan_statuses {
      output.write_string("\n- ")
      output.write_string(id)
    }
  }
  if self.orphan_statements.length() > 0 {
    output.write_string("\nStatement entries without payment:")
    for id in self.orphan_statements {
      output.write_string("\n- ")
      output.write_string(id)
    }
  }
  output.to_string()
}

///|
pub fn LifecycleReport::to_json(self : LifecycleReport) -> String {
  let cases : Array[Json] = []
  for case in self.cases {
    let status : Json = match case.status {
      Some(value) => Json::string(value)
      None => Json::null()
    }
    let statement_amount : Json = match case.statement_amount {
      Some(value) => Json::string(value.to_string())
      None => Json::null()
    }
    let statement_currency : Json = match case.statement_currency {
      Some(value) => Json::string(value)
      None => Json::null()
    }
    cases.push({
      "endToEndId": case.end_to_end_id,
      "amount": case.amount.to_string(),
      "currency": case.currency,
      "status": status,
      "statementAmount": statement_amount,
      "statementCurrency": statement_currency,
      "state": case.state.name(),
      "action": case.action,
    })
  }
  let orphan_statuses : Array[Json] = []
  for id in self.orphan_statuses {
    orphan_statuses.push(Json::string(id))
  }
  let orphan_statements : Array[Json] = []
  for id in self.orphan_statements {
    orphan_statements.push(Json::string(id))
  }
  let root = Map([])
  root.set("cases", Json::array(cases))
  root.set("orphanStatuses", Json::array(orphan_statuses))
  root.set("orphanStatements", Json::array(orphan_statements))
  Json::object(root).stringify(indent=2)
}

///|
pub fn LifecycleReport::to_markdown(self : LifecycleReport) -> String {
  let output = StringBuilder()
  output.write_string("# Payment lifecycle report\n\n")
  output.write_string("| EndToEndId | Amount | Status | State | Action |\n")
  output.write_string("|---|---|---|---|---|\n")
  for case in self.cases {
    output.write_string("| `")
    output.write_string(case.end_to_end_id)
    output.write_string("` | ")
    output.write_string(case.amount.to_string())
    output.write_string(" ")
    output.write_string(case.currency)
    output.write_string(" | ")
    output.write_string(match case.status {
      Some(value) => value
      None => "(missing)"
    })
    output.write_string(" | `")
    output.write_string(case.state.name())
    output.write_string("` | ")
    output.write_string(case.action)
    output.write_string(" |\n")
  }
  output.write_string("\n## Orphan entries\n\n")
  output.write_string("- Status entries without payment: ")
  output.write_string(self.orphan_statuses.length().to_string())
  output.write_string("\n- Statement entries without payment: ")
  output.write_string(self.orphan_statements.length().to_string())
  output.write_string("\n")
  output.to_string()
}