// ─────────────────────────────────────────────────────────────
// libreDecision v2 — domain core (BLUEPRINT-V2.md, docs/dev/)
// Local-first, sync-ready. English identifiers, French UI via app/lexicon.ts.
// INVARIANT 1: THE DECISION IS THE PIVOT — mandates, clauses, revocations,
//   ratifications and dossier elements are chained specializations of it.
// INVARIANT 2: that unification is an architectural fact, NEVER UI vocabulary.
// INVARIANT 3: no aggregate/score on a person, anywhere — we evaluate things.
// INVARIANT 4: no computation ever produces adopted content without a dated
//   human gesture (parametric crystallization, election tie-break, dossier close).
// ─────────────────────────────────────────────────────────────

export type Id = string // UUIDv7 — time-sortable, sync-ready
export type ISODate = string // ISO 8601 UTC
export type Json = string | number | boolean | null | Json[] | { [k: string]: Json }

// Every entity: collective scoping + sync clock + soft delete.
// Sync-ready rules: never a physical delete (archivedAt);
// Vote/Advice/Objection/Assent append-only (trivial merge); the rest LWW by updatedAt.
export interface Entity {
  id: Id
  collectiveId: Id
  createdAt: ISODate
  updatedAt: ISODate
  archivedAt?: ISODate
}

// ── The tenant ───────────────────────────────────────────────
export type CollectiveTemplate =
  | 'blank' // « Page blanche — observatoire d'abord » : one-clause Pact + A1, Consent protocol ONLY
  | 'informal'
  | 'association'
  | 'cooperative'
  | 'commune'
  | 'free-currency' // Ğ1 heritage — ONLY template where protocolByRange.large = inertial binary
  | 'symmetric' // « Institution symétrique » — the founding militant gesture (MNP)

export interface Collective {
  id: Id
  slug: string
  name: string
  color: string // hex accent
  icon: string // i-lucide-*
  template: CollectiveTemplate
  isTransparent: boolean
  pactDocId: Id // TextDoc role='pact' — settings AS a voted document
  rootCircleId: Id
  lineage?: {
    // seeding trail: set automatically when importing a bundle from another collective
    sourceSlug: string
    exportedAt: ISODate
    sha256: string
  } // NO parentCollectiveId — federation is never a table
  createdAt: ISODate
  updatedAt: ISODate
}

// ── The people ───────────────────────────────────────────────
export interface Person extends Entity {
  displayName: string
  isMe: boolean // local profile (v2 single-seat; « mode atelier » for in-person capture)
  attributes?: Record<string, number> // SELF-declared attributes (free keys: 'heures/mois'…),
  // edited in /donnees — feed ONLY the « Pour moi » card;
  // never shown to others, never aggregated (invariant 3)
  duniterAddress?: string // SS58 — reserved for Duniter auth (out of v2 scope)
  wotStatus?: 'member' | 'smith'
}

export type CircleKind = 'place' | 'theme' | 'team'

export interface Circle extends Entity {
  name: string
  purpose: string
  kind?: CircleKind // lieu / thème / équipe — iconography, grouping, suggestion order
  // ONLY; never a right; non-blocking hint when a 'team' exceeds 12
  memberIds: Id[] // v2: nominative list = the ONLY membership rule (auditable)
  parentCircleId?: Id // nesting (couches d'oignon) — root = rootCircle
  domains: string[] // tags — SUGGESTION/discovery only, never a voting right
}
// Corpus of a decision = COMPUTABLE union: members of scoped circles
// ∪ named persons ∪ holders of mandates whose domain intersects.
// Every inclusion keeps its reason (Concern.reason) — auditable in one tap.

// ── THE DECISION (pivot) ─────────────────────────────────────
export type Reversibility = 'easy' | 'costly' | 'irreversible'
export type Weight = 'light' | 'binding' | 'structural'

export type DecisionRoute =
  | 'solo' // « Je décide » (optional trace outside mandate)
  | 'mandate' // « Je décide, sous mandat » (mandatory trace + objection window)
  | 'transmit' // « Je transmets » — someone else's mandate covers
  | 'advice' // « J'écoute, puis je décide »
  | 'collective' // « Nous décidons » (protocol modality)
  | 'record' // « Déjà tranché — je le consigne » : the observatory in one gesture, adopted immediately

export type DecisionStatus =
  | 'draft'
  | 'advice'
  | 'objection'
  | 'framing'
  | 'voting'
  | 'adopted'
  | 'rejected'
  | 'revoked'
  | 'transmitted'
  | 'closed'

