/*
  Scopes for AI agent users — least privilege, declared once, provisioned by script.

  Why this exists: `provisionSandbox.ts` granted `[12, 122, 262, 32, 111, 251]` —
  opaque numeric ids, one fixed scope, one user per environment. Three failures
  in the 2026-07/08 sessions came straight from that:

    1. Production `ai_agent` — documented everywhere as READ-ONLY — actually held
       create/modify on invoices and thirdparties, and DELETE on proposals.
    2. Writing a payment to production required granting a right by hand and
       revoking it after. Twice. A privilege held by nobody is safer than a
       privilege granted ad hoc under time pressure.
    3. A sandbox refresh wiped the agent's proposal rights because they had been
       granted manually and lived nowhere in code.

  The model: one user per (environment × scope), never a single all-powerful
  agent. A session is handed the credentials matching what it is allowed to do,
  and nothing else.

  Rule that admits no exception: **no scope grants DELETE.** The ledger is
  append-only — a wrong entry is offset by a credit note, never removed. Deletion
  stays with the human admin.
*/

/** A scope is a set of Dolibarr permission ids, with the reason it exists. */
export interface Scope {
  /** What this scope is for, in one line — read by the operator at provisioning. */
  purpose: string;
  /** Which environments may hold a user with this scope. */
  environments: ReadonlyArray<"sandbox" | "production">;
  /** Dolibarr permission ids (see PERMISSION_LABELS for what each one is). */
  rights: ReadonlyArray<number>;
}

/**
 * Dolibarr permission ids used below, with their fr_FR label — so a reader can
 * audit a scope without opening the Dolibarr admin UI.
 *
 * VERIFY BEFORE TRUSTING: these ids come from the running instance
 * (`/user/perms.php`) and are stable per Dolibarr version, not across versions.
 * `provisionAiUser.ts --audit` prints the live labels next to each granted id.
 */
export const PERMISSION_LABELS: Readonly<Record<number, string>> = {
  // --- lecture / export (le socle read) ---
  11: "Lire les factures clients",
  16: "Exporter les factures clients",
  21: "Consulter les propositions commerciales",
  28: "Exporter les propositions commerciales",
  31: "Lire les comptes bancaires",
  41: "Lire les projets et les tâches",
  45: "Exporter les projets",
  91: "Lire les charges fiscales ou sociales et paiement de TVA",
  94: "Exporter les charges fiscales ou sociales",
  111: "Lire les factures fournisseurs",
  121: "Consulter les tiers",
  126: "Exporter les tiers",
  141: "Lire tous les projets et tâches",
  161: "Lire les contrats/abonnements",
  167: "Exporter les contrats",
  251: "Lire les utilisateurs",
  262: "Étendre l'accès à TOUS les tiers et leurs objets (voir_tous)",
  281: "Consulter les contacts",
  358: "Exporter les utilisateurs",
  531: "Consulter les services",
  771: "Lire les notes de frais",
  779: "Exporter les notes de frais",
  1181: "Consulter les fournisseurs",
  1182: "Consulter les commandes fournisseurs",
  1191: "Exporter les commandes fournisseurs",
  1201: "Récupérer le résultat d'un export",
  1231: "Lire les factures (et paiements) fournisseurs",
  1236: "Exporter les factures fournisseur",
  1321: "Exporter les factures clients, attributs et règlements",
  2401: "Lire ses propres actions/événements",
  2411: "Lire les actions/événements des autres",
  2414: "Exporter les événements des autres",
  2501: "Lire/récupérer les documents",
  3201: "Lire les événements archivés et leurs empreintes",
  50411: "Lire les opérations du Grand livre",
  40001: "Lire les devises et leurs taux",

  // --- écriture (jamais dans le scope read) ---
  12: "Créer/modifier les factures clients",
  14: "Émettre des paiements sur les factures clients",
  22: "Créer/modifier les propositions commerciales",
  32: "Créer/modifier les comptes bancaires",
  122: "Créer/modifier les tiers",
  130: "Créer/modifier les informations de paiement des tiers",
  282: "Créer/modifier les contacts",
  1202: "Créer/modifier un export",
  1232: "Créer les factures fournisseur",
  2402: "Créer/modifier des actions/événements",
  2503: "Soumettre ou supprimer des documents",
  162: "Créer/modifier les contrats/abonnements",
  50401: "Lier les produits et factures avec des comptes comptables",

  // --- suppression : JAMAIS accordée par aucun scope ---
  // 15   Supprimer les factures clients
  // 27   Supprimer les propositions commerciales
  // 125  Supprimer les tiers
  // 2403 Supprimer des actions/événements
} as const;

/**
 * Le socle de lecture — établi par audit de l'instance de production, pas
 * deviné : c'est l'ensemble exact des droits de lecture/export dont les skills
 * se servent aujourd'hui (factures clients et fournisseurs, tiers, contacts,
 * documents, contrats, projets, banque, grand livre, TVA).
 *
 * `262` (voir_tous) est indispensable : sans lui, les endpoints de liste
 * renvoient des tableaux vides plutôt qu'un 403 — le piège documenté par la
 * skill `dolibarr`.
 */
