// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.

/**
 * @objectstack/spec/contracts/sharing-service
 *
 * Cross-package contract for record-level sharing enforcement.
 *
 * Two concerns live behind this interface:
 *
 *   1. **Filter contribution** — `buildReadFilter()` returns a
 *      `FilterCondition` (or `null` for "no restriction") that the
 *      engine middleware AND-s into every read query. Callers must
 *      treat `null` as "object is public, do not filter".
 *
 *   2. **Per-record gating** — `canEdit()` answers the access question
 *      for `update` / `delete` operations. Returns `true` when the
 *      caller may modify the record, `false` otherwise.
 *
 * Manual share CRUD is exposed via `grant()`, `revoke()`, and
 * `listShares()`. The REST layer wires these to
 * `/data/:object/:id/shares`.
 *
 * The default implementation lives in `@objectstack/plugin-sharing`.
 */

/** Recipient categories — mirrors `ShareRecipientType` in spec/security. */
export type ShareRecipientType =
  | 'user'
  | 'group'
  | 'role'
  | 'role_and_subordinates'
  | 'guest';

/** Access level on a single record. */
export type ShareAccessLevel = 'read' | 'edit' | 'full';

/** Why a share row exists (used by the rule evaluator to reconcile). */
export type ShareSource = 'manual' | 'rule' | 'team' | 'inherited';

/** Single row from `sys_record_share` projected for cross-package callers. */
export interface RecordShare {
  id: string;
  object_name: string;
  record_id: string;
  recipient_type: ShareRecipientType;
  recipient_id: string;
  access_level: ShareAccessLevel;
  source: ShareSource;
  source_id?: string;
  granted_by?: string;
  reason?: string;
  created_at?: string;
  updated_at?: string;
}

/** Input for `ISharingService.grant`. */
export interface GrantShareInput {
  object: string;
  recordId: string;
  recipientType?: ShareRecipientType;
  recipientId: string;
  accessLevel?: ShareAccessLevel;
  source?: ShareSource;
  sourceId?: string;
  reason?: string;
}

/** Minimal execution-context shape the service needs from callers. */
export interface SharingExecutionContext {
  userId?: string;
  tenantId?: string;
  roles?: string[];
  permissions?: string[];
  isSystem?: boolean;
}

/**
 * Public contract.
 *
 * Implementations should treat `context.isSystem === true` as a
 * complete bypass (no filter, every `canEdit` returns `true`) so that
 * platform-internal writers (audit, migrations, the sharing plugin
 * itself) cannot deadlock on their own enforcement.
 */
export interface ISharingService {
  /**
   * Return a filter condition that restricts a `find` to rows the
   * principal in `context` can see for `object`. Return `null` when no
   * restriction applies (public object, system context, no userId).
   */
  buildReadFilter(
    object: string,
    context: SharingExecutionContext,
  ): Promise<unknown | null>;

  /**
   * Return `true` when the principal in `context` may modify the
   * record `(object, recordId)`. Owner-only for `private` / `read`
   * objects; always true for `public` objects.
   */
  canEdit(
    object: string,
    recordId: string,
    context: SharingExecutionContext,
  ): Promise<boolean>;

  /** Create or upsert a manual share row. */
  grant(input: GrantShareInput, context: SharingExecutionContext): Promise<RecordShare>;

  /** Remove a share row by id. No-op when not found. */
  revoke(shareId: string, context: SharingExecutionContext): Promise<void>;

  /** List all share rows attached to `(object, recordId)`. */
  listShares(
    object: string,
    recordId: string,
    context: SharingExecutionContext,
  ): Promise<RecordShare[]>;
}

// ─────────────────────────────────────────────────────────────────────
// SharingRuleService — declarative criteria-based sharing
// (Salesforce-style "any record matching X is shared with team Y").
// ─────────────────────────────────────────────────────────────────────

/**
 * Kinds of principals a rule can target.
 *
 * - `user`       — a specific user id (no expansion)
 * - `team`       — a flat collaboration team (`sys_team` + `sys_team_member`)
 * - `department` — an org-skeleton node (`sys_department` + descendants via
 *                  `parent_department_id` + members from `sys_department_member`)
 * - `role`       — tenant role on `sys_member.role`
 * - `queue`      — opaque queue identifier (resolution left to caller / app)
 */
export type SharingRuleRecipientType = 'user' | 'team' | 'department' | 'role' | 'queue';

