<?php

declare(strict_types=1);

namespace App\Shared\Infrastructure\Database;

/**
 * Catalogue centralise des descriptions SQL (`COMMENT ON TABLE` /
 * `COMMENT ON COLUMN`) appliquees aux tables metier de Starseed.
 *
 * Source unique de verite, utilisee par :
 * - `migrations/Version20260528120000.php` : retrofit initial des tables
 *   pre-existantes (ERP-67).
 * - `App\Module\Core\Infrastructure\Console\ApplyColumnCommentsCommand` :
 *   reapplique les commentaires apres `doctrine:schema:update --force` en
 *   environnement de test (cf. commentaire de `test-db-setup` dans le
 *   `makefile`). Doctrine ORM ne conservant pas les commentaires absents
 *   du mapping PHP, on les rejoue depuis ce catalogue.
 *
 * Pour ajouter ou modifier un commentaire :
 * - Mettre a jour `comments()` ci-dessous.
 * - La migration retrofit pose la valeur initiale, la commande la rejoue
 *   en boucle. Toute future colonne doit etre documentee dans sa propre
 *   migration (cf. CLAUDE.md regle ABSOLUE n°12) — ce catalogue ne sert
 *   qu'au retrofit + au workaround schema:update.
 *
 * Convention : description en francais, ≤ 200 caracteres, semantique du
 * champ + contraintes / lien RG si pertinent. La cle speciale `_table` est
 * appliquee a la table elle-meme (`COMMENT ON TABLE`).
 */
