// ── engine.jsx ────────────────────────────────────────────────────────────────
// ORCHESTRATION. Every estimate is summed from pieces' areas × flat-swatch density:
//   yarn_flat = Σ(piece_area_cm² × stitch_mod) × junction × metres_per_cm²(flat)
//   to_buy    = yarn_flat × (1 + spare)            (spare defaults to 0)
//
// • SWEATERS go through the construction-aware geometry engine (geometry.jsx,
//   estimateSweater) — it decomposes the garment into body / yoke / sleeves / hem /
//   cuffs / neckband, each with its own stitch pattern, and applies the per-
//   construction junction allowance internally.
// • Everything else (hat, sock, scarf, cowl, shawl) keeps the per-piece area path
//   in data.jsx#calcMeters (now on the same flat-swatch density curve).
// There is NO whole-garment regression anywhere and NO back-distribution of a total
// to pieces. The single user-facing figure is "yarn to buy".

// Resolve anchor-follow + cross-piece links into effective dimensions.
function makeEffDims(blocks) {
  const byId = Object.fromEntries(blocks.map(b => [b.id, b]))
  const anchorBlock = blocks.find(b => b.isAnchor)
  const resolveAnchor = (block) => {
    const d = { ...block.dims }
    const srcKey = block.followsAnchorKey || block.followsAnchorDim
    if (block.followsAnchorDim && anchorBlock && (srcKey in (anchorBlock.dims || {})))
      d[block.followsAnchorDim] = anchorBlock.dims[srcKey]
    return d
  }
  return blocks.map(b => {
    const d = resolveAnchor(b)
    Object.entries(b.links || {}).forEach(([dimKey, targetId]) => {
      const target = byId[targetId]
      if (target) { const td = resolveAnchor(target); if (dimKey in td) d[dimKey] = td[dimKey] }
    })
    return d
  })
}

// Effective stitch gauge. Priority: explicit override > custom-yarn gauge >
// the yarn weight's default.
function effectiveGauge(garmentYarn, customYarn, gaugeOverride) {
  if (gaugeOverride != null && gaugeOverride > 0) return gaugeOverride
  if (garmentYarn === "custom" && customYarn && customYarn.gauge) return customYarn.gauge
  return defaultGauge(garmentYarn)
}
function baselineGauge(garmentYarn, customYarn) {
  if (garmentYarn === "custom" && customYarn && customYarn.gauge) return customYarn.gauge
  return defaultGauge(garmentYarn)
}
// Effective ROW gauge. Priority: explicit override > custom-yarn rows > the honest
// default derived from the stitch gauge (visibly estimated, user-editable — A11).
function effectiveRows(gauge, customYarn, rowOverride, garmentYarn) {
  if (rowOverride != null && rowOverride > 0) return rowOverride
  if (garmentYarn === "custom" && customYarn && customYarn.rows) return customYarn.rows
  return Math.round(stdRowsFor(gauge))
}

// How many strands are held together as one fabric (held-double custom yarn = 2).
function strandsFor(garmentYarn, customYarn) {
  if (garmentYarn === "custom" && customYarn && customYarn.strands > 0) return customYarn.strands
  return 1
}

// The metres-per-100g basis for converting a SINGLE-STRAND metres figure to grams.
// For a held-double yarn this is the per-strand value (geometry's toBuyTotal is
// already single-strand metres, so we must NOT divide by the combined value).
function singleStrandBasis(yarnKey) {
  const y = YARN[yarnKey]
  if (!y) return null
  return (y.mPerStrand && y.mPerStrand > 0) ? y.mPerStrand : y.mPer100g
}

// Map a geometry piece's role onto the editable block that represents it, plus a
// swatch colour, so the breakdown rows stay clickable and visually grouped.
const SWEATER_PIECE_ROLE_TO_BLOCK = {
  "sweater-body": "sweater-body", "sweater-yoke": "sweater-body", "sweater-neckband": "sweater-body",
  "sweater-sleeve": "sweater-sleeve", "sweater-cuff": "sweater-sleeve",
  "sweater-hem": "sweater-hem",
}

