import { ForbiddenError, PreconditionFailedError, ValidationError } from '../errors/index.js';
import { hasAnyPermission, hasPermission, type ResourceTarget, isInScope } from '../auth/authorize.js';
import type { SecurityContext } from '../auth/context.js';
import type { Permission } from '../auth/permissions.js';

/**
 * A generic, declarative workflow engine.
 *
 * SPEC.md §13 requires workflows to be explicit state machines rather than free
 * status strings. Every workflow in the system declares a `TransitionTable`;
 * `transition()` is the only way a status changes, and it enforces — in one
 * place, for every workflow — that the move is declared, that the actor holds
 * the required permission, that the target is in scope, that a reason is
 * supplied where one is mandatory, and that named preconditions hold.
 */

export interface TransitionRule {
  /** Permission required to make this move. */
  readonly permission?: Permission;
  /** Alternative to `permission`: any one of these suffices. */
  readonly anyPermission?: readonly Permission[];
  /** When true, `reason` must be a non-empty string (SPEC.md §33.13). */
  readonly requiresReason?: boolean;
  /**
   * Named conditions the caller must have verified. The service resolves them
   * against the database and passes the satisfied set; any missing condition
   * fails the transition with a structured list of what is outstanding.
   */
  readonly requires?: readonly string[];
  /** Human-readable label used in timelines and audit entries. */
  readonly label: string;
}

/**
 * `from` state → `to` state → rule. A move absent from the table is rejected,
 * which is what makes arbitrary status assignment impossible.
 */
export type TransitionTable<S extends string> = Readonly<
  Partial<Record<S, Readonly<Partial<Record<S, TransitionRule>>>>>
>;

export interface WorkflowDefinition<S extends string> {
  readonly name: string;
  readonly initial: S;
  readonly states: readonly S[];
  /** States from which no further transition is possible. */
  readonly terminal: readonly S[];
  readonly transitions: TransitionTable<S>;
}

export interface TransitionRequest<S extends string> {
  readonly from: S;
  readonly to: S;
  readonly ctx: SecurityContext;
  /** Required when the rule sets `requiresReason`. */
  readonly reason?: string | null;
  /** Resource ownership/scope of the record being transitioned. */
  readonly target?: ResourceTarget;
  /**
   * Names of preconditions the caller has verified as satisfied. Compared
   * against the rule's `requires` list.
   */
  readonly satisfied?: readonly string[];
}

export interface TransitionOutcome<S extends string> {
  readonly from: S;
  readonly to: S;
  readonly rule: TransitionRule;
  /** Normalized reason, trimmed; `null` when none was required or supplied. */
  readonly reason: string | null;
  readonly label: string;
}

/** Whether a move exists in the table at all, ignoring permissions. */
export function canTransition<S extends string>(
  definition: WorkflowDefinition<S>,
  from: S,
  to: S,
): boolean {
  return definition.transitions[from]?.[to] !== undefined;
}

/** Every state reachable from `from`, ignoring permissions. */
export function nextStates<S extends string>(definition: WorkflowDefinition<S>, from: S): S[] {
  const row = definition.transitions[from];
  return row ? (Object.keys(row) as S[]) : [];
}

/**
 * The moves this actor could actually make from `from`. Used to render only
 * the actions a user can perform — a convenience for the UI that never
 * substitutes for the server-side check in {@link transition}.
 */
export function availableTransitions<S extends string>(
  definition: WorkflowDefinition<S>,
  from: S,
  ctx: SecurityContext,
  target?: ResourceTarget,
): Array<{ to: S; rule: TransitionRule }> {
  const row = definition.transitions[from];
  if (!row) return [];
  return (Object.entries(row) as Array<[S, TransitionRule]>)
    .filter(([, rule]) => actorSatisfiesPermission(ctx, rule))
    .filter(() => (target ? isInScope(ctx, target) : true))
    .map(([to, rule]) => ({ to, rule }));
}

function actorSatisfiesPermission(ctx: SecurityContext, rule: TransitionRule): boolean {
  if (rule.permission) return hasPermission(ctx, rule.permission);
  if (rule.anyPermission && rule.anyPermission.length > 0) {
    return hasAnyPermission(ctx, rule.anyPermission);
  }
  // A rule with no permission is open to any authenticated actor whose scope
  // admits the target — used for self-service moves such as a student
  // submitting their own document request.
  return true;
}

export function isTerminal<S extends string>(definition: WorkflowDefinition<S>, state: S): boolean {
  return definition.terminal.includes(state);
}