final class ColumnCommentsCatalog
{
    /**
     * @return array<string, array<string, string>>
     */
    public static function comments(): array
    {
        return [
            'audit_log' => [
                '_table'       => "Journal d'audit append-only — trace toutes les modifications BDD sur entites annotees #[Auditable]. Lecture seule via API.",
                'id'           => "UUID v7 — identifiant de la ligne d'audit (genere en PHP, ordre temporel garanti).",
                'entity_type'  => "Type d'entite auditee au format module.Entity (ex: core.User, commercial.Client) — evite les collisions inter-modules.",
                'entity_id'    => "Identifiant de l'entite auditee (supporte INT et UUID — stocke en varchar pour rester generique).",
                'action'       => "Type d'operation auditee : 'create', 'update' ou 'delete'.",
                'changes'      => 'Snapshot complet pour create/delete, diff {champ: {old, new}} pour update. Cles sensibles filtrees (password, token, secret).',
                'performed_by' => "Username de l'auteur de l'action (denormalise, survit a la suppression du user) — vaut 'system' en CLI.",
                'performed_at' => "Horodatage UTC de l'action auditee.",
                'ip_address'   => "Adresse IP de l'auteur (IPv4/IPv6) — null hors contexte HTTP.",
                'request_id'   => "UUID v4 de la requete HTTP — regroupe les changements d'un meme flush, facilite la correlation logs.",
            ],

            'category' => [
                '_table'           => 'Categories M0 — referentiel type par category_type, soft-delete via deleted_at, unicite (LOWER(name), category_type_id) parmi les actifs.',
                'id'               => 'Identifiant interne auto-incremente.',
                'name'             => 'Libelle de la categorie (≤ 120 caracteres) — unique par type parmi les actifs (RG-1.06).',
                'code'             => 'Code technique stable (slug MAJUSCULE du nom, ≤ 50) — unique parmi les actifs (uq_category_code). Fige a la creation. DISTRIBUTEUR/COURTIER pilotent RG-1.03/1.29.',
                'category_type_id' => 'Reference au type de la categorie — FK -> category_type.id, ON DELETE RESTRICT (un type ne peut etre supprime tant qu il a des categories).',
                'deleted_at'       => 'Horodatage UTC du soft-delete (archivage logique) — null si la categorie est active.',
            ] + self::timestampableBlamableComments(),

            'category_type' => [
                '_table' => 'Referentiel statique des types de categories — code technique stable + libelle FR.',
                'id'     => 'Identifiant interne auto-incremente.',
                'code'   => 'Code technique stable du type (snake_case, ≤ 40 caracteres) — unique, utilise dans le code et les configurations.',
                'label'  => 'Libelle affichable du type (FR, ≤ 120 caracteres).',
            ],

            'permission' => [
                '_table' => 'Referentiel des permissions RBAC — codes au format module.resource[.subresource].action, synchronise par app:sync-permissions.',
                'id'     => 'Identifiant interne auto-incremente.',
                'code'   => 'Code RBAC au format module.resource[.subresource].action — unique, synchronise par app:sync-permissions.',
                'label'  => 'Libelle affichable de la permission (FR).',
                'module' => 'Identifiant du module proprietaire de la permission (snake_case, ex: core, commercial).',
                'orphan' => "Drapeau permission orpheline — vrai quand son module declarant a ete supprime, masquee de l'interface RBAC.",
            ],

            'role' => [
                '_table'      => 'Referentiel des roles RBAC — agregent un ensemble de permissions, attribues aux utilisateurs.',
                'id'          => 'Identifiant interne auto-incremente.',
                'code'        => 'Code technique stable du role (snake_case) — utilise dans le code (ex: admin, user). Unique.',
                'label'       => 'Libelle affichable du role (FR).',
                'description' => 'Description longue du role (optionnelle).',
                'is_system'   => "Drapeau role systeme — bloque la suppression et la modification du code via l'interface.",
            ],

            'role_permission' => [
                '_table'        => 'Table de jointure roles <-> permissions (ManyToMany).',
                'role_id'       => 'FK -> role.id, ON DELETE CASCADE — role qui porte la permission.',
                'permission_id' => 'FK -> permission.id, ON DELETE CASCADE — permission attribuee au role.',
            ],

            'site' => [
                '_table'      => 'Sites geographiques — perimetre de scoping multi-site, attribues aux utilisateurs via user_site.',
                'id'          => 'Identifiant interne auto-incremente.',
                'name'        => 'Nom du site (≤ 100 caracteres).',
                'city'        => 'Ville du site (≤ 100 caracteres).',
                'postal_code' => 'Code postal (chaine ≤ 20 caracteres) — VARCHAR pour gerer les zeros initiaux et les formats internationaux.',
                'color'       => "Code couleur hexadecimal (#RRGGBB) — differenciation visuelle dans l'UI.",
                'street'      => "Numero et voie de l'adresse (≤ 200 caracteres).",
                'complement'  => "Complement d'adresse (etage, batiment...) — optionnel.",
                'created_at'  => 'Horodatage UTC de creation de la ligne — rempli par TimestampableBlamableSubscriber au prePersist.',
                'updated_at'  => 'Horodatage UTC de derniere modification — rempli par TimestampableBlamableSubscriber au preUpdate.',
            ],

            'user' => [
                '_table'          => 'Comptes utilisateurs Starseed — authentification JWT, RBAC via roles et permissions directes.',
                'id'              => 'Identifiant interne auto-incremente.',
                'username'        => 'Identifiant de connexion (≤ 100 caracteres) — unique.',
                'password'        => 'Hash du mot de passe (algorithme courant Symfony) — exclu de l audit via #[AuditIgnore].',
                'created_at'      => 'Horodatage UTC de creation du compte — rempli manuellement dans le constructeur (pas via TimestampableBlamableSubscriber).',
                'is_admin'        => 'Drapeau super-administrateur — bypass complet RBAC. Faux par defaut.',
                'current_site_id' => "Site actuellement selectionne par l'utilisateur (contexte de session) — FK -> site.id, ON DELETE SET NULL.",
            ],

            'user_permission' => [
                '_table'        => 'Table de jointure utilisateurs <-> permissions directes (hors role).',
                'user_id'       => 'FK -> user.id, ON DELETE CASCADE — utilisateur destinataire de la permission directe.',
                'permission_id' => 'FK -> permission.id, ON DELETE CASCADE — permission accordee individuellement.',
            ],

            'user_role' => [
                '_table'  => 'Table de jointure utilisateurs <-> roles (ManyToMany).',
                'user_id' => 'FK -> user.id, ON DELETE CASCADE — utilisateur portant le role.',
                'role_id' => 'FK -> role.id, ON DELETE CASCADE — role attribue a l utilisateur.',
            ],

            'user_site' => [
                '_table'  => 'Table de jointure utilisateurs <-> sites accessibles — gere le scoping multi-site (un user ne voit que les donnees de ses sites).',
                'user_id' => 'FK -> user.id, ON DELETE CASCADE — utilisateur ayant acces au site.',
                'site_id' => 'FK -> site.id, ON DELETE CASCADE — site accessible par l utilisateur.',
            ],

            // === M1 Commercial (ERP-53/54) — miroir des COMMENT de la migration
            // Version20260601000000 pour le chemin schema:update (dev/test). ===

            'tva_mode' => [
                '_table'   => 'Referentiel des modes de TVA appliques a un client (France, Export, Intracom).',
                'id'       => 'Identifiant interne auto-incremente.',
                'code'     => 'Code technique stable (UPPER_SNAKE, ≤ 30 caracteres) — unique, utilise par le code metier.',
                'label'    => 'Libelle affichable (FR, ≤ 120 caracteres).',
                'position' => 'Ordre d affichage croissant dans les selecteurs (tri position ASC puis label ASC).',
            ],

            'payment_delay' => [
                '_table'   => 'Referentiel des delais de reglement (15 jours, 30 jours, a reception).',
                'id'       => 'Identifiant interne auto-incremente.',
                'code'     => 'Code technique stable (UPPER_SNAKE, ≤ 30 caracteres) — unique, utilise par le code metier.',
                'label'    => 'Libelle affichable (FR, ≤ 120 caracteres).',
                'position' => 'Ordre d affichage croissant dans les selecteurs (tri position ASC puis label ASC).',
            ],

            'payment_type' => [
                '_table'   => 'Referentiel des types de reglement (virement, LCR, cheque, non soumise).',
                'id'       => 'Identifiant interne auto-incremente.',
                'code'     => 'Code technique stable (UPPER_SNAKE, ≤ 30 caracteres) — unique, utilise par le code metier.',
                'label'    => 'Libelle affichable (FR, ≤ 120 caracteres).',
                'position' => 'Ordre d affichage croissant dans les selecteurs (tri position ASC puis label ASC).',
            ],

            'bank' => [
                '_table'   => 'Referentiel des banques selectionnables pour le reglement par virement.',
                'id'       => 'Identifiant interne auto-incremente.',
                'code'     => 'Code technique stable (UPPER_SNAKE, ≤ 30 caracteres) — unique, utilise par le code metier.',
                'label'    => 'Libelle affichable (FR, ≤ 120 caracteres).',
                'position' => 'Ordre d affichage croissant dans les selecteurs (tri position ASC puis label ASC).',
            ],

            'client' => [
                '_table'       => 'Repertoire clients (M1 Commercial) — entites archivables (is_archived) et soft-deletables (deleted_at, HP M2).',
                'id'           => 'Identifiant interne auto-incremente.',
                'company_name' => 'Raison sociale (stockee en MAJUSCULES, RG-1.18). Unique case-insensitive parmi les actifs non archives/non supprimes (RG-1.16, uq_client_company_name_active).',
                // Contact principal inline supprime (refonte contact) : first_name,
                // last_name, phone_primary, phone_secondary, email vivent desormais
                // uniquement sur client_contact.
                'distributor_id'   => 'FK auto-referente vers un client porteur de la categorie DISTRIBUTEUR — exclusive avec broker_id (RG-1.03, chk_client_distrib_or_broker). FK -> client.id, ON DELETE SET NULL.',
                'broker_id'        => 'FK auto-referente vers un client porteur de la categorie COURTIER — exclusive avec distributor_id (RG-1.03). FK -> client.id, ON DELETE SET NULL.',
                'triage_service'   => 'Drapeau service triage active pour le client. Faux par defaut.',
                'description'      => 'Onglet Information : description libre. Obligatoire pour le role Commerciale (RG-1.04), optionnel sinon.',
                'competitors'      => 'Onglet Information : concurrents identifies (texte libre ≤ 255). Obligatoire role Commerciale (RG-1.04).',
                'founded_at'       => 'Onglet Information : date de creation de l entreprise. Obligatoire role Commerciale (RG-1.04).',
                'employees_count'  => 'Onglet Information : effectif (entier >= 0). Obligatoire role Commerciale (RG-1.04).',
                'revenue_amount'   => 'Onglet Information : chiffre d affaires (NUMERIC 15,2). Obligatoire role Commerciale (RG-1.04).',
                'director_name'    => 'Onglet Information : nom du dirigeant. Obligatoire role Commerciale (RG-1.04).',
                'profit_amount'    => 'Onglet Information : resultat / benefice (NUMERIC 15,2). Obligatoire role Commerciale (RG-1.04).',
                'siren'            => 'Onglet Comptabilite : SIREN (9 chiffres attendus). NON unique — peut etre partage entre etablissements (RG-1.15 supprimee, Q4).',
                'account_number'   => 'Onglet Comptabilite : numero de compte comptable du client.',
                'tva_mode_id'      => 'Onglet Comptabilite : mode de TVA applique — FK -> tva_mode.id, ON DELETE RESTRICT.',
                'n_tva'            => 'Onglet Comptabilite : numero de TVA intracommunautaire.',
                'payment_delay_id' => 'Onglet Comptabilite : delai de reglement — FK -> payment_delay.id, ON DELETE RESTRICT.',
                'payment_type_id'  => 'Onglet Comptabilite : type de reglement — FK -> payment_type.id, ON DELETE RESTRICT. Code LCR impose >= 1 RIB (RG-1.13), VIREMENT impose une banque (RG-1.12).',
                'bank_id'          => 'Onglet Comptabilite : banque — FK -> bank.id, ON DELETE RESTRICT. Obligatoire si payment_type = VIREMENT (RG-1.12).',
                'is_archived'      => 'Drapeau fonctionnel d archivage — masque par defaut dans la liste. Bascule via permission commercial.clients.archive (RG-1.22/23).',
                'archived_at'      => 'Horodatage de l archivage — pose quand is_archived passe a vrai, remis a null a la restauration (RG-1.22/23).',
                'deleted_at'       => 'Horodatage du soft-delete technique (HP M2) — non expose par l API au M1. Null = ligne active.',
            ] + self::timestampableBlamableComments(),

            'client_category' => [
                '_table'      => 'Jointure M2M client <-> category (Catalog) — categories metier du client (au moins une obligatoire).',
                'client_id'   => 'FK -> client.id, ON DELETE CASCADE — client porteur de la categorie.',
                'category_id' => 'FK -> category.id, ON DELETE RESTRICT — categorie rattachee au client.',
            ],

            'client_contact' => [
                '_table'          => 'Contacts d un client (1:n) — au moins firstName OU lastName par contact (RG-1.05).',
                'id'              => 'Identifiant interne auto-incremente.',
                'client_id'       => 'FK -> client.id, ON DELETE CASCADE — client proprietaire du contact.',
                'first_name'      => 'Prenom du contact (capitalise serveur). first_name OU last_name obligatoire (RG-1.05, chk_client_contact_name).',
                'last_name'       => 'Nom du contact (capitalise serveur). first_name OU last_name obligatoire (RG-1.05, chk_client_contact_name).',
                'job_title'       => 'Fonction / intitule de poste du contact (≤ 120 caracteres).',
                'phone_primary'   => 'Telephone principal du contact — chiffres uniquement (RG-1.20).',
                'phone_secondary' => 'Telephone secondaire du contact — chiffres uniquement (RG-1.20).',
                'email'           => 'Email du contact (lowercase serveur, RG-1.21).',
                'position'        => 'Ordre d affichage du contact dans la liste du client (croissant).',
            ] + self::timestampableBlamableComments(),

            'client_address' => [
                '_table'            => 'Adresses d un client (1:n) — prospect exclusif de livraison/facturation (RG-1.06/07/08), >= 1 site rattache (RG-1.10).',
                'id'                => 'Identifiant interne auto-incremente.',
                'client_id'         => 'FK -> client.id, ON DELETE CASCADE — client proprietaire de l adresse.',
                'is_prospect'       => 'Adresse de prospection — exclusive de is_delivery/is_billing (RG-1.06/07/08, chk_client_address_prospect_exclusive). Faux par defaut.',
                'is_delivery'       => 'Adresse de livraison. Exclusive de is_prospect. Faux par defaut.',
                'is_billing'        => 'Adresse de facturation. Exclusive de is_prospect. Impose billing_email (RG-1.11). Faux par defaut.',
                'country'           => 'Pays de l adresse — defaut France.',
                'postal_code'       => 'Code postal (4-5 chiffres attendus, RG-1.09).',
                'city'              => 'Ville — preremplie depuis le code postal via API BAN cote front (RG-1.09).',
                'street'            => 'Numero et voie de l adresse.',
                'street_complement' => 'Complement d adresse (etage, batiment...) — optionnel.',
                'billing_email'     => 'Email de facturation — obligatoire si is_billing, null sinon (RG-1.11, chk_client_address_billing_email).',
                'position'          => 'Ordre d affichage de l adresse dans la liste du client (croissant).',
            ] + self::timestampableBlamableComments(),

            'client_address_site' => [
                '_table'            => 'Jointure M2M client_address <-> site (Sites) — sites desservis par l adresse (>= 1 obligatoire, RG-1.10).',
                'client_address_id' => 'FK -> client_address.id, ON DELETE CASCADE — adresse concernee.',
                'site_id'           => 'FK -> site.id, ON DELETE RESTRICT — site rattache a l adresse.',
            ],

            'client_address_contact' => [
                '_table'            => 'Jointure M2M client_address <-> client_contact — contacts associes a une adresse.',
                'client_address_id' => 'FK -> client_address.id, ON DELETE CASCADE — adresse concernee.',
                'client_contact_id' => 'FK -> client_contact.id, ON DELETE CASCADE — contact associe a l adresse.',
            ],

            'client_address_category' => [
                '_table'            => 'Jointure M2M client_address <-> category — categories d adresse (types SECTEUR/AUTRE uniquement, RG-1.29).',
                'client_address_id' => 'FK -> client_address.id, ON DELETE CASCADE — adresse concernee.',
                'category_id'       => 'FK -> category.id, ON DELETE RESTRICT — categorie d adresse (type SECTEUR ou AUTRE, RG-1.29).',
            ],

            'client_rib' => [
                '_table'    => 'Coordonnees bancaires d un client (1:n) — >= 1 RIB obligatoire si payment_type = LCR (RG-1.13). Tous les champs audites (pas d AuditIgnore).',
                'id'        => 'Identifiant interne auto-incremente.',
                'client_id' => 'FK -> client.id, ON DELETE CASCADE — client proprietaire du RIB.',
                'label'     => 'Libelle du RIB (ex: compte principal).',
                'bic'       => 'Code BIC/SWIFT de la banque (8 ou 11 caracteres).',
                'iban'      => 'IBAN du compte (≤ 34 caracteres).',
                'position'  => 'Ordre d affichage du RIB dans la liste du client (croissant).',
            ] + self::timestampableBlamableComments(),

            // === M2 Commercial (ERP-85) — miroir des COMMENT de la migration
            // Version20260605130000 pour le chemin schema:update (dev/test). ===

            'supplier' => [
                '_table'           => 'Repertoire fournisseurs (M2 Commercial) — entites archivables (is_archived) et soft-deletables (deleted_at, HP M3).',
                'id'               => 'Identifiant interne auto-incremente.',
                'company_name'     => 'Raison sociale du fournisseur (stockee en MAJUSCULES). Unique case-insensitive parmi les actifs non archives/non supprimes (uq_supplier_company_name_active, § 2.6).',
                'description'      => 'Onglet Information : description libre. Obligatoire pour le role Commerciale (RG-2.03), optionnel sinon.',
                'competitors'      => 'Onglet Information : concurrents identifies (texte libre ≤ 255). Obligatoire role Commerciale (RG-2.03).',
                'founded_at'       => 'Onglet Information : date de creation de l entreprise. Obligatoire role Commerciale (RG-2.03).',
                'employees_count'  => 'Onglet Information : effectif (entier >= 0). Obligatoire role Commerciale (RG-2.03).',
                'revenue_amount'   => 'Onglet Information : chiffre d affaires (NUMERIC 15,2). Obligatoire role Commerciale (RG-2.03).',
                'director_name'    => 'Onglet Information : nom du dirigeant. Obligatoire role Commerciale (RG-2.03).',
                'profit_amount'    => 'Onglet Information : resultat / benefice (NUMERIC 15,2). Obligatoire role Commerciale (RG-2.03).',
                'volume_forecast'  => 'Onglet Information : volume previsionnel (entier >= 0) — specifique fournisseur. Obligatoire role Commerciale (RG-2.03).',
                'siren'            => 'Onglet Comptabilite : SIREN (9 chiffres attendus). NON unique — peut etre partage entre etablissements (§ 2.6).',
                'account_number'   => 'Onglet Comptabilite : numero de compte comptable du fournisseur.',
                'tva_mode_id'      => 'Onglet Comptabilite : mode de TVA applique — FK -> tva_mode.id (referentiel partage M1), ON DELETE RESTRICT.',
                'n_tva'            => 'Onglet Comptabilite : numero de TVA intracommunautaire.',
                'payment_delay_id' => 'Onglet Comptabilite : delai de reglement — FK -> payment_delay.id (M1), ON DELETE RESTRICT.',
                'payment_type_id'  => 'Onglet Comptabilite : type de reglement — FK -> payment_type.id (M1), ON DELETE RESTRICT. Pilote RG-2.07 (Banque si VIREMENT) et RG-2.08 (RIB).',
                'bank_id'          => 'Onglet Comptabilite : banque — FK -> bank.id (M1), ON DELETE RESTRICT. Obligatoire ssi payment_type = VIREMENT (RG-2.07), null sinon.',
                'is_archived'      => 'Drapeau fonctionnel d archivage — masque par defaut dans la liste. Bascule via permission commercial.suppliers.archive.',
                'archived_at'      => 'Horodatage de l archivage — pose quand is_archived passe a vrai, remis a null a la restauration.',
                'deleted_at'       => 'Horodatage du soft-delete technique (HP M3) — non expose par l API au M2. Null = ligne active.',
            ] + self::timestampableBlamableComments(),

            'supplier_category' => [
                '_table'      => 'Jointure M2M supplier <-> category (Catalog) — categories de type FOURNISSEUR du fournisseur, au moins une obligatoire (RG-2.10).',
                'supplier_id' => 'FK -> supplier.id, ON DELETE CASCADE — fournisseur porteur de la categorie.',
                'category_id' => 'FK -> category.id, ON DELETE RESTRICT — categorie de type FOURNISSEUR rattachee au fournisseur (RG-2.10).',
            ],

            'supplier_contact' => [
                '_table'          => 'Contacts d un fournisseur (1:n) — au moins firstName OU lastName par contact (RG-2.04).',
                'id'              => 'Identifiant interne auto-incremente.',
                'supplier_id'     => 'FK -> supplier.id, ON DELETE CASCADE — fournisseur proprietaire du contact.',
                'first_name'      => 'Prenom du contact (capitalise serveur). first_name OU last_name obligatoire (RG-2.04, chk_supplier_contact_name).',
                'last_name'       => 'Nom du contact (capitalise serveur). first_name OU last_name obligatoire (RG-2.04, chk_supplier_contact_name).',
                'job_title'       => 'Fonction / intitule de poste du contact (≤ 120 caracteres).',
                'phone_primary'   => 'Telephone principal du contact — chiffres uniquement (normalisation serveur).',
                'phone_secondary' => 'Telephone secondaire du contact — chiffres uniquement (normalisation serveur).',
                'email'           => 'Email du contact (lowercase serveur).',
                'position'        => 'Ordre d affichage du contact dans la liste du fournisseur (croissant).',
            ] + self::timestampableBlamableComments(),

            'supplier_address' => [
                '_table'            => 'Adresses d un fournisseur (1:n) — type PROSPECT/DEPART/RENDU exclusif (RG-2.09), >= 1 site rattache (RG-2.06).',
                'id'                => 'Identifiant interne auto-incremente.',
                'supplier_id'       => 'FK -> supplier.id, ON DELETE CASCADE — fournisseur proprietaire de l adresse.',
                'address_type'      => 'Type d adresse : PROSPECT | DEPART | RENDU (radio exclusif par construction — RG-2.09, chk_supplier_address_type).',
                'country'           => 'Pays de l adresse — defaut France.',
                'postal_code'       => 'Code postal (4-5 chiffres attendus).',
                'city'              => 'Ville — preremplie depuis le code postal via API BAN cote front.',
                'street'            => 'Numero et voie de l adresse.',
                'street_complement' => 'Complement d adresse (etage, batiment...) — optionnel.',
                'bennes'            => 'Nombre de bennes sur le site fournisseur (entier nullable) — specifique fournisseur.',
                'triage_provider'   => 'Le fournisseur est prestataire de triage sur cette adresse. Faux par defaut.',
                'position'          => 'Ordre d affichage de l adresse dans la liste du fournisseur (croissant).',
            ] + self::timestampableBlamableComments(),

            'supplier_address_site' => [
                '_table'              => 'Jointure M2M supplier_address <-> site (Sites) — sites rattaches a l adresse (>= 1 obligatoire, RG-2.06).',
                'supplier_address_id' => 'FK -> supplier_address.id, ON DELETE CASCADE — adresse concernee.',
                'site_id'             => 'FK -> site.id, ON DELETE RESTRICT — site rattache a l adresse.',
            ],

            'supplier_address_contact' => [
                '_table'              => 'Jointure M2M supplier_address <-> supplier_contact — contacts associes a une adresse.',
                'supplier_address_id' => 'FK -> supplier_address.id, ON DELETE CASCADE — adresse concernee.',
                'supplier_contact_id' => 'FK -> supplier_contact.id, ON DELETE CASCADE — contact associe a l adresse.',
            ],

            'supplier_address_category' => [
                '_table'              => 'Jointure M2M supplier_address <-> category — categories d adresse de type FOURNISSEUR (RG-2.10).',
                'supplier_address_id' => 'FK -> supplier_address.id, ON DELETE CASCADE — adresse concernee.',
                'category_id'         => 'FK -> category.id, ON DELETE RESTRICT — categorie d adresse de type FOURNISSEUR (RG-2.10).',
            ],

            'supplier_rib' => [
                '_table'      => 'Coordonnees bancaires d un fournisseur (1:n) — >= 1 RIB attendu selon le type de reglement (RG-2.08). Tous les champs audites (pas d AuditIgnore).',
                'id'          => 'Identifiant interne auto-incremente.',
                'supplier_id' => 'FK -> supplier.id, ON DELETE CASCADE — fournisseur proprietaire du RIB.',
                'label'       => 'Libelle du RIB (ex: compte principal).',
                'bic'         => 'Code BIC/SWIFT de la banque (8 ou 11 caracteres).',
                'iban'        => 'IBAN du compte (≤ 34 caracteres).',
                'position'    => 'Ordre d affichage du RIB dans la liste du fournisseur (croissant).',
            ] + self::timestampableBlamableComments(),
        ];
    }