const READ_ONLY: ReadonlyArray<number> = [
  11, 16, 21, 28, 31, 41, 45, 91, 94, 111, 121, 126, 141, 161, 167, 251, 262,
  281, 358, 531, 771, 779, 1181, 1182, 1191, 1201, 1231, 1236, 1321, 2401,
  2411, 2414, 2501, 3201, 40001, 50411,
];

export const SCOPES: Readonly<Record<string, Scope>> = {
  /**
   * The default. Every agent session that only needs to look at the books gets
   * this one — audits, reconciliation reads, TVA preparation, reporting.
   * Valid on production BECAUSE it cannot change anything.
   */
  read: {
    purpose: "Lecture seule — audits, réconciliation, préparation TVA, reporting",
    environments: ["sandbox", "production"],
    rights: READ_ONLY,
  },

  /**
   * Rehearsals. Broad write on the sandbox, because that is what the sandbox is
   * for: a disposable copy where a change-set is tried before a human sees it.
   * Refused on production by the environment list AND by the host guard.
   */
  "sandbox-write": {
    purpose: "Répétition d'un change-set sur la sandbox (factures, tiers, produits, propositions, règlements)",
    environments: ["sandbox"],
    rights: [...READ_ONLY, 12, 14, 22, 32, 122, 130, 162, 282, 1232, 2503],
  },

  /**
   * The narrow production writer, used ONLY by the gated promote apply, and only
   * for what that step actually does: create/modify invoices, attach payments.
   * No product creation, no proposals — those are rehearsed then applied by a
   * human when they are genuinely needed.
   * `122` (thirdparties) WAS excluded on that same reasoning, and the reasoning
   * did not survive contact: a supplier invoice needs a supplier, so the scope
   * could create the document but not the party it names. The exclusion turned
   * every new supplier into a manual step in the middle of a gated flow — which
   * is where discipline erodes.
   * `162` (contracts) IS included: recording the engagement behind an invoice is
   * what the piste d'audit fiable asks for, and it recurs (Darnis, then KM).
   *
   * Provisioning this user is itself a deliberate act: `--env production`
   * requires the explicit opt-in in guard.ts.
   */
  "prod-write": {
    purpose: "Écriture de production, gated — création/modification de factures et rattachement de règlements",
    environments: ["production"],
    // 2503 est nécessaire à builddoc (régénérer le PDF d'une facture modifiée).
    // Dolibarr le livre en bundle « soumettre OU supprimer » : on ne peut pas
    // avoir l'un sans l'autre. C'est la seule capacité de suppression du modèle,
    // et elle est confinée au writer de production, gated par le promote.
    // 1232 (factures fournisseur) : oubli initial. Le scope dit « création/
    // modification de factures » — les factures fournisseur en sont, et leur
    // saisie est l'opération la plus courante du back-office. Ajouté après un
    // 403 en production sur la facture Darnis F1048.
    //
    // 122 (tiers) : ajouté sur décision de l'opérateur du 2026-08-15, après un
    // second 403 — cette fois sur Anthropic PBC, entité américaine distincte
    // d'Anthropic Ireland, dont la facture ne pouvait être saisie faute de
    // pouvoir créer le fournisseur qu'elle nomme.
    //
    // CE QUE CE DROIT COÛTE, et il faut le dire : Dolibarr le libelle
    // « Créer/MODIFIER les tiers ». Il n'existe pas de droit de création seule.
    // Le writer de production peut donc désormais altérer l'identité de
    // n'importe quel tiers existant — nom, adresse, identifiants fiscaux — et
    // non seulement en créer. C'est la contrepartie assumée : sans lui, toute
    // facture d'un fournisseur nouveau s'arrête au milieu du flux gated.
    // La suppression (125) reste exclue, comme dans tous les scopes.
    rights: [...READ_ONLY, 12, 14, 122, 162, 1232, 2503],
  },
} as const;

export type ScopeName = keyof typeof SCOPES;

/**
 * Conventional login for a given scope + environment. One user per pair.
 *
 * The scope name is kept verbatim, so `prod-write` on production reads
 * `ai_agent_prod_prod_write`. Redundant, deliberately left alone: renaming a
 * provisioned production credential means creating a second privileged user and
 * repointing the promote flow — churn for cosmetics. Don't "fix" it.
 */
export function loginFor(scope: string, env: "sandbox" | "production"): string {
  return env === "sandbox" ? `ai_agent_sandbox_${scope.replace(/-/g, "_")}`
                           : `ai_agent_prod_${scope.replace(/-/g, "_")}`;
}

/** Fail loudly rather than provision something nobody declared. */
export function resolveScope(name: string, env: "sandbox" | "production"): Scope {
  const s = SCOPES[name];
  if (!s) {
    throw new Error(
      `unknown scope '${name}'. Declared scopes: ${Object.keys(SCOPES).join(", ")}`,
    );
  }
  if (!s.environments.includes(env)) {
    throw new Error(
      `scope '${name}' is not allowed on '${env}' (allowed: ${s.environments.join(", ")}).\n` +
        "  Scopes are per-environment by design — a sandbox writer has no business holding\n" +
        "  production credentials, and vice versa.",
    );
  }
  return s;
}
