/**
 * Pact settings resolution — the Pact IS the settings store (Δ10).
 *
 * resolveSettings() reads the Pact clauses that carry a settingKey, finds the
 * 'current' ClauseVersion bearing a settingValue for each, and produces the
 * resolved CollectiveSettings consumed by triage.ts and state.ts. Pure
 * function — NOT a settings table: every threshold is a voted clause.
 *
 * Rules (BLUEPRINT-V2.md « Modèle de domaine » + Δ10):
 * - any absent key falls back to SETTINGS_DEFAULTS;
 * - invalid values (wrong type, negative number, unknown enum member) fall
 *   back to the default SILENTLY — a corrupted Pact never crashes the engine;
 * - any unresolved OPTIONAL protocolByRange key stays undefined: the caller
 *   falls back to the consent protocol;
 * - a missing 'protocols.consent' (corrupted bundle) still returns a full
 *   settings object with protocolByRange.consent === '' — hasConsentProtocol()
 *   lets the triage route 'advice' with the banner « Aucun protocole — crée-le
 *   ou décide sur avis », NEVER a crash.
 */

import type {
  Clause,
  ClauseVersion,
  CollectiveSettings,
  Id,
  InertiaPreset,
  Json,
} from '../types/domain'

/** Default triage settings — used for every key the Pact does not resolve. */
export const SETTINGS_DEFAULTS = {
  smallGroupMax: 5,
  collectiveMin: 50,
  consentMax: 7,
  objectionWindowHours: 48,
  adviceWindowHours: 72,
  framingDays: 14,
  concernEscalateRatio: 0.5,
  recurrenceThreshold: 3,
  reviewDelayDays: 90,
  requireEffects: 'binding',
} as const satisfies CollectiveSettings['triage']

const INERTIA_PRESETS: readonly InertiaPreset[] = ['low', 'standard', 'high', 'max']

/** Most recent wins when a clause carries several 'current' versions (data anomaly). */
function stampOf(version: ClauseVersion): string {
  return version.adoptedAt ?? version.updatedAt
}

/** The settingValue of the clause's 'current' version, if any. */
function currentValueOf(clause: Clause, versions: ClauseVersion[]): Json | undefined {
  let best: ClauseVersion | undefined
  for (const version of versions) {
    if (version.clauseId !== clause.id || version.archivedAt) continue
    if (version.status !== 'current' || version.settingValue === undefined) continue
    if (!best || stampOf(version) > stampOf(best)) best = version
  }
  return best?.settingValue
}

/** First non-archived clause holding this settingKey with a resolvable current value. */
function resolveRaw(key: string, clauses: Clause[], versions: ClauseVersion[]): Json | undefined {
  for (const clause of clauses) {
    if (clause.settingKey !== key || clause.archivedAt) continue
    const value = currentValueOf(clause, versions)
    if (value !== undefined) return value
  }
  return undefined
}

/** Wrong type or negative number ⇒ default, silently. */
function numberOrDefault(value: Json | undefined, fallback: number): number {
  return typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : fallback
}

/** Unknown enum member ⇒ default, silently. */
function requireEffectsOrDefault(value: Json | undefined): 'none' | 'structural' | 'binding' {
  return value === 'none' || value === 'structural' || value === 'binding'
    ? value
    : SETTINGS_DEFAULTS.requireEffects
}

/** Protocol ids must be non-empty strings — anything else counts as unresolved. */
function idOrUndefined(value: Json | undefined): Id | undefined {
  return typeof value === 'string' && value.trim().length > 0 ? value : undefined
}

/**
 * Resolve the collective's settings from its Pact clauses.
 *
 * Recognized setting keys:
 *   triage.smallGroupMax · triage.collectiveMin · triage.consentMax ·
 *   triage.objectionWindowHours · triage.adviceWindowHours ·
 *   triage.framingDays · triage.concernEscalateRatio ·
 *   triage.recurrenceThreshold · triage.reviewDelayDays ·
 *   triage.requireEffects · protocols.consent · protocols.nuanced ·
 *   protocols.large · protocols.parametric · protocols.election ·
 *   protocols.clauseByInertia.{low,standard,high,max}
 */
export function resolveSettings(clauses: Clause[], versions: ClauseVersion[]): CollectiveSettings {
  const raw = (key: string): Json | undefined => resolveRaw(key, clauses, versions)

  const triage: CollectiveSettings['triage'] = {
    smallGroupMax: numberOrDefault(raw('triage.smallGroupMax'), SETTINGS_DEFAULTS.smallGroupMax),
    collectiveMin: numberOrDefault(raw('triage.collectiveMin'), SETTINGS_DEFAULTS.collectiveMin),
    consentMax: numberOrDefault(raw('triage.consentMax'), SETTINGS_DEFAULTS.consentMax),
    objectionWindowHours: numberOrDefault(
      raw('triage.objectionWindowHours'),
      SETTINGS_DEFAULTS.objectionWindowHours,
    ),
    adviceWindowHours: numberOrDefault(
      raw('triage.adviceWindowHours'),
      SETTINGS_DEFAULTS.adviceWindowHours,
    ),
    framingDays: numberOrDefault(raw('triage.framingDays'), SETTINGS_DEFAULTS.framingDays),
    concernEscalateRatio: numberOrDefault(
      raw('triage.concernEscalateRatio'),
      SETTINGS_DEFAULTS.concernEscalateRatio,
    ),
    recurrenceThreshold: numberOrDefault(
      raw('triage.recurrenceThreshold'),
      SETTINGS_DEFAULTS.recurrenceThreshold,
    ),
    reviewDelayDays: numberOrDefault(raw('triage.reviewDelayDays'), SETTINGS_DEFAULTS.reviewDelayDays),
    requireEffects: requireEffectsOrDefault(raw('triage.requireEffects')),
  }

  // Consent is the MANDATORY invariant key. When unfindable (corrupted
  // bundle), we still return a complete object with consent === '' so the
  // caller can degrade gracefully (hasConsentProtocol) — never a crash.
  const protocolByRange: CollectiveSettings['protocolByRange'] = {
    consent: idOrUndefined(raw('protocols.consent')) ?? '',
  }

  const nuanced = idOrUndefined(raw('protocols.nuanced'))
  if (nuanced) protocolByRange.nuanced = nuanced
  const large = idOrUndefined(raw('protocols.large'))
  if (large) protocolByRange.large = large
  const parametric = idOrUndefined(raw('protocols.parametric'))
  if (parametric) protocolByRange.parametric = parametric
  const election = idOrUndefined(raw('protocols.election'))
  if (election) protocolByRange.election = election

  // The Ğ1 heritage lives here, intact: the map is exposed only when ALL four
  // presets resolve (the type is a complete Record — a partial map would lie).
  // Otherwise the caller falls back to consent, like any optional key.
  const byInertia: Partial<Record<InertiaPreset, Id>> = {}
  for (const preset of INERTIA_PRESETS) {
    const id = idOrUndefined(raw(`protocols.clauseByInertia.${preset}`))
    if (id) byInertia[preset] = id
  }
  if (INERTIA_PRESETS.every((preset) => byInertia[preset] !== undefined)) {
    protocolByRange.clauseByInertia = byInertia as Record<InertiaPreset, Id>
  }

  return { triage, protocolByRange }
}

/**
 * True when the collective has a resolvable consent protocol.
 * False ⇒ the triage routes 'advice' with the banner
 * « Aucun protocole — crée-le ou décide sur avis » — never a crash.
 */
export function hasConsentProtocol(settings: CollectiveSettings): boolean {
  return settings.protocolByRange.consent.trim().length > 0
}
