<?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 [
            'uploaded_document' => [
                '_table'            => 'Fichiers televerses (infra generique Shared, ERP-154) — documents immuables (PDF / images), 1er consommateur la Decharge M4.',
                'id'                => 'Identifiant interne auto-incremente.',
                'original_filename' => 'Nom de fichier d origine fourni par le client (≤ 255) — metadonnee d affichage uniquement, jamais utilise pour le stockage disque.',
                'stored_path'       => 'Chemin relatif du fichier sous var/uploads (ex: 2026/06/<hash>.pdf) — nom genere aleatoirement, jamais le nom client.',
                'mime_type'         => 'Type MIME detecte SERVER-SIDE via getMimeType (jamais getClientMimeType, spoofable) — borne a la whitelist FileUploader (PDF + images).',
                'size_bytes'        => 'Taille du fichier en octets — bornee par FileUploader::MAX_SIZE_BYTES.',
                'checksum'          => 'Empreinte SHA-256 du contenu (64 caracteres hex) — controle d integrite + deduplication eventuelle (hors scope).',
                'created_at'        => 'Horodatage UTC du televersement — rempli par FileUploader via l horloge injectee (pas via TimestampableBlamableSubscriber).',
                'created_by'        => 'ID de l utilisateur ayant televerse le fichier — null hors HTTP (CLI, fixture). FK -> "user".id, ON DELETE SET NULL.',
            ],

            '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 — referentiel multi-types via la jonction category_category_type, soft-delete via deleted_at, unicite LOWER(name) GLOBALE parmi les actifs (uq_category_name_active).',
                'id'         => 'Identifiant interne auto-incremente.',
                'name'       => 'Libelle de la categorie (≤ 120 caracteres) — unique GLOBALEMENT parmi les actifs (RG-1.07, uq_category_name_active).',
                '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.',
                '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).',
            ],

            'category_category_type' => [
                '_table'           => 'Jointure M2M category <-> category_type (Catalog) — types portes par la categorie, au moins un obligatoire (RG-1.05).',
                'category_id'      => 'FK -> category.id, ON DELETE CASCADE — categorie portant le type.',
                'category_type_id' => 'FK -> category_type.id, ON DELETE RESTRICT — type rattache (un type ne peut etre supprime tant qu il reste reference).',
            ],

            '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).',
            ],

            'country' => [
                '_table'   => 'Referentiel des pays selectionnables dans les adresses (clients/fournisseurs). Perimetre minimal : code ISO + libelle + ordre (pas de longueurs bancaires/fiscales).',
                'id'       => 'Identifiant interne auto-incremente.',
                'code'     => 'Code pays ISO 3166-1 alpha-2 (2 lettres MAJUSCULES, ex: FR) — unique (uq_country_code), fige a la creation.',
                'name'     => 'Libelle FR du pays (≤ 80 caracteres) — valeur stockee telle quelle dans les adresses (country en chaine libre a ce stade).',
                'position' => 'Ordre d affichage croissant dans le selecteur pays (tri position ASC puis name ASC ; France en tete).',
            ],

            '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. Facultatif.',
                'competitors'      => 'Onglet Information : concurrents identifies (texte libre ≤ 255). Facultatif.',
                'founded_at'       => 'Onglet Information : date de creation de l entreprise. Facultatif.',
                'employees_count'  => 'Onglet Information : effectif (entier >= 0). Facultatif.',
                'revenue_amount'   => 'Onglet Information : chiffre d affaires (NUMERIC 15,2). Facultatif.',
                'director_name'    => 'Onglet Information : nom du dirigeant. Facultatif.',
                'profit_amount'    => 'Onglet Information : resultat / benefice (NUMERIC 15,2). Facultatif.',
                '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) — types prospect / livraison / facturation (exclusivites RG-1.06/07/08) + Courtier / Distributeur autonomes (exclusifs de tout autre usage), >= 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.',
                'is_broker'               => 'Adresse Courtier — type autonome exclusif de tout autre usage (chk_client_address_broker_exclusive). Faux par defaut.',
                'is_distributor'          => 'Adresse Distributeur — type autonome exclusif de tout autre usage (chk_client_address_distributor_exclusive). 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).',
                'billing_email_secondary' => '2e email de facturation, optionnel (max 2). Interdit hors facturation, normalise en minuscules (RG-1.21).',
                '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(),

            // Tables provider* (M3 Technique) — ajoutees au ticket entites (ERP-133),
            // comme l a fait supplier (ERP-86) apres sa migration (ERP-85). En test,
            // `schema:update --force` recree ces tables depuis le mapping ORM (sans
            // COMMENT) ; `app:apply-column-comments` les repose depuis ce catalogue.
            'provider' => [
                '_table'           => 'Repertoire prestataires (M3 Technique) — entites archivables (is_archived) et soft-deletables (deleted_at, HP M4). Pas d onglet Information (≠ supplier).',
                'id'               => 'Identifiant interne auto-incremente.',
                'company_name'     => 'Raison sociale du prestataire (stockee en MAJUSCULES). Unique case-insensitive parmi les actifs non archives/non supprimes (uq_provider_company_name_active, RG-3.10).',
                'siren'            => 'Onglet Comptabilite : SIREN (9 chiffres attendus). NON unique — peut etre partage entre etablissements (RG-3.10).',
                'account_number'   => 'Onglet Comptabilite : numero de compte comptable du prestataire.',
                '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-3.07 (Banque si VIREMENT) et RG-3.08 (RIB).',
                'bank_id'          => 'Onglet Comptabilite : banque — FK -> bank.id (M1), ON DELETE RESTRICT. Obligatoire ssi payment_type = VIREMENT (RG-3.07), null sinon.',
                'is_archived'      => 'Drapeau fonctionnel d archivage — masque par defaut dans la liste. Bascule via permission technique.providers.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 M4) — non expose par l API au M3. Null = ligne active.',
            ] + self::timestampableBlamableComments(),

            'provider_category' => [
                '_table'      => 'Jointure M2M provider <-> category (Catalog) — categories de type PRESTATAIRE du prestataire, au moins une obligatoire (RG-3.09).',
                'provider_id' => 'FK -> provider.id, ON DELETE CASCADE — prestataire porteur de la categorie.',
                'category_id' => 'FK -> category.id, ON DELETE RESTRICT — categorie de type PRESTATAIRE rattachee au prestataire (RG-3.09).',
            ],

            'provider_site' => [
                '_table'      => 'Jointure M2M provider <-> site (Sites) — sites du prestataire, selecteur du formulaire principal, au moins un obligatoire (RG-3.03). Sert le cloisonnement par site (§ 2.13).',
                'provider_id' => 'FK -> provider.id, ON DELETE CASCADE — prestataire porteur du site.',
                'site_id'     => 'FK -> site.id, ON DELETE RESTRICT — site rattache au prestataire (RG-3.03, idx_provider_site_site).',
            ],

            'provider_contact' => [
                '_table'          => 'Contacts d un prestataire (1:n) — au moins le prenom OU le nom rempli (RG-3.04, chk_provider_contact_name).',
                'id'              => 'Identifiant interne auto-incremente.',
                'provider_id'     => 'FK -> provider.id, ON DELETE CASCADE — prestataire proprietaire du contact.',
                'first_name'      => 'Prenom du contact (capitalise serveur). Prenom OU nom obligatoire (RG-3.04, chk_provider_contact_name).',
                'last_name'       => 'Nom du contact (capitalise serveur). Prenom OU nom obligatoire (RG-3.04, chk_provider_contact_name).',
                'job_title'       => 'Fonction / intitule de poste du contact (≤ 120 caracteres). Facultatif — ne suffit plus a valider le contact (RG-3.04).',
                '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 prestataire (croissant).',
            ] + self::timestampableBlamableComments(),

            'provider_address' => [
                '_table'            => 'Adresses d un prestataire (1:n) — >= 1 site rattache (RG-3.05). SANS address_type / bennes / triage_provider (specifiques fournisseur).',
                'id'                => 'Identifiant interne auto-incremente.',
                'provider_id'       => 'FK -> provider.id, ON DELETE CASCADE — prestataire proprietaire de l adresse.',
                'country'           => 'Pays de l adresse — defaut France.',
                'postal_code'       => 'Code postal (4-5 chiffres attendus) — declenche l autocompletion ville via l API BAN cote front (RG-3.06).',
                '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.',
                'position'          => 'Ordre d affichage de l adresse dans la liste du prestataire (croissant).',
            ] + self::timestampableBlamableComments(),

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

            'provider_address_contact' => [
                '_table'              => 'Jointure M2M provider_address <-> provider_contact — contacts associes a une adresse.',
                'provider_address_id' => 'FK -> provider_address.id, ON DELETE CASCADE — adresse concernee.',
                'provider_contact_id' => 'FK -> provider_contact.id, ON DELETE CASCADE — contact associe a l adresse.',
            ],

            'provider_address_category' => [
                '_table'              => 'Jointure M2M provider_address <-> category — categories d adresse de type PRESTATAIRE (RG-3.09).',
                'provider_address_id' => 'FK -> provider_address.id, ON DELETE CASCADE — adresse concernee.',
                'category_id'         => 'FK -> category.id, ON DELETE RESTRICT — categorie d adresse de type PRESTATAIRE (RG-3.09).',
            ],

            'provider_rib' => [
                '_table'      => 'Coordonnees bancaires d un prestataire (1:n) — >= 1 RIB attendu selon le type de reglement (RG-3.08). Tous les champs audites (pas d AuditIgnore).',
                'id'          => 'Identifiant interne auto-incremente.',
                'provider_id' => 'FK -> provider.id, ON DELETE CASCADE — prestataire 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 prestataire (croissant).',
            ] + self::timestampableBlamableComments(),

            // === M4 Transport — referentiel QUALIMAT (ERP-39, mappe lecture seule des ERP-155) ===
            // Mappe par l'entite QualimatCarrier depuis M4 -> retire du schema_filter,
            // donc ses COMMENT sont rejoues par app:apply-column-comments apres schema:update.
            'qualimat_carrier' => [
                '_table'         => "Referentiel des transporteurs agrees QUALIMAT, synchronise quotidiennement depuis l'API qualimat.org (type=operateur_transport).",
                'id'             => 'Cle technique auto-incrementee.',
                'siret'          => 'SIRET normalise (chiffres sans espaces). Cle naturelle de synchro (unique). Source parfois incomplete (longueur variable), non contrainte a 14.',
                'name'           => 'Raison sociale du transporteur (champs Nom = Societe de la source, identiques).',
                'address'        => 'Adresse postale (voie). Nullable.',
                'postal_code'    => 'Code postal. Nullable.',
                'city'           => 'Ville. Nullable.',
                'phone'          => 'Telephone au format source "indicatif|numero" (ex: +33|0608890316). Nullable.',
                'department'     => 'Departement au format source "code - libelle" (ex: 65 - Hautes-Pyrenees). Nullable.',
                'status'         => "Statut d'agrement QUALIMAT (valeurs connues : Audite, Valide, Suspendu). Valeur brute de la source, non contrainte.",
                'validity_date'  => 'Date de fin de validite de la certification (convertie depuis dd/mm/yyyy). Nullable.',
                'is_active'      => 'Faux = transporteur absent du dernier import (soft-delete). Toute ligne non revue par le dernier run passe a FALSE.',
                'last_synced_at' => 'Horodatage du run de synchro ayant vu cette ligne en dernier (soft-delete : last_synced_at < run courant).',
            ],

            // === M4 Transport — repertoire transporteurs (ERP-155/157) ===
            'carrier' => [
                '_table'                => 'Repertoire transporteurs (M4 Transport) — entites editables, archivables (is_archived) et soft-deletables (deleted_at). Distinct du referentiel qualimat_carrier.',
                'id'                    => 'Identifiant interne auto-incremente.',
                'qualimat_carrier_id'   => 'Lien editable vers le referentiel QUALIMAT (saisie assistee RG-4.01). FK -> qualimat_carrier.id, ON DELETE SET NULL : transporteur conserve si la ligne QUALIMAT disparait.',
                'name'                  => 'Raison sociale du transporteur (stockee en MAJUSCULES). Unique case-insensitive parmi les non-archives/non-supprimes (uq_carrier_name_active, RG-4.12 / § 2.6).',
                'certification_type'    => 'Type de certification : QUALIMAT (si lie, lecture seule) ou GMP_PLUS/OVOCOM/COMPTE_PROPRE/AUTRE. AUTRE declenche le champ Decharge (RG-4.02). Null en cas LIOT (RG-4.01).',
                'is_chartered'          => '« Affreter » coche : declenche indexation/benne-fond mouvant/volume, obligatoires (RG-4.03). Faux par defaut.',
                'indexation_rate'       => 'Taux d indexation en pourcentage (NUMERIC 5,2) — renseigne si affrete (RG-4.03).',
                'container_type'        => 'Type de contenant BENNE|FOND_MOUVANT (chk_carrier_container_type) — renseigne si affrete (RG-4.03).',
                'volume_m3'             => 'Volume en m3 (NUMERIC 10,2) — renseigne si affrete (RG-4.03).',
                'discharge_document_id' => 'Document de Decharge (visible si certification_type = AUTRE, RG-4.02). FK -> uploaded_document.id (infra Shared § 2.7), ON DELETE SET NULL.',
                'liot_plates'           => 'Immatriculations LIOT separees par « ; » (cas special nom=LIOT, RG-4.01). Les autres champs sont masques dans ce cas.',
                'is_archived'           => 'Drapeau fonctionnel d archivage — masque par defaut dans la liste. Bascule via permission transport.carriers.archive (Admin seul).',
                '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 — non expose par l API au M4. Null = ligne active.',
            ] + self::timestampableBlamableComments(),

            'carrier_address' => [
                '_table'            => 'Adresses d un transporteur (1:n) — onglet Adresse (M4). Pre-remplie depuis QUALIMAT si applicable (RG-4.05).',
                'id'                => 'Identifiant interne auto-incremente.',
                'carrier_id'        => 'FK -> carrier.id, ON DELETE CASCADE — transporteur proprietaire de l adresse.',
                'country'           => 'Pays de l adresse — defaut France.',
                'postal_code'       => 'Code postal (saisie assistee BAN cote front, RG-4.06).',
                '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.',
                'position'          => 'Ordre d affichage de l adresse dans la liste du transporteur (croissant).',
            ] + self::timestampableBlamableComments(),

            'carrier_contact' => [
                '_table'          => 'Contacts d un transporteur (1:n) — onglet Contact (M4). Au moins un champ rempli (RG-4.08, chk_carrier_contact_filled), max 2 telephones.',
                'id'              => 'Identifiant interne auto-incremente.',
                'carrier_id'      => 'FK -> carrier.id, ON DELETE CASCADE — transporteur proprietaire du contact.',
                'first_name'      => 'Prenom du contact (capitalise serveur). Au moins un champ du contact est requis (RG-4.08).',
                'last_name'       => 'Nom du contact (capitalise serveur). Au moins un champ du contact est requis (RG-4.08).',
                'job_title'       => 'Fonction / intitule de poste du contact (≤ 120 caracteres).',
                'phone_primary'   => 'Telephone principal — chiffres uniquement (normalisation serveur).',
                'phone_secondary' => 'Telephone secondaire — chiffres uniquement (max 2 telephones, RG-4.08).',
                'email'           => 'Email du contact (lowercase serveur).',
                'position'        => 'Ordre d affichage du contact dans la liste du transporteur (croissant).',
            ] + self::timestampableBlamableComments(),

            'carrier_price' => [
                '_table'                     => 'Prix d un transporteur (1:n) — onglet Prix (M4). Branche CLIENT ou FOURNISSEUR selon direction (RG-4.09->4.11, CHECK chk_carrier_price_*).',
                'id'                         => 'Identifiant interne auto-incremente.',
                'carrier_id'                 => 'FK -> carrier.id, ON DELETE CASCADE — transporteur proprietaire du prix.',
                'direction'                  => 'Sens du prix : CLIENT ou FOURNISSEUR (RG-4.09). Pilote l affichage et l obligation des colonnes client_*/supplier_* (RG-4.10/4.11).',
                'client_id'                  => 'Branche CLIENT (RG-4.10) : client concerne. FK -> client.id, ON DELETE RESTRICT. Requis ssi direction = CLIENT.',
                'client_delivery_address_id' => 'Branche CLIENT : adresse de livraison du client. FK -> client_address.id, ON DELETE RESTRICT.',
                'departure_site_id'          => 'Branche CLIENT : adresse de depart = un des 3 sites (86/17/82). FK -> site.id, ON DELETE RESTRICT.',
                'supplier_id'                => 'Branche FOURNISSEUR (RG-4.11) : fournisseur concerne. FK -> supplier.id, ON DELETE RESTRICT. Requis ssi direction = FOURNISSEUR.',
                'supplier_supply_address_id' => 'Branche FOURNISSEUR : adresse d approvisionnement du fournisseur. FK -> supplier_address.id, ON DELETE RESTRICT.',
                'delivery_site_id'           => 'Branche FOURNISSEUR : adresse de livraison = un des 3 sites (86/17/82). FK -> site.id, ON DELETE RESTRICT.',
                'container_type'             => 'Type de contenant BENNE|FOND_MOUVANT (chk_carrier_price_container).',
                'pricing_unit'               => 'Unite de tarification FORFAIT|TONNE (chk_carrier_price_unit).',
                'price'                      => 'Montant du prix (NUMERIC 12,2).',
                'price_state'                => 'Etat du prix : EN_COURS, VALIDE ou NON_VALIDE (chk_carrier_price_state). Affiche dans le tableau Prix.',
                'position'                   => 'Ordre d affichage du prix dans la liste du transporteur (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).'"';
    }
}