export type TriageRule = 'R-U' | 'R0a' | 'R0b' | 'R0c' | 'R2' | 'R3' | 'R4' | 'R5' | 'R6'

// The matter — « il n'est pas consulté, il est instruit »
export interface Effect {
  label: string // sought effect, one line
  target?: string // optional measurable target (« ≤ 400 € », « +5 membres »)
  measured?: { note: string; at: ISODate; byId: Id } // observed — filled by measurerIds at review time
}
export interface Brief {
  context?: string
  sources?: { label: string; url: string }[]
  symptomsVsCauses?: string // doctrinal qualification of the subject
  effects: Effect[] // GUARD (state.ts, opening of a COLLECTIVE session only):
  // required when weight ∈ Pact 'triage.requireEffects' ('binding' covers binding+structural);
  // structural under guard ⇒ ≥1 effect with target. NEVER for weight 'light',
  // NEVER in the tunnel, NEVER for solo/record.
}

// The linked resource decision — « chaque décision implique une allocation »
export interface Resources {
  note: string // « Ce que ça engage » in one sentence
  amount?: number
  unit?: 'heures' | '€' | 'DU' | string // oil/water: units NEVER converted nor summed together
}

// The collective tuning — « raisonner curseur / table de mixage »
export type ParamKind = 'slider' | 'share' // 'curve' reserved for v3 (full SejeteralO)
export interface ParamDef {
  key: string
  label: string // MANDATORY business label — never raw a, b, c
  kind: ParamKind
  min: number
  max: number
  step: number
  unit?: string
  baseline?: number // status quo — hollow marker always displayed
  derived?: boolean // resolved by the constraint, never voted — labelled « calculé »
}
export interface ParamSpec {
  params: ParamDef[] // ≤ 7 (SejeteralO lesson: small manipulable space)
  constraint: 'none' | 'sum100' // sum100: 'share' params total 100; EXACTLY ONE 'derived'
  // share required (validated at spec creation) — the absorption variable, resolved
  // linearly (100 − Σ others)
  impactAttrKey?: string // key of Person.attributes → « Pour moi » card ('linear-share')
}

export type ChainKind = 'ratification' | 'revision' | 'revocation' | 'element'

export interface Decision extends Entity {
  authorId: Id
  title: string // THE capture sentence — only mandatory field
  body?: string // progressive disclosure
  brief?: Brief // matter — see opening guard above
  baselineNote?: string // « Aujourd'hui : … » — suggested on advice/collective routes, never solo
  resources?: Resources // SUGGESTED-unfolded in the tunnel (never blocking); REQUIRED by the
  // window/session opening guard (state.ts) when weight != 'light' outside solo
  tags: string[] // discovery + recurrence — NEVER a corpus

  // — triage verdict (pre-filled by the engine, adjustable) —
  reversibility: Reversibility
  weight: Weight
  urgent: boolean
  scope: {
    selfOnly: boolean
    circleIds: Id[] // floor: widening free, NEVER narrowing below the computation
    personIds: Id[]
  }
  route: DecisionRoute
  triageRule: TriageRule // relegated to the « pourquoi ? » disclosure, PV, journal — never on the card
  routeOverridden: boolean
  overrideNote?: string // MANDATORY when lightening (derogation asymmetry)
  scopeKeptNote?: string // influx reached + perimeter kept ⇒ mandatory PUBLIC motivated note

  // — chained specializations (the generative folding) —
  underMandateId?: Id // decided UNDER this mandate → the mandate's « feux de la rampe »
  createsMandate?: MandateDraft
  amendsClauseId?: Id
  protocolId?: Id // resolved by the Pact, adjustable by weighting up
  paramSpec?: ParamSpec // when parametric modality (« Réglage collectif »)
  parentDecisionId?: Id
  chainKind?: ChainKind
  // 'element': micro-decision of a split dossier (« décision incrémentielle »).
  // Parent of elements: framing→closed transition guarded by
  // « all element children terminal » — steward GESTURE (Fil card), never automatic;
  // the closing cartography becomes the dossier's PV.
  decidedHow?: string // route 'record': « comment ça s'est décidé », one sentence

  // — time & windows —
  status: DecisionStatus
  windowEndsAt?: ISODate
  windowSuspendedAt?: ISODate // boundary objection → countdown SUSPENDED (visible)
  decidedAt?: ISODate
  review?: Review // mandatory when irreversible || structural
  sunsetAt?: ISODate
  stewardIds: Id[] // stewards — REALIZE (and crystallize parametric, close dossiers)
  measurerIds: Id[] // measurers — OBSERVE (fill Effect.measured): the doctrinal pair

