import { logger } from '../../logger.js';

// ── Public types ─────────────────────────────────────────────────────────────

export type RevokeOutcome =
  | { status: 'revoked'; delegationId: string; grantId: string | null; cancelledJobs: number }
  | { status: 'already-revoked'; delegationId: string }
  | { status: 'not-found' }
  | { status: 'forbidden' };

export interface RevokeActor {
  userId: string;
  isAdmin: boolean;
}

/** Structural subset of Repository used by revokeDelegationCascade. */
export interface RevokeRepo {
  getA2aDelegationById(
    id: string,
  ): { id: string; userId: string; clientId: string; grantId: string | null; revokedAt: string | null } | undefined;
  revokeA2aDelegation(id: string, revokedAtIso: string): void;
  oidcRevokeByGrantId(grantId: string): void;
  listNonTerminalA2aTasksByGrant(
    grantId: string,
    limit?: number,
  ): Array<{ id: string; jobId: string | null; payload: Record<string, unknown> }>;
  cancelA2aLinkedJob(jobId: string): boolean;
  addAuditLog(jobId: string | null, action: string, actor: string, detail: object): Promise<void>;
}

// ── Core service ─────────────────────────────────────────────────────────────

/**
 * Revoke an A2A delegation with cascading effects:
 *  - marks the delegation row as revoked
 *  - destroys the OIDC grant artifacts (opaque token stops resolving)
 *  - cancels all non-terminal A2A-linked jobs for the grant
 *  - writes an audit log entry
 *
 * Idempotent: an already-revoked delegation returns `already-revoked` with no
 * side effects. The HTTP layer maps `not-found` and `forbidden` both to 404 to
 * avoid existence leaks; the distinct statuses exist for admin-path auditing.
 */
export async function revokeDelegationCascade(
  repo: RevokeRepo,
  delegationId: string,
  actor: RevokeActor,
  nowIso: string,
): Promise<RevokeOutcome> {
  // 1. Lookup
  const row = repo.getA2aDelegationById(delegationId);
  if (!row) return { status: 'not-found' };

  // 2. Authorisation
  if (!actor.isAdmin && row.userId !== actor.userId) return { status: 'forbidden' };

  // 3. Idempotency guard
  if (row.revokedAt) return { status: 'already-revoked', delegationId };

  // 4. Mark revoked
  repo.revokeA2aDelegation(delegationId, nowIso);

  // 5–6. OIDC grant tear-down + job cancellation
  let cancelledJobs = 0;
  if (row.grantId) {
    repo.oidcRevokeByGrantId(row.grantId);
    const tasks = repo.listNonTerminalA2aTasksByGrant(row.grantId);
    for (const task of tasks) {
      if (task.jobId) {
        const ok = repo.cancelA2aLinkedJob(task.jobId);
        if (ok) cancelledJobs++;
      }
    }
  }

  // 7. Audit (non-fatal)
  try {
    await repo.addAuditLog(null, 'a2a.delegation.revoked', actor.userId, {
      delegationId,
      clientId: row.clientId,
      cancelledJobs,
      byAdmin: actor.isAdmin,
    });
  } catch (err) {
    logger.warn(`[a2a-revoke] audit log failed (non-fatal): ${err}`);
  }

  logger.info(
    `[a2a-revoke] revoked delegation=${delegationId} grant=${row.grantId} cancelledJobs=${cancelledJobs} byAdmin=${actor.isAdmin}`,
  );

  // 8. Return outcome
  return { status: 'revoked', delegationId, grantId: row.grantId, cancelledJobs };
}
