# Positioning defaults — make_* helpers that apply cross-field contracts
# on top of the raw schema in `../schemas/positioning.ncl`.
#
# Same pattern as adr-defaults.ncl: schema declares record shapes, defaults
# applies the cross-field invariants (multi-field reads) on the merged record.

let s = import "../schemas/positioning.ncl" in

# value-prop: audience_ids must not be empty — a claim without a target is
# positioning into the void.
let _non_empty_audience_ids = std.contract.custom (
  fun label =>
    fun value =>
      if std.array.length value.audience_ids == 0 then
        'Error {
          message = "value-prop '%{value.id}': audience_ids must not be empty — a claim without a target audience is positioning into the void"
        }
      else
        'Ok value
) in

# value-prop: status='Live requires non-empty evidence_adrs — a Live claim
# that cites no architectural decision as its backing is marketing without
# substance. Drafts may be unbacked (the ADRs may not exist yet).
let _live_requires_evidence_adrs = std.contract.custom (
  fun label =>
    fun value =>
      if value.status == 'Live && std.array.length value.evidence_adrs == 0 then
        'Error {
          message = "value-prop '%{value.id}': status='Live but evidence_adrs is empty — Live claims must cite at least one ADR as architectural backing"
        }
      else
        'Ok value
) in

# value-prop / campaign: status='Retired requires retired_at (audit trail).
let _retired_has_date = std.contract.custom (
  fun label =>
    fun value =>
      if value.status == 'Retired && !(std.record.has_field "retired_at" value) then
        'Error {
          message = "'%{value.id}': status='Retired requires retired_at (ISO date) for audit trail"
        }
      else
        'Ok value
) in

# value-prop: if superseded_by is set, status must be 'Retired (forbidden to
# claim supersession while remaining Live).
let _superseded_implies_retired = std.contract.custom (
  fun label =>
    fun value =>
      if std.record.has_field "superseded_by" value && value.status != 'Retired then
        'Error {
          message = "value-prop '%{value.id}': superseded_by is set but status is not 'Retired — supersession implies retirement"
        }
      else
        'Ok value
) in

# launch_check: per-tag well-formedness. Mirrors EachCheckWellFormed in
# adr-constraints.ncl. Kept in sync manually until ADR-035's lift-out lands.
let _each_launch_check_well_formed = std.contract.custom (
  fun label =>
    fun checks =>
      let validate = fun c =>
        let tag = c.tag in
        let needs = fun field => !(std.record.has_field field c) in
        if tag == 'Grep then
          if needs "pattern" || needs "paths" || needs "must_be_empty" then
            "Grep check requires 'pattern', 'paths', 'must_be_empty'"
          else ""
        else if tag == 'NuCmd then
          if needs "cmd" || needs "expect_exit" then
            "NuCmd check requires 'cmd' and 'expect_exit'"
          else ""
        else if tag == 'ApiCall then
          if needs "endpoint" || needs "json_path" || needs "expected" then
            "ApiCall check requires 'endpoint', 'json_path', 'expected'"
          else ""
        else if tag == 'FileExists then
          if needs "path" || needs "present" then
            "FileExists check requires 'path' and 'present'"
          else ""
        else
          "unknown launch_check tag '%{std.to_str tag}'"
      in
      let first_err =
        std.array.fold_left
          (fun acc c => if acc != "" then acc else validate c)
          ""
          checks
      in
      if first_err == "" then 'Ok checks
      else 'Error { message = first_err }
) in

# value-prop: status='Live requires >=1 differentiator — the QUÉ×QUIÉN seam.
# A Live claim aimed at an audience with no differential value behind it is
# positioning without substance (coherence rule 3, local half).
let _live_requires_differentiator = std.contract.custom (
  fun label =>
    fun value =>
      if value.status == 'Live && std.array.length value.differentiator_ids == 0 then
        'Error {
          message = "value-prop '%{value.id}': status='Live but differentiator_ids is empty — a Live claim must bind to >=1 differentiator (the QUÉ axis)"
        }
      else
        'Ok value
) in