    /**
     * Descriptions standardisees pour les 4 colonnes du pattern
     * Timestampable/Blamable (`TimestampableBlamableTrait`).
     *
     * @return array<string, string>
     */
    public static function timestampableBlamableComments(): array
    {
        return [
            'created_at' => 'Horodatage UTC de creation de la ligne — rempli par TimestampableBlamableSubscriber au prePersist.',
            'updated_at' => 'Horodatage UTC de derniere modification — rempli par TimestampableBlamableSubscriber au preUpdate.',
            'created_by' => "ID de l'utilisateur ayant cree la ligne — null hors HTTP (CLI, migration, fixture). FK -> \"user\".id, ON DELETE SET NULL.",
            'updated_by' => "ID de l'utilisateur ayant modifie la ligne en dernier — null hors HTTP. FK -> \"user\".id, ON DELETE SET NULL.",
        ];
    }

    /**
     * Construit la liste des requetes SQL `COMMENT ON TABLE/COLUMN` (en
     * dollar-quoting Postgres `$_$`) a partir du catalogue.
     *
     * @param null|list<string> $onlyTables Restreint la generation a ces tables
     *                                      (utile pour la migration retrofit qui
     *                                      ne doit commenter que les tables deja
     *                                      presentes a son instant T — les tables
     *                                      des modules crees plus tard posent
     *                                      leurs propres COMMENT). null = tout.
     *
     * @return list<string>
     */
    public static function toSqlStatements(?array $onlyTables = null): array
    {
        $allowed = null === $onlyTables ? null : array_fill_keys($onlyTables, true);

        $statements = [];
        foreach (self::comments() as $table => $entries) {
            if (null !== $allowed && !isset($allowed[$table])) {
                continue;
            }

            $quotedTable = self::quoteIdent($table);
            foreach ($entries as $column => $description) {
                if ('_table' === $column) {
                    $statements[] = sprintf('COMMENT ON TABLE %s IS $_$%s$_$', $quotedTable, $description);

                    continue;
                }

                $statements[] = sprintf(
                    'COMMENT ON COLUMN %s.%s IS $_$%s$_$',
                    $quotedTable,
                    self::quoteIdent($column),
                    $description,
                );
            }
        }

        return $statements;
    }

    /**
     * Quote un identifiant SQL avec des guillemets doubles. Necessaire pour
     * la table `user` (mot reserve PG) ; applique a tous par coherence.
     */
    private static function quoteIdent(string $name): string
    {
        return '"'.str_replace('"', '""', $name).'"';
    }
}