  // — proof & visibility —
  visibility: 'private' | 'scope' | 'collective' // anti-surveillance: trace owned outside mandate
  engraving?: Engraving
}

export interface Review {
  dueAt: ISODate
  verdict?: 'confirmed' | 'revise' | 'revoke' // UI: « Ça tient / À revoir / À révoquer »
  note?: string
  decidedAt?: ISODate
}

export interface Engraving {
  sha256: string
  engravedAt: ISODate
  proofLevel: 'local' // v2: ONLY 'local' — « empreinte locale — démo »
  ipfsCid?: string // reserved v3 — never displayed until real
  chainRef?: string
}

// ── Inclusion & windows ──────────────────────────────────────
export interface Concern extends Entity {
  decisionId: Id
  personId: Id
  origin: 'computed' | 'declared' // UI: « concerné·e en premier lieu » / « en second lieu »
  reason: string // « membre du cercle Forgerons » — shown on tap
  declaredNote?: string
  beforeSnapshot: boolean // before the list is arrested ⇒ vote; after ⇒ consultative voice
  priority?: 0 | 1 | 2 | 3 // « pondère tes enjeux » — per element of a dossier;
  // feeds the closing cartography, never the voting right
}

export interface Assent extends Entity {
  // « Ça me va » STORED — the explicit agreement of objection windows
  decisionId: Id
  personId: Id
  recordedById?: Id // mode atelier
}
// OBJECTION WINDOW RULE (state.ts): at deadline with no open objection —
// easy ⇒ adopted (silence counts as agreement); costly/irreversible ⇒ adopted ONLY if
// ≥1 Assent from a concerned person ≠ author, otherwise EXTENSION by one notch
// (+objectionWindowHours, Fil reminder) — never adoption by pure silence outside easy.

export interface Objection extends Entity {
  decisionId: Id
  personId: Id
  kind: 'content' | 'boundary' // boundary SUSPENDS the window — the frontier precedes substance
  argument: string
  status: 'open' | 'withdrawn' | 'integrated' | 'escalated'
  resolutionNote?: string
  recordedById?: Id // mode atelier: « saisi par X pour Y — atelier du <date> »
}

export interface Advice extends Entity {
  decisionId: Id
  personId: Id
  position: 'favorable' | 'reserved' | 'unfavorable'
  note?: string
  recordedById?: Id // mode atelier
}

// ── The power ────────────────────────────────────────────────
export type NominationMethod =
  | 'ratified-self'
  | 'election-no-candidate'
  | 'nuanced-vote'
  | 'consent'
  | 'draw'
  | 'rotation'

export interface Mandate extends Entity {
  title: string
  holderId: Id
  originDecisionId: Id // the mandate IS born of a decision — clickable
  domain: { circleIds: Id[]; tags: string[] }
  startsAt: ISODate
  endsAt: ISODate // always bounded
  electorCircleId: Id // election corpus = revocation corpus
  nominationMethod: NominationMethod
  reports: MandateReport[]
  status: 'proposed' | 'active' | 'expired' | 'revoked'
  // NO aggregate/gauge: the sheet shows counted facts (« Exercice du mandat »),
  // never synthesized; the public stream = « Feux de la rampe »
}
export interface MandateReport {
  dueAt: ISODate
  deliveredAt?: ISODate
  content?: string
}
export interface MandateDraft {
  title: string
  domainCircleIds: Id[]
  domainTags: string[]
  durationDays: number
  reportEveryDays?: number
}

// ── The texts ────────────────────────────────────────────────
export interface TextDoc extends Entity {
  slug: string
  title: string
  role: 'pact' | 'reference'
  description: string // seeded Pact: « Notre contrat social — sacralisé, jamais immuable »
  provenance?: Provenance
}
export interface Provenance {
  sources: { title: string; url: string; date?: string; version?: string }[]
  voteRecord?: {
    url: string
    modeParams: string
    period: string
    result: {
      for: number
      against: number
      invalid?: number
      wotSize: number
      thresholdRequired: number
      status: string
    }
  }
  contributors?: string[]
  notes?: string // typos of voted texts: FLAGGED, never fixed without a vote
}

export type InertiaPreset = 'low' | 'standard' | 'high' | 'max'
// REAL wiring: low={M:50,G:0.1} standard={M:50,G:0.2} high={M:60,G:0.4} max={M:66,G:0.6} (B=0.1, C=0)

