import { Injectable } from '@nestjs/common';
import type { Prisma } from '@prisma/client';
import { PrismaService } from 'nestjs-prisma';
import { DEFAULT_LIMIT, MAX_LIMIT, type AdminUsersQueryDto } from './users-query.dto';

/**
 * SPA-facing projection of a row from `public.user_directory_entries`.
 * Mirrors the Prisma model but exposes ISO-string timestamps so the
 * SPA never has to know about `Date` serialisation conventions.
 */
export interface AdminUserDto {
  readonly oid: string;
  readonly tid: string;
  readonly audience: string;
  readonly username: string;
  readonly displayName: string;
  readonly firstSeenAt: string;
  readonly lastSeenAt: string;
}

export interface AdminUsersPage {
  readonly items: readonly AdminUserDto[];
  readonly total: number;
  readonly limit: number;
  readonly offset: number;
}

/**
 * `AdminUsersReader` — query side of the
 * `public.user_directory_entries` directory per ADR-0020 §"v1 scope
 * — User list (read-only)". Drives the `GET /api/admin/users` admin
 * endpoint.
 *
 * Uses Prisma's typed client directly — unlike `AuditReader`, no
 * `SET LOCAL ROLE` is needed because `public.user_directory_entries`
 * has no role-based privilege gate (it's a regular business table;
 * the trust boundary is the `@RequireAdmin` guard on the controller).
 *
 * Default order: `last_seen_at DESC` — the "most recently active"
 * sort the admin UI most often wants. Falls back to `oid ASC` as
 * a tie-breaker so pagination stays deterministic when two rows
 * share a timestamp (rare, but possible during a sign-in burst).
 */
@Injectable()
export class AdminUsersReader {
  constructor(private readonly prisma: PrismaService) {}

  async findUsers(filters: AdminUsersQueryDto): Promise<AdminUsersPage> {
    const limit = Math.min(filters.limit ?? DEFAULT_LIMIT, MAX_LIMIT);
    const offset = filters.offset ?? 0;
    const where = buildWhere(filters);

    // Run COUNT + SELECT in a single Prisma transaction so the
    // `total` reported to the SPA matches what's on the page —
    // a concurrent sign-in landing between the two queries would
    // otherwise produce an off-by-one between the count and the
    // items.
    const [total, items] = await this.prisma.$transaction([
      this.prisma.userDirectoryEntry.count({ where }),
      this.prisma.userDirectoryEntry.findMany({
        where,
        orderBy: [{ lastSeenAt: 'desc' }, { oid: 'asc' }],
        take: limit,
        skip: offset,
      }),
    ]);

    return {
      items: items.map(toDto),
      total,
      limit,
      offset,
    };
  }
}

function buildWhere(filters: AdminUsersQueryDto): Prisma.UserDirectoryEntryWhereInput {
  const where: Prisma.UserDirectoryEntryWhereInput = {};
  if (filters.username !== undefined) {
    where.username = { startsWith: filters.username };
  }
  if (filters.displayName !== undefined) {
    // Display names have natural casing variation ("Jane Doe" vs
    // "jane doe") so the admin filter matches case-insensitively.
    where.displayName = { contains: filters.displayName, mode: 'insensitive' };
  }
  if (filters.audience !== undefined) {
    where.audience = filters.audience;
  }
  if (filters.lastSeenAtFrom !== undefined || filters.lastSeenAtTo !== undefined) {
    where.lastSeenAt = {
      ...(filters.lastSeenAtFrom !== undefined ? { gte: new Date(filters.lastSeenAtFrom) } : {}),
      ...(filters.lastSeenAtTo !== undefined ? { lt: new Date(filters.lastSeenAtTo) } : {}),
    };
  }
  return where;
}

interface UserRow {
  oid: string;
  tid: string;
  audience: string;
  username: string;
  displayName: string;
  firstSeenAt: Date;
  lastSeenAt: Date;
}

function toDto(row: UserRow): AdminUserDto {
  return {
    oid: row.oid,
    tid: row.tid,
    audience: row.audience,
    username: row.username,
    displayName: row.displayName,
    firstSeenAt: row.firstSeenAt.toISOString(),
    lastSeenAt: row.lastSeenAt.toISOString(),
  };
}
