/**
 * The ONE state machine of the decision (Δ29).
 *
 * Every mutation goes through canTransition(decision, to, ctx): the doctrinal
 * guards (subsidiarity, instruction, consent) are tested code, not intentions.
 * The context is assembled by the store; the function stays pure and testable.
 *
 * Guards, in order (BLUEPRINT-V2.md « Cycle de vie » + Δ13, Δ17, Δ27, Δ29):
 *   a) unknown transition;
 *   b) boundary — windowSuspendedAt suspends any exit from objection/advice;
 *   c) influx — concernEscalateRatio reached ⇒ widen or scopeKeptNote;
 *   d) matter — requireEffects at collective session opening;
 *   e) resources — « Ce que ça engage » at window/session opening;
 *   f) assent — non-easy objection windows adopt only on an explicit third-party
 *      agreement, never by pure silence;
 *   g) open objection — no adoption over an open objection;
 *   h) dossier — framing→closed only when every element child is terminal;
 *   i) crystallization — parametric sessions close by a dated human gesture,
 *      the engine NEVER crystallizes.
 *
 * Every refusal reason is one French sentence whose subject is the collective
 * or the person — never the engine.
 */

import type {
  Assent,
  CollectiveSettings,
  Concern,
  Decision,
  DecisionStatus,
  ISODate,
  Objection,
  VoteSession,
} from '../types/domain'

/** Allowed transitions — statuses absent from the table are terminal. */
export const TRANSITIONS: Readonly<Partial<Record<DecisionStatus, readonly DecisionStatus[]>>> = {
  draft: ['advice', 'objection', 'framing', 'voting', 'adopted', 'transmitted', 'rejected'],
  advice: ['adopted', 'voting'],
  objection: ['adopted', 'framing', 'voting'],
  framing: ['voting', 'closed'],
  voting: ['adopted', 'rejected', 'framing'],
  // voting→framing: ONLY the « Reformuler » choice of the crystallization gesture —
  // guarded below to a frozen, non-crystallized parametric session (Δ3).
  adopted: ['revoked', 'closed'],
}

/** Terminal statuses of an element child — condition of the dossier closure. */
export const TERMINAL_STATUSES: readonly DecisionStatus[] = [
  'adopted',
  'rejected',
  'revoked',
  'closed',
  'transmitted',
]

/** Window states — a boundary objection suspends any exit from these. */
const WINDOW_STATUSES: readonly DecisionStatus[] = ['objection', 'advice']

/** Sources whose closing is subject to the influx guard. */
const INFLUX_SOURCES: readonly DecisionStatus[] = ['advice', 'objection', 'voting']

/** Adoption/closure targets watched by the influx guard. */
const INFLUX_TARGETS: readonly DecisionStatus[] = ['adopted', 'rejected', 'closed']

const REASONS = {
  unknown: 'La décision ne peut pas prendre ce chemin depuis son état actuel.',
  boundary:
    'La frontière est contestée — le compte à rebours reste suspendu jusqu’à l’intégration ou une réponse motivée.',
  influx: 'Le périmètre déborde — élargis d’un cran ou motive publiquement son maintien.',
  matterMissing: 'Le collectif s’instruit avant de voter — formule au moins un effet recherché.',
  matterTarget:
    'Une décision structurante se mesure — donne une cible à au moins un effet recherché.',
  resources:
    'Le collectif doit savoir ce que ça engage — écris la note de ressources avant d’ouvrir.',
  assent: 'Il manque un accord explicite — la fenêtre se prolonge.',
  openObjection: 'Une objection reste ouverte — le collectif l’entend avant d’adopter.',
  dossierEmpty: 'Le dossier n’a pas d’éléments — découpe-le avant de le clore.',
  dossierPending:
    'Des éléments du dossier sont encore en cours — le dossier se clôt quand tous ont abouti.',
  crystallization: 'Les votes sont figés — la cristallisation attend son geste.',
  reformulate:
    'Reformuler n’est possible que sur un réglage collectif figé, avant sa cristallisation.',
} as const

export interface TransitionContext {
  concerns: Concern[]
  settings: CollectiveSettings
  session?: VoteSession
  children?: Decision[]
  assents?: Assent[]
  objections?: Objection[]
  now: ISODate
}

export type TransitionResult = { ok: true } | { ok: false; reason: string }

export type WindowOutcome = 'adopt' | 'extend' | 'wait'

function refuse(reason: string): TransitionResult {
  return { ok: false, reason }
}

/** ≥1 non-archived Assent on this decision from someone other than the author. */
function hasThirdPartyAssent(decision: Decision, assents: Assent[] | undefined): boolean {
  return (assents ?? []).some(
    (assent) =>
      assent.decisionId === decision.id &&
      !assent.archivedAt &&
      assent.personId !== decision.authorId,
  )
}

/**
 * Can this decision move to `to`? Pure — the store assembles ctx.
 * Refusals carry one French sentence (subject: the collective or the person).
 */