export interface Clause extends Entity {
  docId: Id
  section: string
  position: number
  code: string
  title: string
  inertia: InertiaPreset
  currentVersionId?: Id
  settingKey?: string // Pact clauses: e.g. 'triage.smallGroupMax'
}

export interface ClauseVersion extends Entity {
  clauseId: Id
  decisionId: Id // EVERY version is born of a decision (founding seeded ones included)
  versionLabel: string
  content: string
  settingValue?: Json
  status: 'current' | 'proposed' | 'superseded' | 'rejected'
  adoptedAt?: ISODate
}

// ── Modalities & vote ────────────────────────────────────────
export type VoteMethod = 'consent' | 'binary' | 'nuanced' | 'parametric' | 'election'
// UI: consentement · pour/contre (last-resort tool) · nuancé ·
//     « Réglage collectif » (au curseur) · élection

export interface Protocol extends Entity {
  name: string
  method: VoteMethod
  description: string
  durationDays: number
  ballot: 'open' | 'secret' // decided by the Pact — default 'open' (feux de la rampe).
  // v2 single-seat: « secret d'affichage — démo » (aggregate tallies and anonymized
  // comments only); real crypto arrives with sync (v3)
  formula: FormulaParams
  modeParams: string // DERIVED display DSL: 'D30M50B.1G.2S.1' — never hand-edited
  pactClauseId?: Id // referenced by the Pact ⇒ changing it = amending the Pact
}
export interface FormulaParams {
  majorityPct: number // M
  baseExponent: number // B (default 0.1)
  gradientExponent: number // G — the inertia
  constantBase: number // C (default 0)
  smithExponent?: number
  techcommExponent?: number
  nuancedMinParticipants?: number
  nuancedThresholdPct?: number
  parametricMinParticipants?: number // collective-tuning quorum — not reached ⇒ rejected at the gesture
  electionMinParticipants?: number // election quorum — not reached ⇒ rejected
  tieBreak?: 'runoff' | 'draw' // election tie-break DECIDED IN ADVANCE by the Pact
  // (default 'runoff': chained runoff session among tied; 'draw': draw among tied —
  // never chosen by the engine)
}

export interface VoteSession extends Entity {
  decisionId: Id
  protocolId: Id
  corpusPersonIds: Id[] // SNAPSHOT at opening — UI: « liste arrêtée le … »
  corpusSize: number // W — never 0 again (letter reserved to the Atelier)
  opensAt: ISODate
  closesAt: ISODate
  status: 'open' | 'frozen' | 'closed'
  // 'frozen': PARAMETRIC only — at closesAt votes are frozen, the frozen median is shown,
  // and the session WAITS for the steward's human gesture (Fil reminder if late).
  crystallizedById?: Id // the « Cristalliser » gesture: who, when — dated and signed
  crystallizedAt?: ISODate
  outcome?: 'adopted' | 'rejected' | 'tie'
  // 'tie': tied election — the tool NEVER breaks a tie: chained runoff session
  // (restricted to tied) or draw IF the protocol planned it (tieBreak).
  // Consent/nuanced/binary: open→closed automatic at closesAt (the engine only observes a
  // threshold over human-written content). Parametric: open→frozen→closed (gesture).
  // INVARIANT: tallies, histograms AND medians ALWAYS recomputed from Vote[] — never denormalized.
}

export type NuancedValue = 0 | 1 | 2 | 3 | 4 | 5

export interface Vote extends Entity {
  sessionId: Id
  voterId: Id
  value?: 'for' | 'against' | NuancedValue
  // nuancé UI: 0 Pas du tout · 1 Plutôt non · 2 Réservé · 3 Plutôt oui · 4 Oui · 5 Tout à fait
  values?: number[] // parametric — order of paramSpec.params EXCLUDING derived
  choicePersonId?: Id // election — designation; a vote without choicePersonId = BLANK
  // (counts for participation, not designation)
  // INVARIANT: EXACTLY ONE of value / values / choicePersonId per method (validated by the
  // store) — except an election blank (none of the three, method 'election' only)
  comment?: string // MANDATORY when negative (against, 0, 1)
  supersedesVoteId?: Id // audited re-vote — PRIVACY: the chain is NEVER displayed publicly
  // under a voter's name; visible to its AUTHOR only; even with ballot 'open', only the
  // LAST active vote appears under a name, aggregate tallies recomputed
  // (« pardonner les positions »)
  recordedById?: Id // mode atelier
}

