///|
/// Gregorian dates: inclusive start, exclusive optional end.
pub(all) struct EffectivePlan {
  starts_on : String
  ends_before : String?
  plan : Plan
}

///|
priv struct CompiledPeriod {
  start : Int32
  end : Int32?
  entry : EffectivePlan
}

///|
/// Validated, detached timeline for one plan identity, metric, unit and currency.
pub struct Schedule {
  priv periods : Array[CompiledPeriod]
}

///|
pub(all) struct ScheduledEvidence {
  on : String
  evidence : Evidence
} derive(Debug, Eq)

///|
pub extend ScheduledEvidence with Debug::{to_repr}

///|
pub extend ScheduledEvidence with Eq::{equal, not_equal}

///|
fn date_key(text : String, path : String) -> Int32 raise PricingError {
  if text.length() != 10 || text[4] != '-' || text[7] != '-' {
    invalid(
      "date", path, "expected a Gregorian date YYYY-MM-DD, years 0001..9999; timestamps and time zones are unsupported",
    )
  }
  let mut y = 0
  let mut m = 0
  let mut d = 0
  for i in 0..<10 {
    if i == 4 || i == 7 {
      continue
    }
    let c = text[i]
    if c < '0' || c > '9' {
      invalid("date", path, "date must contain ASCII digits and hyphens")
    }
    let n = c.to_int() - 48
    if i < 4 {
      y = y * 10 + n
    } else if i < 7 {
      m = m * 10 + n
    } else {
      d = d * 10 + n
    }
  }
  if y == 0 || m < 1 || m > 12 {
    invalid("date", path, "year or month is outside the supported range")
  }
  let leap = y % 4 == 0 && (y % 100 != 0 || y % 400 == 0)
  let days = if m == 2 {
    if leap {
      29
    } else {
      28
    }
  } else if [4, 6, 9, 11].contains(m) {
    30
  } else {
    31
  }
  if d < 1 || d > days {
    invalid("date", path, "day does not exist in this Gregorian month")
  }
  y * 10000 + m * 100 + d
}

///|
/// Require ordered nonoverlapping intervals and unique versions. Gaps are
/// allowed but selecting a date inside a gap fails rather than choosing a fallback.
pub fn compile_schedule(
  entries : ArrayView[EffectivePlan],
) -> Schedule raise PricingError {
  if entries.length() == 0 || entries.length() > 128 {
    invalid("size", "schedule.entries", "provide 1 to 128 effective plans")
  }
  let first = entries[0].plan
  let periods : Array[CompiledPeriod] = []
  let versions : Map[String, Unit] = Map([])
  for i, entry in entries {
    let path = "schedule.entries[\{i}]"
    let start = date_key(entry.starts_on, path + ".starts_on")
    let end = entry.ends_before.map(text => {
      date_key(text, path + ".ends_before")
    })
    if end is Some(n) && n <= start {
      invalid("interval", path, "end must be strictly after start")
    }
    if i > 0 {
      let previous = periods[i - 1]
      if start <= previous.start ||
        previous.end is None ||
        (previous.end is Some(n) && start < n) {
        invalid(
          "overlap", path, "intervals must be ordered, disjoint, and only the final end may be null",
        )
      }
    }
    let p = entry.plan
    if p.id != first.id ||
      p.metric != first.metric ||
      p.unit != first.unit ||
      p.currency != first.currency {
      invalid(
        "identity",
        path + ".plan",
        "all versions must share plan id, metric, unit and currency",
      )
    }
    if versions.contains(p.version) {
      invalid(
        "duplicate",
        path + ".plan.version",
        "each price version must have one unique interval",
      )
    }
    versions[p.version] = ()
    periods.push({ start, end, entry, })
  }
  { periods, }
}

///|
/// Select exactly one version by an explicit civil date; usage.period is opaque.
pub fn select_plan(schedule : Schedule, on : String) -> Plan raise PricingError {
  let day = date_key(on, "schedule.on")
  for period in schedule.periods {
    if day >= period.start &&
      (period.end is None || (period.end is Some(n) && day < n)) {
      return period.entry.plan
    }
  }
  invalid(
    "uncovered", "schedule.on", "no price version covers this date; supply an explicit interval instead of assuming the nearest version",
  )
  schedule.periods[0].entry.plan
}

///|
/// Rate one already aggregated snapshot under the selected version.
///
/// ```mbt check
/// test {
///   let p = @jieheng.compile_plan(
///     id="api",
///     version="v1",
///     metric="calls",
///     unit="call",
///     currency="USD",
///     tiers=[{ up_to: None, unit_price: "1", }],
///   )
///   let s = @jieheng.compile_schedule([
///     { starts_on: "2026-10-01", ends_before: None, plan: p, },
///   ])
///   let e = @jieheng.rate_scheduled(
///     s,
///     "2026-10-22",
///     { customer: "acme", metric: "calls", period: "October", quantity: "2", },
///     scale=2,
///     rounding=HalfEven,
///   )
///   inspect(e.evidence.settlement.amount, content="2.00")
/// }
/// ```
pub fn rate_scheduled(
  schedule : Schedule,
  on : String,
  usage : Usage,
  scale~ : Int32,
  rounding~ : Rounding,
) -> ScheduledEvidence raise PricingError {
  let plan = select_plan(schedule, on)
  let calculation = rate(plan, usage)
  {
    on,
    evidence: {
      calculation,
      settlement: settle(calculation, scale~, rounding~),
    },
  }
}

///|
pub fn verify_scheduled(
  schedule : Schedule,
  on : String,
  usage : Usage,
  record : ScheduledEvidence,
  scale~ : Int32,
  rounding~ : Rounding,
) -> Unit raise PricingError {
  let plan = select_plan(schedule, on)
  if record.on != on {
    invalid(
      "identity", "scheduled.on", "date must match the trusted pricing date",
    )
  }
  verify_evidence(plan, usage, record.evidence, scale~, rounding~)
}