# differentiator: must be anchored — >=1 linked_node AND >=1 evidence_adr.
# An unanchored differentiator is a feature claim, not a value difference.
let _diff_requires_anchor = std.contract.custom (
  fun label =>
    fun value =>
      if std.array.length value.linked_nodes == 0 || std.array.length value.evidence_adrs == 0 then
        'Error {
          message = "differentiator '%{value.id}': must cite >=1 linked_node and >=1 evidence_adr — an unanchored differentiator is a feature, not a value difference"
        }
      else
        'Ok value
) in

# differentiator: `vs` must name >=1 competitor category it beats.
let _diff_requires_vs = std.contract.custom (
  fun label =>
    fun value =>
      if std.array.length value.vs == 0 then
        'Error {
          message = "differentiator '%{value.id}': vs must name >=1 competitor category that does NOT do this"
        }
      else
        'Ok value
) in

# difusion mechanism: >=1 channel AND >=1 thing it difunde (value-prop or
# differentiator) — difusión with no channel or no payload is empty.
let _mechanism_non_empty = std.contract.custom (
  fun label =>
    fun value =>
      if std.array.length value.channels == 0 then
        'Error {
          message = "mechanism '%{value.id}': channels must not be empty — difusión needs a channel"
        }
      else if std.array.length value.value_prop_ids == 0 && std.array.length value.differentiator_ids == 0 then
        'Error {
          message = "mechanism '%{value.id}': must difundir >=1 value-prop or differentiator"
        }
      else
        'Ok value
) in

# A service-backed mechanism cannot be 'Live while its execution backend is not
# operational. The drift-audit property applied to execution: the positioning
# layer refuses to publish a channel as live before the service that runs it is
# actually configured and serving. Mechanisms without a backend (Web, Talk, …)
# are unaffected — the gate only fires when a backend is present.
let _backend_live_requires_operational = std.contract.custom (
  fun label =>
    fun value =>
      if value.status == 'Live
         && std.record.has_field "backend" value
         && !value.backend.operational then
        'Error {
          message = "mechanism '%{value.id}': status='Live but backend not operational — %{value.backend.service} at %{value.backend.endpoint} must serve before the channel goes Live"
        }
      else
        'Ok value
) in

# proof: converges must carry >=1 id on each axis — the triple seam. A proof
# is the convergence of all three or it is not a proof.
let _proof_converges_all = std.contract.custom (
  fun label =>
    fun value =>
      let c = value.converges in
      if std.array.length c.differentiator_ids == 0
      || std.array.length c.audience_ids == 0
      || std.array.length c.mechanism_ids == 0 then
        'Error {
          message = "proof '%{value.id}': converges must reference >=1 differentiator, >=1 audience and >=1 mechanism — a proof is the triple convergence or it is not one"
        }
      else
        'Ok value
) in

# viability-path: converges must carry >=1 audience, >=1 mechanism and >=1
# value-prop — a sustaining flow that does not draw from an audience reached by a
# difusión mechanism monetizing a claim is not a path, it is a wish.
let _viability_converges_all = std.contract.custom (
  fun label =>
    fun value =>
      let c = value.converges in
      if std.array.length c.audience_ids == 0
      || std.array.length c.mechanism_ids == 0
      || std.array.length c.value_prop_ids == 0 then
        'Error {
          message = "viability-path '%{value.id}': converges must reference >=1 audience, >=1 mechanism and >=1 value-prop — a sustaining flow is the convergence of PARA QUIÉN × CÓMO × (QUÉ×QUIÉN) or it is not a path"
        }
      else
        'Ok value
) in

# viability-path: must be anchored to >=1 ontology node — a viability path is
# meant to engage the openness-vs-sustainability tension explicitly, not float
# free of the substrate. Mirrors _diff_requires_anchor's linked_nodes half.
let _viability_anchored = std.contract.custom (
  fun label =>
    fun value =>
      if std.array.length value.linked_nodes == 0 then
        'Error {
          message = "viability-path '%{value.id}': must cite >=1 linked_node — a viability path that engages no ontology node (typically openness-vs-sustainability) is reasoned from outside the graph"
        }
      else
        'Ok value
) in

