// Server-side FreeIPA LDAP client. Used for the narrow set of
// user-attribute writes that the kit's createAuth flow can't do
// (Keycloak's LDAP federation is READ_ONLY).
//
// Bind is as the `svc-gsc-admin` service account; the realm role
// "User Preferred Language Manager" gives it `write` on the
// `preferredLanguage` attribute of every user entry.
//
// Connection: LDAPS over the internal-gateway Envoy
// (`ldaps.internal-gateway.svc.cluster.local:636`), proxied to
// FreeIPA primary at 172.17.3.100. We accept self-signed TLS for the
// in-cluster proxy chain — the upstream verifies the Envoy cert.

import "server-only";
import { Attribute, Change, Client } from "ldapts";

const URL = process.env.FREEIPA_LDAP_URL ?? "";
const BIND_DN = process.env.FREEIPA_BIND_DN ?? "";
const BIND_PASSWORD = process.env.FREEIPA_BIND_PASSWORD ?? "";
const USERS_BASE_DN =
  process.env.FREEIPA_USERS_BASE_DN ?? "cn=users,cn=accounts,dc=gosec,dc=auth";

export function isConfigured(): boolean {
  return Boolean(URL && BIND_DN && BIND_PASSWORD);
}

async function withClient<T>(fn: (c: Client) => Promise<T>): Promise<T> {
  if (!isConfigured()) {
    throw new Error("FreeIPA LDAP bind not configured (FREEIPA_LDAP_URL / FREEIPA_BIND_DN / FREEIPA_BIND_PASSWORD)");
  }
  const client = new Client({
    url: URL,
    tlsOptions: { rejectUnauthorized: false },
    timeout: 5_000,
    connectTimeout: 5_000,
  });
  try {
    await client.bind(BIND_DN, BIND_PASSWORD);
    return await fn(client);
  } finally {
    try {
      await client.unbind();
    } catch {
      /* best-effort */
    }
  }
}

/**
 * Set the user's `preferredLanguage` attribute.
 *
 * `uid` is the FreeIPA uid (login name) — extract it from the gscSID
 * with {@link uidFromGscSid} when calling from session-scoped code.
 *
 * `language` must already be validated against the app's locale list;
 * this function doesn't gate-keep beyond LDAP-level validation.
 */
export async function setUserPreferredLanguage(uid: string, language: string): Promise<void> {
  if (!/^[a-z0-9._-]{1,64}$/i.test(uid)) {
    throw new Error(`refusing to LDAP-modify suspicious uid: ${uid}`);
  }
  const dn = `uid=${uid},${USERS_BASE_DN}`;
  await withClient(async (c) => {
    await c.modify(dn, [
      new Change({
        operation: "replace",
        modification: new Attribute({
          type: "preferredLanguage",
          values: [language],
        }),
      }),
    ]);
  });
}

/**
 * Pull the FreeIPA uid out of a gscSID like
 * `00000001-019b-0000-0000-b10003fe23f6` → `b10003fe23f6`.
 * The gscSID format is `<customer-8h>-<tenant-4h>-0000-0000-<uid>`,
 * so the uid is everything after the 4th hyphen (uids may contain
 * hyphens themselves; rejoin the rest).
 */
export function uidFromGscSid(gscsid: string): string {
  const parts = gscsid.split("-");
  if (parts.length < 5) {
    throw new Error(`unexpected gscSid format: ${gscsid}`);
  }
  return parts.slice(4).join("-");
}