/**
 * Stored shape of a sharing rule. Maps 1-to-1 to `sys_sharing_rule`
 * with `criteria` lifted out of the JSON column.
 */
export interface SharingRuleRow {
  id: string;
  organization_id?: string | null;
  name: string;
  label: string;
  description?: string | null;
  object_name: string;
  /**
   * Engine-compatible FilterCondition. `undefined`/`null` matches every
   * row of `object_name` (use with care — typically scoped via teams).
   */
  criteria?: unknown;
  recipient_type: SharingRuleRecipientType;
  recipient_id: string;
  access_level: ShareAccessLevel;
  active: boolean;
  created_at?: string;
  updated_at?: string;
}

/** Input to {@link ISharingRuleService.defineRule}. */
export interface DefineSharingRuleInput {
  name: string;
  label: string;
  description?: string;
  object: string;
  criteria?: unknown;
  recipientType: SharingRuleRecipientType;
  recipientId: string;
  accessLevel?: ShareAccessLevel;
  active?: boolean;
}

/** Result of a rule evaluation pass. */
export interface SharingRuleEvaluationResult {
  ruleId: string;
  matchedRecords: number;
  expandedUsers: number;
  grantsCreated: number;
  grantsUpdated: number;
  grantsRevoked: number;
}

/**
 * Declarative sharing rules. The evaluator materialises grants into
 * `sys_record_share` with `source='rule'` and `source_id=rule.id` so
 * stale grants from a rule update can be reconciled without touching
 * manual or team-derived shares.
 */
export interface ISharingRuleService {
  defineRule(input: DefineSharingRuleInput, context: SharingExecutionContext): Promise<SharingRuleRow>;
  listRules(filter: { object?: string; activeOnly?: boolean }, context: SharingExecutionContext): Promise<SharingRuleRow[]>;
  getRule(idOrName: string, context: SharingExecutionContext): Promise<SharingRuleRow | null>;
  deleteRule(idOrName: string, context: SharingExecutionContext): Promise<void>;

  /**
   * Re-evaluate a rule across every record of its object_name and
   * reconcile the resulting `sys_record_share` rows. Admin-initiated;
   * use after rule edits or for backfill.
   */
  evaluateRule(idOrName: string, context: SharingExecutionContext): Promise<SharingRuleEvaluationResult>;

  /**
   * Incremental evaluation triggered by the lifecycle hook — re-checks
   * every active rule for `object` against this single record and
   * upserts/reconciles only that record's share rows.
   */
  evaluateAllForRecord(object: string, recordId: string, context: SharingExecutionContext): Promise<SharingRuleEvaluationResult[]>;
}

// ─────────────────────────────────────────────────────────────────────
// Team graph helpers (lives behind ISharingRuleService implementations
// but is exported so the approval engine can expand `team:` / `role:`
// approver references without depending on the plugin directly).
// ─────────────────────────────────────────────────────────────────────

/**
 * Flat collaboration team graph (better-auth's `sys_team` semantics).
 *
 * Teams in this model are *not* hierarchical — they are ad-hoc groupings.
 * For the enterprise org-chart hierarchy, see {@link IDepartmentGraphService}.
 */
export interface ITeamGraphService {
  /** Return all user ids that are members of `teamId`. */
  expandUsers(teamId: string): Promise<string[]>;
  /** Return all user ids that hold a role of `roleName` in `organizationId`. */
  expandRoleUsers(roleName: string, organizationId?: string): Promise<string[]>;
  /** Return the manager id for a user (best-effort; null when none). */
  managerOf(userId: string, organizationId?: string): Promise<string | null>;
}

/**
 * Hierarchical department graph (`sys_department` org skeleton).
 *
 * Walks `parent_department_id` to expand a department into the union of
 * its members and all descendant members. Drives:
 *   - `recipient_type='department'` sharing rules
 *   - `dept:` approver prefix in the approval engine
 *   - report rollups, manager chains, and similar org-aware logic
 */
export interface IDepartmentGraphService {
  /** Return all descendant department ids (BFS, includes the seed). */
  descendants(departmentId: string): Promise<string[]>;
  /** Return all user ids in `departmentId` or any descendant department. */
  expandUsers(departmentId: string): Promise<string[]>;
  /** Return the department head (manager_user_id) — best-effort, null when none. */
  headOf(departmentId: string): Promise<string | null>;
  /** Return the manager id for a user — proxy to {@link ITeamGraphService.managerOf}. */
  managerOf(userId: string, organizationId?: string): Promise<string | null>;
}