# viability-path: a realized status (Validated|Live) requires realized_outcome —
# you cannot claim a flow sustains the project without a real, measured result.
# One-directional gate (realized ⇒ outcome present), NOT a Hard biconditional on
# the Spiral question: Draft paths are hypotheses and may carry no outcome.
let _realized_requires_outcome = std.contract.custom (
  fun label =>
    fun value =>
      if (value.status == 'Validated || value.status == 'Live)
      && !(std.record.has_field "realized_outcome" value) then
        'Error {
          message = "viability-path '%{value.id}': status='%{std.to_str value.status} but realized_outcome is absent — a realized path must carry the real, measured compensation outcome; Draft paths may omit it"
        }
      else
        'Ok value
) in

# viability-path: a Live path requires >=1 evidence_adr — a flow declared live as
# architectural commitment must cite the decision that froze it. Drafts may be
# unbacked (the ADR may not exist yet). Mirrors _live_requires_evidence_adrs.
let _viability_live_requires_adr = std.contract.custom (
  fun label =>
    fun value =>
      if value.status == 'Live && std.array.length value.evidence_adrs == 0 then
        'Error {
          message = "viability-path '%{value.id}': status='Live but evidence_adrs is empty — a Live viability path must cite >=1 ADR as architectural backing"
        }
      else
        'Ok value
) in

{
  make_audience = fun data => s.Audience & data,

  make_value_prop = fun data =>
    let r1 | _non_empty_audience_ids        = s.ValueProp & data in
    let r2 | _live_requires_evidence_adrs   = r1 in
    let r3 | _live_requires_differentiator  = r2 in
    let r4 | _retired_has_date              = r3 in
    let r5 | _superseded_implies_retired    = r4 in
    let _  | _each_launch_check_well_formed = r5.launch_criteria in
    r5,

  make_campaign = fun data =>
    let r1 | _retired_has_date             = s.Campaign & data in
    let _  | _each_launch_check_well_formed = r1.launch_criteria in
    r1,

  make_differentiator = fun data =>
    let r1 | _diff_requires_anchor = s.Differentiator & data in
    let r2 | _diff_requires_vs     = r1 in
    let _  | _retired_has_date     = r2 in
    r2,

  make_competitor = fun data => s.Competitor & data,

  make_difusion_mechanism = fun data =>
    let r1 | _mechanism_non_empty              = s.DifusionMechanism & data in
    let r2 | _retired_has_date                 = r1 in
    let r3 | _backend_live_requires_operational = r2 in
    let _  | _each_launch_check_well_formed    = r3.launch_criteria in
    r3,

  make_proof = fun data =>
    let r1 | _proof_converges_all           = s.Proof & data in
    let r2 | _retired_has_date              = r1 in
    let _  | _each_launch_check_well_formed = r2.launch_criteria in
    r2,

  make_viability_path = fun data =>
    let r1 | _viability_converges_all       = s.ViabilityPath & data in
    let r2 | _viability_anchored            = r1 in
    let r3 | _realized_requires_outcome     = r2 in
    let r4 | _viability_live_requires_adr   = r3 in
    let r5 | _retired_has_date              = r4 in
    let _  | _each_launch_check_well_formed = r5.launch_criteria in
    r5,

  Audience          = s.Audience,
  ValueProp         = s.ValueProp,
  Campaign          = s.Campaign,
  Differentiator    = s.Differentiator,
  Competitor        = s.Competitor,
  DifusionMechanism = s.DifusionMechanism,
  MechanismBackend  = s.MechanismBackend,
  Proof             = s.Proof,
  ViabilityPath     = s.ViabilityPath,
  LaunchCheck       = s.LaunchCheck,
  LaunchCheckTag    = s.LaunchCheckTag,
  PositioningStatus = s.PositioningStatus,
  AudienceStatus    = s.AudienceStatus,
  MechanismKind     = s.MechanismKind,
  ProofKind         = s.ProofKind,
  Directness        = s.Directness,
  FlowKind          = s.FlowKind,
}
