/**
 * SESSION_ATTENDANCE resolution engine — the pure core of the module.
 *
 * A registrant's AttendanceEvent[] log holds at most one event per source — a
 * new mark from a source replaces its previous one, and an undo removes that
 * source's entry outright. This function computes the one effective attendance
 * status (present / absent / unknown) by a fixed source hierarchy: the
 * highest-ranked source with a recorded mark decides, in either direction.
 * This function has no DB, clock, or role knowledge, so it is exhaustively
 * unit-testable against the PRD truth table.
 *
 * See docs/implementation/SESSION_ATTENDANCE/{prd.md,trd.md,use-cases.md}.
 */

import { AttendanceSourceEnum } from '../enum/attendance-source.enum';
import { AttendanceStatus } from '../enum/attendance-status.enum';
import { AttendanceEvent } from '../interfaces/attendance-event.interface';
import { ROLE_VALUES } from '../constants/strings-constants';

/**
 * Source precedence (higher wins). Coordinator > RM > Zoom > QR > Join-click is
 * locked; the order within the automated tier is provisional (PRD oq-01).
 * MANUAL_ADMIN (legacy) is ranked at coordinator level so historical events keep
 * resolving without a lossy rewrite (TRD dec-11). This map is the single place
 * that declares ranking — adding a source is a change here alone (PRD req-01).
 */
export const ATTENDANCE_SOURCE_RANK: Record<AttendanceSourceEnum, number> = {
  [AttendanceSourceEnum.MANUAL_COORDINATOR]: 5,
  [AttendanceSourceEnum.MANUAL_ADMIN]: 5,
  [AttendanceSourceEnum.MANUAL_RM]: 4,
  [AttendanceSourceEnum.ZOOM_WEBHOOK]: 3,
  [AttendanceSourceEnum.QR_SCAN]: 2,
  [AttendanceSourceEnum.JOIN_CLICK]: 1,
};

/** Manual sources whose events carry an actor and are owner-scoped for undo. */
export const MANUAL_SOURCES: readonly AttendanceSourceEnum[] = [
  AttendanceSourceEnum.MANUAL_COORDINATOR,
  AttendanceSourceEnum.MANUAL_ADMIN,
  AttendanceSourceEnum.MANUAL_RM,
];

/** The resolved attendance status plus the source that decided it. */
export interface AttendanceResolution {
  status: AttendanceStatus;
  decidedBySource: AttendanceSourceEnum | null;
}

/** A legacy event without an explicit status is read as PRESENT (TRD §5). */
function statusOf(event: AttendanceEvent): AttendanceStatus {
  return event.status ?? AttendanceStatus.PRESENT;
}

/**
 * Returns one event per source. The log is written so each source holds at
 * most one entry already; this defensively keeps only the latest (by
 * occurredAt) per source in case duplicates ever land in the array (e.g.
 * legacy data written before this invariant existed).
 */
export function activeEvents(events: AttendanceEvent[]): AttendanceEvent[] {
  const latestBySource = new Map<AttendanceSourceEnum, AttendanceEvent>();
  for (const e of events) {
    const current = latestBySource.get(e.source);
    if (!current || e.occurredAt >= current.occurredAt) {
      latestBySource.set(e.source, e);
    }
  }
  return Array.from(latestBySource.values());
}

/**
 * Resolves the effective attendance status from an event log. Picks the
 * highest-ranked active source; ties at the same rank (e.g. MANUAL_ADMIN vs
 * MANUAL_COORDINATOR) are broken by the most recent occurredAt. No active
 * record → UNKNOWN.
 */
export function resolveAttendanceStatus(
  events: AttendanceEvent[] | null | undefined,
): AttendanceResolution {
  const active = activeEvents(events ?? []);
  if (active.length === 0) {
    return { status: AttendanceStatus.UNKNOWN, decidedBySource: null };
  }

  let winner: AttendanceEvent | null = null;
  let winnerRank = -1;
  for (const e of active) {
    const rank = ATTENDANCE_SOURCE_RANK[e.source] ?? 0;
    // Higher rank wins; equal rank breaks toward the later occurredAt.
    if (
      rank > winnerRank ||
      (rank === winnerRank && winner !== null && e.occurredAt >= winner.occurredAt)
    ) {
      winner = e;
      winnerRank = rank;
    }
  }

  return {
    status: statusOf(winner as AttendanceEvent),
    decidedBySource: (winner as AttendanceEvent).source,
  };
}

/** True when the resolved attendance status counts the registrant as present. */
export function isAttendedFromStatus(status: AttendanceStatus): boolean {
  return status === AttendanceStatus.PRESENT;
}

/**
 * A single source's own mark, independent of the overall resolved winner — e.g. a Zoom join stays
 * visible as `true` even after a Coordinator's mark outranks it for `attendanceStatus`. Null when
 * that source never marked this registrant.
 */
export function activeMarkFrom(
  events: AttendanceEvent[] | null | undefined,
  source: AttendanceSourceEnum,
): boolean | null {
  const match = activeEvents(events ?? []).find((e) => e.source === source);
  return match ? statusOf(match) === AttendanceStatus.PRESENT : null;
}

/** The manual source a mark records, plus the specific active role that chose it. */
export interface ManualActor {
  source: AttendanceSourceEnum;
  /** The active role name this mark was made under (stored on the event for audit). */
  role: string;
}

/**
 * Resolves the manual source AND the active role for a marking action from the
 * acting user's roles. A user may hold several roles; `roles` should already be
 * scoped to the active role (the auth layer filters it by the `active-role`
 * header). Coordinator (`shoba`) and `admin` resolve at coordinator level, RM
 * (`relational_manager`) at RM level. When more than one qualifying role is
 * present (no active-role header), the higher authority wins and its role name
 * is reported. Returns null when no role authorizes a manual mark.
 */
export function resolveManualActor(roles: string[] | undefined): ManualActor | null {
  const list = roles ?? [];
  if (list.includes(ROLE_VALUES.COORDINATOR)) {
    return { source: AttendanceSourceEnum.MANUAL_COORDINATOR, role: ROLE_VALUES.COORDINATOR };
  }
  if (list.includes(ROLE_VALUES.ADMIN)) {
    return { source: AttendanceSourceEnum.MANUAL_COORDINATOR, role: ROLE_VALUES.ADMIN };
  }
  if (list.includes(ROLE_VALUES.RELATIONAL_MANAGER)) {
    return { source: AttendanceSourceEnum.MANUAL_RM, role: ROLE_VALUES.RELATIONAL_MANAGER };
  }
  return null;
}

/**
 * Maps the acting user's active roles to the manual source their mark records.
 * Thin wrapper over {@link resolveManualActor} for callers that only need the
 * source. Returns null when no role authorizes a manual mark.
 */
export function deriveManualSource(
  roles: string[] | undefined,
): AttendanceSourceEnum | null {
  return resolveManualActor(roles)?.source ?? null;
}
