import { Injectable } from '@nestjs/common';
import { Logger } from 'nestjs-pino';
import { PrismaService } from 'nestjs-prisma';

/**
 * The minimal slice of `AuthenticatedUser` the directory cares
 * about. Defined here (rather than importing the full session-side
 * type) so the directory service stays decoupled from the auth
 * module's internal shape and the spec can hand-roll the input.
 *
 * Distinct from the Prisma-generated `UserDirectoryEntry` type
 * (the full DB row shape) — this is the input to `recordSignIn`,
 * not the persisted entry itself.
 */
export interface RecordSignInInput {
  readonly oid: string;
  readonly tid: string;
  readonly username: string;
  readonly displayName: string;
  /**
   * `workforce` for v1 (single workforce-tenant Entra audience per
   * ADR-0008). External-ID / customer audience will land its own
   * value here when activated.
   */
  readonly audience: 'workforce' | 'customer';
}

/**
 * `UserDirectoryService` — persistent ledger of identities the BFF
 * has seen sign in. v1 is one upsert per sign-in, called from
 * `SessionEstablisher.establish` AFTER the blocking audit write
 * (so an audit failure short-circuits the sign-in before the
 * directory entry is ever recorded). Read by the future
 * `GET /api/admin/users` admin endpoint per ADR-0020 §"User list
 * (read-only)".
 *
 * Best-effort write. A Postgres hiccup writing the directory entry
 * MUST NOT fail the sign-in — the directory is a convenience for
 * admin browsing, not a security boundary. ADR-0013's "no audit ⇒
 * no action" applies to the audit module; this is a separate
 * write path with weaker invariants. Failures are logged via Pino
 * so ops can spot a directory falling behind.
 */
@Injectable()
export class UserDirectoryService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly logger: Logger,
  ) {}

  /**
   * Upsert: on first sign-in, INSERT a new row with both
   * `first_seen_at` and `last_seen_at` set to NOW(); on subsequent
   * sign-ins, UPDATE `last_seen_at`, `username`, `display_name`,
   * `audience`, `tid` (any of those five may legitimately change —
   * a user's `displayName` can be edited tenant-side; a previously
   * workforce-only user could appear under the customer audience
   * once dual-audience ships).
   *
   * `first_seen_at` is NEVER overwritten — `@updatedAt` semantics
   * don't apply; the field is set once via the DEFAULT clause on
   * INSERT and the UPDATE branch leaves it alone.
   */
  async recordSignIn(entry: RecordSignInInput): Promise<void> {
    try {
      await this.prisma.userDirectoryEntry.upsert({
        where: { oid: entry.oid },
        create: {
          oid: entry.oid,
          tid: entry.tid,
          audience: entry.audience,
          username: entry.username,
          displayName: entry.displayName,
        },
        update: {
          tid: entry.tid,
          audience: entry.audience,
          username: entry.username,
          displayName: entry.displayName,
          lastSeenAt: new Date(),
        },
      });
    } catch (err) {
      // Postgres hiccup, schema drift, or a row-locked race against
      // a concurrent sign-in. Logged but not propagated — the
      // admin UI tolerates a missing entry until the next sign-in.
      this.logger.warn(
        {
          event: 'user_directory.record_sign_in_failed',
          oid: entry.oid,
          reason: err instanceof Error ? err.message : String(err),
        },
        'UserDirectoryService',
      );
    }
  }
}