// Build the geometry spec for a sweater from its blocks + app state. Returns null
// if there's no body block (e.g. a freeform build) so the caller can fall back.
function sweaterSpecFrom(blocks, effDims, gauge, rowOverride, construction, strands, mPerCm2) {
  const idx = role => blocks.findIndex(b => b.role === role)
  const bi = idx("sweater-body")
  if (bi < 0) return null
  const bodyBlock = blocks[bi], body = effDims[bi]
  const si = idx("sweater-sleeve")
  const sleeve = si >= 0 ? effDims[si] : null
  const sleeveBlock = si >= 0 ? blocks[si] : null
  const hi = idx("sweater-hem")
  const hemBlock = hi >= 0 ? blocks[hi] : null
  const hemDims = hi >= 0 ? effDims[hi] : null

  // per-piece stitch overrides drawn from the editable blocks' stitch patterns
  const stitchByRole = {}
  if (bodyBlock && bodyBlock.construction) {
    stitchByRole["sweater-body"] = bodyBlock.construction
    stitchByRole["sweater-yoke"] = bodyBlock.construction
  }
  if (sleeveBlock && sleeveBlock.construction) stitchByRole["sweater-sleeve"] = sleeveBlock.construction
  if (hemBlock && hemBlock.construction) stitchByRole["sweater-hem"] = hemBlock.construction

  return {
    chest: body.width,
    bodyLen: body.length,
    gauge,
    rows: (rowOverride != null && rowOverride > 0) ? rowOverride : null,
    construction,
    strands: strands || 1,
    buffer: 0,                       // base figure; the spare slider scales it in the UI
    sleeveLen: sleeve ? sleeve.depth : null,
    upperArm: sleeve ? sleeve.circumference : null,
    cuff: sleeve ? sleeve.cuff : null,
    hemDepth: hemDims ? hemDims.depth : null,
    neckType: (bodyBlock && bodyBlock.neckType) || "none",
    mPerCm2: (mPerCm2 && mPerCm2 > 0) ? mPerCm2 : null,   // precision mode: weighed-swatch override
    stitchByRole,
  }
}

