import { Injectable } from '@nestjs/common';
import { Logger } from 'nestjs-pino';
import { PrismaService } from 'nestjs-prisma';
import { isScopeKind, type Scope } from 'shared-auth';
import { ScopeResolver } from './scope-resolver';

/**
 * `PrismaScopeResolver` — production implementation of
 * [`ScopeResolver`](scope-resolver.ts) backed by the `user_scopes`
 * table introduced in ADR-0026 PR 1.
 *
 * Replaces `StubScopeResolver` (which always returned
 * `[{ kind: 'unrestricted' }]`) per ADR-0025 §"Sources of truth —
 * apf_portal-side `user_scopes` table". Called once per sign-in by
 * `PrincipalBuilder.build`; the resolved scopes ride on the
 * session-resident `Principal` so guards never re-query at request
 * time.
 *
 * **Expiry handling.** Rows with a non-null `expiresAt` in the past
 * are filtered server-side (`expiresAt IS NULL OR expiresAt > NOW()`).
 * The unique-tuple constraint on `(userId, kind, value)` means a
 * persona with a `delegation:33` scope that just expired can be
 * re-granted by inserting a new row — the resolver picks up the
 * fresh one on the next sign-in. No background cleanup job in v1;
 * expired rows are harmless until either a re-grant fires the
 * unique constraint or an admin manually purges them.
 *
 * **Catalogue defense.** Rows with an unknown `kind` (off-catalogue
 * — should be impossible given the drift gate, but possible if a
 * future migration writes raw SQL or a future operator runs an
 * unguarded INSERT) are skipped + logged. Defensive read; the write
 * path is the canonical place to enforce catalogue membership.
 */
@Injectable()
export class PrismaScopeResolver extends ScopeResolver {
  constructor(
    private readonly prisma: PrismaService,
    private readonly logger: Logger,
  ) {
    super();
  }

  async resolve({ userId }: { userId: string }): Promise<ReadonlyArray<Scope>> {
    const now = new Date();
    const rows = await this.prisma.userScope.findMany({
      where: {
        userId,
        OR: [{ expiresAt: null }, { expiresAt: { gt: now } }],
      },
      select: { kind: true, value: true },
    });

    const scopes: Scope[] = [];
    for (const row of rows) {
      const scope = toScope(row.kind, row.value);
      if (scope === null) {
        this.logger.warn(
          {
            event: 'scope_resolver.unknown_kind',
            userId,
            kind: row.kind,
          },
          'PrismaScopeResolver',
        );
        continue;
      }
      scopes.push(scope);
    }
    return scopes;
  }
}

/**
 * Map a `(kind, value)` DB row to a typed `Scope`. Returns null for
 * off-catalogue kinds so the resolver can log + skip them without a
 * throw. Exported for the spec — call sites should always go through
 * the resolver.
 */
export function toScope(kind: string, value: string): Scope | null {
  if (!isScopeKind(kind)) return null;
  switch (kind) {
    case 'self':
    case 'siege':
    case 'unrestricted':
      return { kind };
    case 'etablissement':
    case 'delegation':
    case 'region':
      return { kind, value };
  }
}
