/**
 * Perimeter engine — who is concerned by THIS decision.
 *
 * Pure functions — no store, no I/O. The store assembles the inputs and
 * persists the result as Concern rows (origin 'computed').
 *
 * The computed union (BLUEPRINT-V2.md « Périmètres et inclusion ») :
 *   members of the scoped circles ∪ persons named by the author
 *   ∪ holders of active mandates whose domain intersects the scoped circles.
 * Every inclusion carries its human-readable French reason — auditable in
 * one tap, never « la machine t'a désigné ».
 *
 * MEMBERSHIP CHOICE (v2, documented): circle membership is NOMINATIVE per
 * circle (Circle.memberIds is the ONLY membership rule). Circle nesting
 * (parentCircleId, « couches d'oignon ») does NOT propagate membership:
 * a parent circle does NOT automatically include the members of its child
 * circles, nor the reverse. Nesting serves ONE gesture — widening the
 * perimeter by one notch (« élargir d'un cran ») — which explicitly adds
 * the parent circle to the scope; the computation never infers it.
 */

import type { Circle, Decision, Id, Mandate } from '~/types/domain'

export interface ConcernedEntry {
  personId: Id
  /** French reason shown on tap (becomes Concern.reason). */
  reason: string
}

/**
 * Flatten circle ids into the set of their DIRECT nominative members.
 *
 * NON-PROPAGATION (see module header): only Circle.memberIds of the listed
 * circles count. Members of child (or parent) circles are NOT included —
 * widening is a scope gesture, never an automatic inference.
 * Unknown circle ids are ignored (robustness over crash).
 */
export function expandCircleMembers(circleIds: Id[], circles: Circle[]): Set<Id> {
  const members = new Set<Id>()
  for (const circleId of circleIds) {
    const circle = circles.find(c => c.id === circleId)
    if (!circle) continue
    for (const personId of circle.memberIds) members.add(personId)
  }
  return members
}

/**
 * Compute the concerned persons of a decision scope, each with its reason.
 *
 * Deduplication: first reason wins, with source priority
 * circle membership > named by author > mandate holder.
 * The author is EXCLUDED from the list (they decide, they are not
 * « concerned » by their own perimeter).
 *
 * Mandates count only when status 'active' and when their domain.circleIds
 * INTERSECTS the scoped circles (any shared circle is enough — a mandate
 * holder is concerned as soon as the decision touches their domain).
 */
export function computeConcerned(
  scope: Decision['scope'],
  circles: Circle[],
  mandates: Mandate[],
  authorId: Id,
): ConcernedEntry[] {
  // Map preserves insertion order; first reason wins (priority by pass order).
  const concerned = new Map<Id, string>()

  // 1. Members of the scoped circles (highest priority reason).
  for (const circleId of scope.circleIds) {
    const circle = circles.find(c => c.id === circleId)
    if (!circle) continue
    for (const personId of circle.memberIds) {
      if (personId === authorId) continue
      if (!concerned.has(personId)) {
        concerned.set(personId, `membre du cercle ${circle.name}`)
      }
    }
  }

  // 2. Persons named by the author.
  for (const personId of scope.personIds) {
    if (personId === authorId) continue
    if (!concerned.has(personId)) {
      concerned.set(personId, 'nommé·e par l\'auteur')
    }
  }

  // 3. Holders of active mandates whose domain intersects the scoped circles.
  const scopedCircleIds = new Set(scope.circleIds)
  for (const mandate of mandates) {
    if (mandate.status !== 'active') continue
    if (!mandate.domain.circleIds.some(id => scopedCircleIds.has(id))) continue
    if (mandate.holderId === authorId) continue
    if (!concerned.has(mandate.holderId)) {
      concerned.set(mandate.holderId, `titulaire du mandat ${mandate.title}`)
    }
  }

  return [...concerned].map(([personId, reason]) => ({ personId, reason }))
}