export function canTransition(
  decision: Decision,
  to: DecisionStatus,
  ctx: TransitionContext,
): TransitionResult {
  const from = decision.status

  // ── (a) Unknown transition ─────────────────────────────────────────────
  // A window state may explicitly return to itself (re-arming after a
  // resolved boundary objection, extension of a window) — nothing else loops.
  const isWindowSelfReturn = WINDOW_STATUSES.includes(from) && to === from
  if (!isWindowSelfReturn && !(TRANSITIONS[from] ?? []).includes(to)) {
    return refuse(REASONS.unknown)
  }

  // ── (b) Boundary — the contestation of frontiers precedes substance ────
  if (WINDOW_STATUSES.includes(from) && decision.windowSuspendedAt && to !== from) {
    return refuse(REASONS.boundary)
  }

  // ── (c) Influx — the scale-up is DECIDED, never evaporated (Δ17) ───────
  if (INFLUX_SOURCES.includes(from) && INFLUX_TARGETS.includes(to)) {
    const concerns = ctx.concerns.filter(
      (concern) => concern.decisionId === decision.id && !concern.archivedAt,
    )
    const computed = concerns.filter((concern) => concern.origin === 'computed').length
    const declared = concerns.filter((concern) => concern.origin === 'declared').length
    const ratioReached =
      computed > 0 && declared >= ctx.settings.triage.concernEscalateRatio * computed
    if (ratioReached && !decision.scopeKeptNote) {
      return refuse(REASONS.influx)
    }
  }

  const opensCollectiveSession = (from === 'draft' || from === 'framing') && to === 'voting'

  // ── (d) Matter — no engaging collective vote without instruction ───────
  if (opensCollectiveSession && decision.route === 'collective') {
    const mode = ctx.settings.triage.requireEffects
    const underGuard =
      mode === 'binding'
        ? decision.weight === 'binding' || decision.weight === 'structural'
        : mode === 'structural' && decision.weight === 'structural'
    if (underGuard) {
      const effects = decision.brief?.effects ?? []
      if (effects.length === 0) {
        return refuse(REASONS.matterMissing)
      }
      const hasMeasurableEffect = effects.some(
        (effect) => effect.target !== undefined && effect.target.trim().length > 0,
      )
      if (decision.weight === 'structural' && !hasMeasurableEffect) {
        return refuse(REASONS.matterTarget)
      }
    }
  }

  // ── (e) Resources — « Ce que ça engage » lives at the opening ──────────
  const opensWindowOrSession =
    opensCollectiveSession || (from === 'draft' && (to === 'objection' || to === 'advice'))
  if (
    opensWindowOrSession &&
    decision.weight !== 'light' &&
    decision.route !== 'solo' &&
    decision.route !== 'record'
  ) {
    const note = decision.resources?.note ?? ''
    if (note.trim().length === 0) {
      return refuse(REASONS.resources)
    }
  }

  // ── (f) Assent — outside easy, agreement is a gesture, not silence (Δ27)
  if (from === 'objection' && to === 'adopted' && decision.reversibility !== 'easy') {
    if (!hasThirdPartyAssent(decision, ctx.assents)) {
      return refuse(REASONS.assent)
    }
  }

  // ── (g) Open objection — never adopted over someone's maintained voice ─
  if (from === 'objection' && to === 'adopted') {
    const hasOpenObjection = (ctx.objections ?? []).some(
      (objection) =>
        objection.decisionId === decision.id &&
        !objection.archivedAt &&
        objection.status === 'open',
    )
    if (hasOpenObjection) {
      return refuse(REASONS.openObjection)
    }
  }

  // ── (h) Dossier — the closure is a steward gesture on a complete map (Δ13)
  if (from === 'framing' && to === 'closed') {
    const elements = (ctx.children ?? []).filter(
      (child) =>
        child.chainKind === 'element' &&
        child.parentDecisionId === decision.id &&
        !child.archivedAt,
    )
    if (elements.length === 0) {
      return refuse(REASONS.dossierEmpty)
    }
    if (elements.some((child) => !TERMINAL_STATUSES.includes(child.status))) {
      return refuse(REASONS.dossierPending)
    }
  }

  // ── (i) Crystallization — the engine NEVER crystallizes ────────────────
  // A parametric session is recognized by decision.paramSpec, or by the
  // 'frozen' status (only parametric sessions ever freeze).
  if (from === 'voting' && (to === 'adopted' || to === 'rejected') && ctx.session) {
    const isParametric = decision.paramSpec !== undefined || ctx.session.status === 'frozen'
    const crystallized = ctx.session.status === 'closed' && !!ctx.session.crystallizedById
    if (isParametric && !crystallized) {
      return refuse(REASONS.crystallization)
    }
  }

  // voting→framing is EXCLUSIVELY the « Reformuler » branch of the crystallization
  // gesture (Δ3): a frozen, not-yet-crystallized parametric session may go back to
  // formulation instead of being crystallized. Any other voting→framing is refused.
  if (from === 'voting' && to === 'framing') {
    const reformulable =
      ctx.session?.status === 'frozen' && !ctx.session.crystallizedById
    if (!reformulable) {
      return refuse(REASONS.reformulate)
    }
  }

  return { ok: true }
}

/**
 * Outcome of an objection window at its deadline, WITHOUT open objection
 * (the caller handles open objections — escalation or suspension):
 * - suspended boundary ⇒ 'wait' (the countdown is not running);
 * - easy ⇒ 'adopt' (silence counts as agreement — only there);
 * - otherwise ⇒ 'adopt' on a third-party Assent, else 'extend' by one notch
 *   (+objectionWindowHours, Fil reminder) — never adoption by pure silence.
 */
export function windowOutcome(decision: Decision, ctx: TransitionContext): WindowOutcome {
  if (decision.windowSuspendedAt) return 'wait'
  if (decision.reversibility === 'easy') return 'adopt'
  return hasThirdPartyAssent(decision, ctx.assents) ? 'adopt' : 'extend'
}
