import { readFileSync } from 'node:fs';
import { isAbsolute, resolve } from 'node:path';
import { EntraGroupMapError, EntraGroupToRoleResolver, parseEntraGroupMap } from 'shared-auth';

/**
 * Outcome of `loadEntraGroupToRoleResolver` — paired with the
 * source path (when set) so the boot log can name the file that
 * was loaded, even when the resolver itself ends up empty.
 */
export interface EntraGroupResolverLoadResult {
  readonly resolver: EntraGroupToRoleResolver;
  /** `null` when no path was configured (env var unset). */
  readonly sourcePath: string | null;
}

/**
 * Loads the Entra group-GUID → role-slug map at BFF boot per
 * [ADR-0025 §"Sources of truth — Entra-side configuration"](../../../../docs/decisions/0025-authorization-model-privileges-roles-scopes.md).
 *
 * Sourcing precedence:
 *   1. `ENTRA_GROUP_MAP_PATH` env var → JSON file path (absolute,
 *      or relative to `cwd`).
 *   2. Unset → empty resolver, WARN logged at boot. Sign-in still
 *      works but every user gets an empty `roles[]` until the
 *      operator wires up the file. Acceptable for fresh dev
 *      environments; production should always set the var.
 *
 * Failure modes:
 *   - File configured but unreadable / missing → empty resolver,
 *     WARN. Same fail-soft posture: an operator pointing at the
 *     wrong path should not block every sign-in.
 *   - File present but malformed (bad JSON, wrong shape, unknown
 *     slug, duplicate GUID) → throws. A misconfigured map is a
 *     hard-fail because the alternative is silently mis-resolving
 *     a role.
 *
 * The function never reads `process.env` outside the documented
 * key and never mutates it.
 */
export function loadEntraGroupToRoleResolver(opts: {
  cwd?: string;
  env?: NodeJS.ProcessEnv;
  onWarn: (event: string, payload: Record<string, unknown>) => void;
}): EntraGroupResolverLoadResult {
  const env = opts.env ?? process.env;
  const cwd = opts.cwd ?? process.cwd();
  const rawPath = env['ENTRA_GROUP_MAP_PATH'];

  if (typeof rawPath !== 'string' || rawPath === '') {
    opts.onWarn('auth.entra_group_map_path_unset', {
      hint: 'set ENTRA_GROUP_MAP_PATH to infra/<env>-tenant.entra.json — sign-in will succeed but resolve zero functional roles until configured',
    });
    return { resolver: new EntraGroupToRoleResolver(new Map()), sourcePath: null };
  }

  const absolutePath = isAbsolute(rawPath) ? rawPath : resolve(cwd, rawPath);

  let raw: string;
  try {
    raw = readFileSync(absolutePath, 'utf8');
  } catch (err) {
    opts.onWarn('auth.entra_group_map_file_unreadable', {
      path: absolutePath,
      message: err instanceof Error ? err.message : String(err),
    });
    return { resolver: new EntraGroupToRoleResolver(new Map()), sourcePath: absolutePath };
  }

  let parsed: unknown;
  try {
    parsed = JSON.parse(raw);
  } catch (err) {
    throw new EntraGroupMapError(
      `Entra group map at ${absolutePath} is not valid JSON: ${
        err instanceof Error ? err.message : String(err)
      }`,
    );
  }

  if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
    throw new EntraGroupMapError(
      `Entra group map at ${absolutePath} must be a JSON object keyed on group GUID.`,
    );
  }

  const stringified: Record<string, string> = {};
  for (const [key, value] of Object.entries(parsed as Record<string, unknown>)) {
    if (typeof value !== 'string') {
      throw new EntraGroupMapError(
        `Entra group map entry "${key}" in ${absolutePath} must be a string (the role slug); got ${typeof value}.`,
      );
    }
    stringified[key] = value;
  }

  const map = parseEntraGroupMap(stringified);
  return { resolver: new EntraGroupToRoleResolver(map), sourcePath: absolutePath };
}