// ── Resolved Pact settings (pure function — NOT a settings table) ──
export interface CollectiveSettings {
  // resolveSettings(pactClauses, versions)
  triage: {
    smallGroupMax: number // default 5
    collectiveMin: number // default 50
    consentMax: number // default 7
    objectionWindowHours: number // default 48
    adviceWindowHours: number // default 72
    framingDays: number // default 14 — « s'instruire et formuler des contre-propositions »
    concernEscalateRatio: number // default 0.5 — reached ⇒ MANDATORY handling (state.ts guard)
    recurrenceThreshold: number // default 3
    reviewDelayDays: number // default 90
    requireEffects: 'none' | 'structural' | 'binding'
    // key 'triage.requireEffects' — governs the matter guard of COLLECTIVE sessions:
    // 'binding' = binding AND structural (default of structured templates);
    // 'structural' = structural only; 'none' seeded in blank and informal.
    // Under guard, structural ⇒ ≥1 effect with measurable target.
  }
  protocolByRange: {
    consent: Id // MANDATORY — invariant: every collective has a Consent protocol
    // (every template seeds it, import validates it)
    nuanced?: Id
    large?: Id // ex-inertialBinary: the name no longer presumes the method.
    // Standard templates → nuanced; free-currency → inertial binary
    parametric?: Id // « Réglage collectif »
    election?: Id
    clauseByInertia?: Record<InertiaPreset, Id> // the Ğ1 heritage lives here, intact
  }
  // SPECIFIED FALLBACK (resolveSettings): any unresolved optional key falls back to consent;
  // consent unfindable (corrupted bundle) ⇒ triage routes 'advice' with the banner
  // « Aucun protocole — crée-le ou décide sur avis » — NEVER a crash.
}

// ── Triage engine contracts ──────────────────────────────────
export interface TriageInput {
  title: string
  tags: string[]
  scope: Decision['scope']
  reversibility: Reversibility
  weight: Weight
  urgent: boolean
  amendsClauseId?: Id
}
export interface TriageContext {
  // assembled by the store, consumed pure — testable without I/O
  myActiveMandates: Mandate[]
  otherActiveMandates: Mandate[]
  matchingClauses: Clause[] // Q0 index « déjà décidé ? » — the SAME index as Cmd+K
  similarRecentCount: number // adopted decisions sharing ≥2 tags over 90 d
  similarRecordedCount: number // 'record' entries sharing ≥2 tags — maturation
  computedConcernedIds: Id[]
}
export interface Verdict {
  route: DecisionRoute
  rule: TriageRule
  explanation: string // ONE French sentence — displayed ALONE, without rule code
  protocolId?: Id
  windowHours?: number
  framingDays?: number
  reviewRequired: boolean
  engravingSuggested: boolean
  conservatoryChain?: boolean
  parametricHint?: boolean // number/%/amount detected ⇒ « C'est un réglage » put forward
  alternatives: { route: DecisionRoute; label: string; cost: string }[]
  // always present: « C'est un réglage — décidez au curseur » (parametric);
  // « Déjà tranché — je le consigne » (record); binary with its cost (« deux camps, un perdant »)
  suggestion?: { kind: 'claim-mandate' | 'create-rule' | 'protocolize'; prefill: Json }
}

// ── Local persistence & future sync ──────────────────────────
export interface Bundle {
  // export/import AND seeds — the SAME code path
  schemaVersion: 2
  exportedAt: ISODate
  collective: Collective
  people: Person[]
  circles: Circle[]
  decisions: Decision[]
  concerns: Concern[]
  objections: Objection[]
  advices: Advice[]
  assents: Assent[]
  mandates: Mandate[]
  docs: TextDoc[]
  clauses: Clause[]
  versions: ClauseVersion[]
  protocols: Protocol[]
  sessions: VoteSession[]
  votes: Vote[]
}
// data/persistence.ts: IndexedDB (idb-keyval), key `ld2:<collectiveId>`, 500 ms debounce.
// importBundle() validates protocolByRange.consent + sets Collective.lineage for foreign bundles.
// Sync-ready: UUIDv7, updatedAt (LWW), archivedAt, Vote/Advice/Objection/Assent append-only.
// Pinia store contract unchanged — the future FastAPI adapter changes NO screen.
// Contract notes: ballot 'secret' must become cryptographic server-side (v3);
// the privacy of supersedesVoteId chains (author-visible only) is a contract invariant.
