import { Injectable } from '@nestjs/common';
import { eq, inArray, sql } from 'drizzle-orm';
import { DatabaseService } from '../database/database.service';
import { userModuleAccess } from '../../database/schema';
import {
  isModuleName,
  type ModuleAccessLevel,
  type ModuleAccessMap,
  type ModuleName,
  noModuleAccess,
} from './modules';

/**
 * Reads and writes per-user module entitlements. The only place that turns
 * `user_module_access` rows into a complete map — every caller (session
 * resolution, admin cockpit) goes through here so "missing row means NONE" is
 * decided once.
 */
@Injectable()
export class ModuleAccessRepository {
  constructor(private readonly database: DatabaseService) {}

  async forUser(userId: string): Promise<ModuleAccessMap> {
    const rows = await this.database.db
      .select({ module: userModuleAccess.module, level: userModuleAccess.level })
      .from(userModuleAccess)
      .where(eq(userModuleAccess.userId, userId));
    return toMap(rows);
  }

  /** Entitlements for many users at once, keyed by user id. Used by the admin list. */
  async forUsers(userIds: string[]): Promise<Map<string, ModuleAccessMap>> {
    const result = new Map<string, ModuleAccessMap>(
      userIds.map((id) => [id, noModuleAccess()] as const),
    );
    if (!userIds.length) return result;

    const rows = await this.database.db
      .select({
        userId: userModuleAccess.userId,
        module: userModuleAccess.module,
        level: userModuleAccess.level,
      })
      .from(userModuleAccess)
      .where(inArray(userModuleAccess.userId, userIds));

    for (const row of rows) {
      if (!isModuleName(row.module)) continue;
      const map = result.get(row.userId);
      if (map) map[row.module] = row.level;
    }
    return result;
  }

  /**
   * Apply a partial change. Only the supplied modules are touched, so the admin
   * UI can send a single toggle without restating the whole map.
   */
  async set(userId: string, changes: Partial<Record<ModuleName, ModuleAccessLevel>>): Promise<void> {
    const entries = Object.entries(changes).filter(([module]) => isModuleName(module)) as Array<
      [ModuleName, ModuleAccessLevel]
    >;
    if (!entries.length) return;

    await this.database.db
      .insert(userModuleAccess)
      .values(entries.map(([module, level]) => ({ userId, module, level })))
      .onConflictDoUpdate({
        target: [userModuleAccess.userId, userModuleAccess.module],
        set: {
          level: sql`excluded.level`,
          updatedAt: new Date(),
        },
      });
  }
}

function toMap(rows: Array<{ module: string; level: ModuleAccessLevel }>): ModuleAccessMap {
  const map = noModuleAccess();
  for (const row of rows) {
    // Rows for modules that no longer exist in code are ignored rather than
    // deleted: dropping them would destroy an administrator's intent if the
    // module is reintroduced later.
    if (isModuleName(row.module)) map[row.module] = row.level;
  }
  return map;
}