/**
 * Validates and describes a status change. Returns the outcome to persist;
 * throws if the move is not permitted.
 *
 * The caller is responsible for writing the new status **and** the status
 * history row in a single transaction, so current state and history can never
 * diverge (ARCHITECTURE.md §6).
 *
 * @throws ValidationError        the move is not declared, or a reason is missing.
 * @throws ForbiddenError         the actor lacks the permission.
 * @throws PreconditionFailedError a named precondition is unmet.
 */
export function transition<S extends string>(
  definition: WorkflowDefinition<S>,
  request: TransitionRequest<S>,
): TransitionOutcome<S> {
  const { from, to, ctx, target } = request;

  if (from === to) {
    throw new ValidationError(`The ${definition.name} is already in the "${to}" state.`);
  }

  if (isTerminal(definition, from)) {
    throw new ValidationError(
      `The ${definition.name} is in the final state "${from}" and cannot be changed.`,
    );
  }

  const rule = definition.transitions[from]?.[to];
  if (!rule) {
    throw new ValidationError(
      `Moving a ${definition.name} from "${from}" to "${to}" is not a permitted step.`,
      { meta: { from, to, allowed: nextStates(definition, from) } },
    );
  }

  if (!actorSatisfiesPermission(ctx, rule)) {
    throw new ForbiddenError(`You do not have permission to ${rule.label.toLowerCase()}.`, {
      meta: {
        requiredPermission: rule.permission,
        requiredAnyOf: rule.anyPermission,
      },
    });
  }

  if (target && !isInScope(ctx, target)) {
    throw new ForbiddenError('This record is outside the scope assigned to your account.', {
      meta: { workflow: definition.name },
    });
  }

  const reason = typeof request.reason === 'string' ? request.reason.trim() : '';
  if (rule.requiresReason && reason.length === 0) {
    throw new ValidationError('A reason is required for this action.', {
      details: [{ path: 'reason', message: 'A reason is required for this action.' }],
    });
  }

  if (rule.requires && rule.requires.length > 0) {
    const satisfied = new Set(request.satisfied ?? []);
    const unmet = rule.requires.filter((condition) => !satisfied.has(condition));
    if (unmet.length > 0) {
      throw new PreconditionFailedError(
        `This step cannot be completed until the outstanding requirements are met.`,
        {
          details: unmet.map((condition) => ({
            path: condition,
            message: describeCondition(condition),
          })),
          meta: { unmetConditions: unmet },
        },
      );
    }
  }

  return { from, to, rule, reason: reason.length > 0 ? reason : null, label: rule.label };
}

/**
 * Human-readable text for a precondition key. Keys are stable identifiers used
 * by the API; this map keeps the message in one place rather than duplicating
 * it at every call site.
 */
const CONDITION_MESSAGES: Record<string, string> = {
  REQUIREMENTS_COMPLETE: 'All mandatory admission requirements must be accepted or waived.',
  EXAM_RESULT_RECORDED: 'An entrance examination result must be recorded.',
  NMAT_THRESHOLD_MET: 'The applicant must meet the configured minimum NMAT percentile.',
  INTERVIEW_COMPLETED: 'The applicant interview must be completed and evaluated.',
  PANEL_INTERVIEW_COMPLETED: 'The panel interview must be completed and evaluated.',
  CLEARANCE_COMPLETE: 'Clearance must be completed for all required offices.',
  PRE_REGISTRATION_APPROVED: 'Pre-registration must be approved by the Dean.',
  ASSESSMENT_GENERATED: 'An assessment must be generated for this enrollment.',
  MINIMUM_PAYMENT_MET: 'The configured minimum down payment must be recorded.',
  NO_SCHEDULE_CONFLICT: 'The selected sections must not conflict with each other.',
  UNIT_LOAD_WITHIN_LIMIT: 'The total unit load must be within the configured maximum.',
  PREREQUISITES_SATISFIED: 'All course prerequisites must be satisfied or formally overridden.',
  ALL_GRADES_ENTERED: 'A grade must be entered for every student in the class list.',
  GRADES_SUBMITTED: 'Grades must be submitted by the faculty member before publication.',
  PAYMENT_SETTLED: 'The associated fee must be settled before release.',
  DOCUMENT_ATTACHED: 'The prepared document must be attached before release.',
};

export function describeCondition(condition: string): string {
  return CONDITION_MESSAGES[condition] ?? `The condition "${condition}" is not yet satisfied.`;
}