// Full estimate. Returns a uniform shape for both the sweater (geometry) and the
// non-sweater (per-piece area) paths:
//   { effDims, effGauge, effRows, isSweater, strands,
//     blockMeters,   — flat metres contributed by each editable block (0 spare)
//     baseTotal,     — single-strand flat metres for the whole garment (0 spare)
//     pieces,        — breakdown rows [{ name, role, metres, stitch, color, blockId }]
//     byYarn }       — [{ key, metres, basis }] for the buy summary / grams
function computeEstimate({ blocks, lastPreset, garmentYarn, customYarn, gaugeOverride, rowOverride, construction }) {
  const effDims = makeEffDims(blocks)
  const effGauge = effectiveGauge(garmentYarn, customYarn, gaugeOverride)
  const effRows = effectiveRows(effGauge, customYarn, rowOverride, garmentYarn)
  const strands = strandsFor(garmentYarn, customYarn)

  // ── SWEATER: construction-aware geometry engine ─────────────────────────────
  const spec = lastPreset === "sweater"
    ? sweaterSpecFrom(blocks, effDims, effGauge, rowOverride, construction, strands,
        (garmentYarn === "custom" && customYarn && customYarn.mPerCm2 > 0) ? customYarn.mPerCm2
          : (YARN[garmentYarn] && YARN[garmentYarn].mPerCm2 > 0) ? YARN[garmentYarn].mPerCm2 : null)
    : null
  if (spec) {
    const result = estimateSweater(spec)
    // geometry reports per-STRAND metres; the headline (toBuyTotal) and every
    // breakdown row are reported in TOTAL single-strand metres so the rows always
    // sum to the headline (×2 for held-double, ×1 otherwise).
    const sMul = result.strands || 1
    // fold each geometry piece onto the editable block that represents it: the
    // cuff rolls into the sleeve, the yoke + neckband roll into the body. We show
    // ONE clickable row per editable piece (the ones with adjustable dimensions),
    // never the internal sub-pieces — they can't be sized on their own.
    const roleSum = {}     // block role → metres
    result.pieces.forEach(p => {
      const bRole = SWEATER_PIECE_ROLE_TO_BLOCK[p.role] || null
      if (bRole) roleSum[bRole] = (roleSum[bRole] || 0) + p.metres * sMul
    })
    // a block is "covered" by the geometry engine when its role maps to a geometry
    // piece. Any OTHER block — a manually added panel, an extra primitive — is not
    // in the geometry spec, so price it through the same area path the non-sweater
    // garments use and append it, so manual pieces are counted (breakdown + total).
    const isGeoBlock = b => b.role && roleSum[b.role] != null
    const blockMeters = blocks.map((b, i) =>
      isGeoBlock(b) ? roleSum[b.role] : calcMeters(b, effDims[i], garmentYarn, effGauge, effRows))
    const pieces = blocks.map((b, i) => ({
      name: b.label || PRIMITIVES[b.type].label, role: b.role, metres: blockMeters[i],
      stitch: PRIMITIVES[b.type].isFixed ? null : b.construction,
      color: PRIMITIVES[b.type].color, blockId: b.id,
      quantity: b.quantity || 1, yarnOverride: b.yarnOverride || null,
    }))
    const extraTotal = blocks.reduce((s, b, i) => s + (isGeoBlock(b) ? 0 : blockMeters[i]), 0)
    const baseTotal = result.toBuyTotal + extraTotal      // geometry + manual pieces, 0 spare
    // group by yarn: the geometry total sits on the garment yarn; extra pieces may
    // carry their own yarn override.
    const byYarnMap = {}
    if (result.toBuyTotal > 0) byYarnMap[garmentYarn] = result.toBuyTotal
    blocks.forEach((b, i) => {
      if (isGeoBlock(b)) return
      const y = b.yarnOverride || garmentYarn
      byYarnMap[y] = (byYarnMap[y] || 0) + blockMeters[i]
    })
    const byYarn = Object.entries(byYarnMap).filter(([, m]) => m > 0)
      .map(([key, metres]) => ({ key, metres, basis: singleStrandBasis(key) }))
    return { effDims, effGauge, effRows, isSweater: true, strands,
      blockMeters, baseTotal, pieces, byYarn }
  }

  // ── EVERYTHING ELSE: per-piece area path (flat-swatch density) ──────────────
  const blockMeters = blocks.map((b, i) => calcMeters(b, effDims[i], garmentYarn, effGauge, effRows))
  const baseTotal = blockMeters.reduce((s, m) => s + m, 0)
  const pieces = blocks.map((b, i) => ({
    name: b.label || PRIMITIVES[b.type].label, role: b.role, metres: blockMeters[i],
    stitch: PRIMITIVES[b.type].isFixed ? null : b.construction,
    color: PRIMITIVES[b.type].color, blockId: b.id,
    quantity: b.quantity || 1, yarnOverride: b.yarnOverride || null,
  }))
  const byYarnMap = {}
  blocks.forEach((b, i) => { const y = b.yarnOverride || garmentYarn; byYarnMap[y] = (byYarnMap[y] || 0) + blockMeters[i] })
  const byYarn = Object.entries(byYarnMap).filter(([, m]) => m > 0)
    .map(([key, metres]) => ({ key, metres, basis: singleStrandBasis(key) }))

  return { effDims, effGauge, effRows, isSweater: false, strands,
    blockMeters, baseTotal, pieces, byYarn }
}

// Sum a byYarn list into the headline buy figure at a given spare fraction and
// ball size (grams per ball, default 100):
//   { metres, grams, balls }  — balls are whole balls of `ballGrams`, rounded up per yarn.
function buyFromYarns(byYarn, spareFrac, ballGrams) {
  const bg = (ballGrams && ballGrams > 0) ? ballGrams : 100
  let metres = 0, grams = 0, balls = 0
  ;(byYarn || []).forEach(({ metres: m, basis }) => {
    const mm = m * (1 + (spareFrac || 0))
    metres += mm
    if (basis && basis > 0) {
      grams += mm / basis * 100
      const mPerBall = basis * bg / 100
      balls += Math.ceil(mm / mPerBall)
    }
  })
  return { metres: Math.round(metres), grams: Math.round(grams), balls, ballGrams: bg }
}

Object.assign(window, {
  makeEffDims, effectiveGauge, baselineGauge, effectiveRows, strandsFor, singleStrandBasis, buyFromYarns, computeEstimate,
})
