<?php

declare(strict_types=1);

namespace DoctrineMigrations;

use App\Shared\Infrastructure\Database\ColumnCommentsCatalog;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

/**
 * M1 — Repertoire clients (ERP-53) : creation de toute la structure BDD du
 * module Commercial (clients + sous-collections + referentiels comptables).
 *
 * Tables creees :
 *  - Referentiels comptables (statiques, seedes ici) : tva_mode, payment_delay,
 *    payment_type, bank.
 *  - Table principale : client (formulaire + Information + Comptabilite +
 *    archive + soft-delete + Timestampable/Blamable).
 *  - Sous-collections : client_category (M2M), client_contact (1:n),
 *    client_address (1:n), client_rib (1:n).
 *  - Jointures de client_address : client_address_site, client_address_contact,
 *    client_address_category.
 *
 * Seed `category_type` (extension M0) : DISTRIBUTEUR / COURTIER / SECTEUR /
 * AUTRE, en `ON CONFLICT (code) DO NOTHING` (idempotent — la table peut deja
 * porter des donnees en prod). En dev/test, les fixtures purgent et re-seedent
 * ces 4 types (cf. CategoryTypeFixtures) ; ce seed migration couvre la prod ou
 * les fixtures ne tournent pas.
 *
 * Namespace racine `DoctrineMigrations` (regle ABSOLUE Starseed n°11) et NON
 * `App\Module\Commercial\...` : avec plusieurs migrations_paths, Doctrine
 * Migrations 3.x trie par FQCN alphabetique (AlphabeticalComparator → strcmp).
 * Un namespace `App\Module\Commercial\...` trierait AVANT `DoctrineMigrations\...`
 * et la migration s'executerait avant la creation de user/category/site sur
 * base vide → echec des FK. Le namespace racine garantit l'ordre par timestamp.
 *
 * Style DDL aligne sur la migration M0 (Version20260527164000) plutot que sur
 * le pseudo-SQL de la spec § 3.2 : `INT GENERATED BY DEFAULT AS IDENTITY` (et
 * non SERIAL), `TIMESTAMP(0) WITHOUT TIME ZONE` (et non TIMESTAMPTZ, car le
 * `TimestampableBlamableTrait` mappe `datetime_immutable`). Garantit que
 * `schema:update` restera un no-op quand les entites arriveront (ticket ERP-54).
 *
 * Decision Q4 (29/05/2026) : unicite metier sur le NOM DE SOCIETE uniquement.
 * Pas d'index unique sur siren ni email (RG-1.15 / RG-1.17 supprimees).
 *
 * Chaque colonne porte un `COMMENT ON COLUMN` (regle ABSOLUE n°12, garde-fou
 * ColumnsHaveSqlCommentTest). Les tables n'etant pas encore mappees par l'ORM,
 * ces commentaires survivent au `schema:update --force` du setup de test.
 */
final class Version20260601000000 extends AbstractMigration
{
    public function getDescription(): string
    {
        return 'ERP-53 (M1) : tables client + sous-collections + referentiels comptables + seed category_type.';
    }

    public function up(Schema $schema): void
    {
        $this->createAccountingReferentials();
        $this->createClientTable();
        $this->createClientCategory();
        $this->createClientContact();
        $this->createClientAddress();
        $this->createClientAddressJoinTables();
        $this->createClientRib();
        $this->seedCategoryTypes();
    }

    public function down(Schema $schema): void
    {
        // Ordre inverse des dependances FK : on supprime d'abord les jointures
        // et sous-collections, puis client, puis les referentiels.
        $this->addSql('DROP TABLE client_address_category');
        $this->addSql('DROP TABLE client_address_contact');
        $this->addSql('DROP TABLE client_address_site');
        $this->addSql('DROP TABLE client_rib');
        $this->addSql('DROP TABLE client_address');
        $this->addSql('DROP TABLE client_contact');
        $this->addSql('DROP TABLE client_category');
        $this->addSql('DROP TABLE client');
        $this->addSql('DROP TABLE bank');
        $this->addSql('DROP TABLE payment_type');
        $this->addSql('DROP TABLE payment_delay');
        $this->addSql('DROP TABLE tva_mode');

        // Retire uniquement les 4 types seedes par cette migration ET restes
        // orphelins (aucune `category` ne les reference). Sans la clause
        // NOT EXISTS, le DELETE casse sur la FK RESTRICT category.category_type_id
        // des qu'une categorie pointe sur l'un d'eux. Symetrique du
        // `ON CONFLICT (code) DO NOTHING` du up() : on ne defait que ce qu'on a
        // reellement cree et qui n'est pas reutilise.
        $this->addSql(<<<'SQL'
            DELETE FROM category_type
            WHERE code IN ('DISTRIBUTEUR', 'COURTIER', 'SECTEUR', 'AUTRE')
              AND NOT EXISTS (
                  SELECT 1 FROM category c WHERE c.category_type_id = category_type.id
              )
            SQL);
    }

    // =================================================================
    // Referentiels comptables (4 tables statiques, memes colonnes)
    // =================================================================

    private function createAccountingReferentials(): void
    {
        $referentials = [
            'tva_mode'      => 'Referentiel des modes de TVA appliques a un client (France, Export, Intracom).',
            'payment_delay' => 'Referentiel des delais de reglement (15 jours, 30 jours, a reception).',
            'payment_type'  => 'Referentiel des types de reglement (virement, LCR, cheque, non soumise).',
            'bank'          => 'Referentiel des banques selectionnables pour le reglement par virement.',
        ];

        foreach ($referentials as $table => $tableComment) {
            $this->addSql(sprintf(<<<'SQL'
                CREATE TABLE %s (
                    id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
                    code VARCHAR(30) NOT NULL,
                    label VARCHAR(120) NOT NULL,
                    position INT DEFAULT 0 NOT NULL,
                    PRIMARY KEY (id)
                )
                SQL, $table));
            $this->addSql(sprintf('CREATE UNIQUE INDEX uq_%s_code ON %s (code)', $table, $table));

            $this->comment($table, '_table', $tableComment);
            $this->comment($table, 'id', 'Identifiant interne auto-incremente.');
            $this->comment($table, 'code', 'Code technique stable (UPPER_SNAKE, ≤ 30 caracteres) — unique, utilise par le code metier.');
            $this->comment($table, 'label', 'Libelle affichable (FR, ≤ 120 caracteres).');
            $this->comment($table, 'position', 'Ordre d affichage croissant dans les selecteurs (tri position ASC puis label ASC).');
        }

        // Seed initial (cf. spec § 3.2). Tables fraichement creees donc vides :
        // INSERT direct sans ON CONFLICT.
        $this->addSql(<<<'SQL'
            INSERT INTO tva_mode (code, label, position) VALUES
                ('FRANCE_VENTES',   'France (ventes)',    10),
                ('EXPORT_VENTES',   'Export (ventes)',    20),
                ('INTRACOM_VENTES', 'Intracom (ventes)',  30)
            SQL);
        $this->addSql(<<<'SQL'
            INSERT INTO payment_delay (code, label, position) VALUES
                ('J15',         '15 jours',     10),
                ('J30',         '30 jours',     20),
                ('A_RECEPTION', 'À réception',  30)
            SQL);
        $this->addSql(<<<'SQL'
            INSERT INTO payment_type (code, label, position) VALUES
                ('VIREMENT',    'Virement',     10),
                ('LCR',         'LCR',          20),
                ('NON_SOUMISE', 'Non soumise',  30),
                ('CHEQUE',      'Chèque',       40)
            SQL);
        $this->addSql(<<<'SQL'
            INSERT INTO bank (code, label, position) VALUES
                ('SG',  'Société Générale', 10),
                ('CIC', 'CIC',              20),
                ('CA',  'Crédit Agricole',  30)
            SQL);
    }

    // =================================================================
    // Table principale `client`
    // =================================================================

    private function createClientTable(): void
    {
        $this->addSql(<<<'SQL'
            CREATE TABLE client (
                id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
                company_name VARCHAR(180) NOT NULL,
                first_name VARCHAR(120) DEFAULT NULL,
                last_name VARCHAR(120) DEFAULT NULL,
                phone_primary VARCHAR(20) NOT NULL,
                phone_secondary VARCHAR(20) DEFAULT NULL,
                email VARCHAR(180) NOT NULL,
                distributor_id INT DEFAULT NULL,
                broker_id INT DEFAULT NULL,
                triage_service BOOLEAN DEFAULT FALSE NOT NULL,
                description TEXT DEFAULT NULL,
                competitors VARCHAR(255) DEFAULT NULL,
                founded_at DATE DEFAULT NULL,
                employees_count INT DEFAULT NULL,
                revenue_amount NUMERIC(15, 2) DEFAULT NULL,
                director_name VARCHAR(120) DEFAULT NULL,
                profit_amount NUMERIC(15, 2) DEFAULT NULL,
                siren VARCHAR(20) DEFAULT NULL,
                account_number VARCHAR(40) DEFAULT NULL,
                tva_mode_id INT DEFAULT NULL,
                n_tva VARCHAR(40) DEFAULT NULL,
                payment_delay_id INT DEFAULT NULL,
                payment_type_id INT DEFAULT NULL,
                bank_id INT DEFAULT NULL,
                is_archived BOOLEAN DEFAULT FALSE NOT NULL,
                archived_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL,
                deleted_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL,
                created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL,
                updated_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL,
                created_by INT DEFAULT NULL,
                updated_by INT DEFAULT NULL,
                PRIMARY KEY (id),
                CONSTRAINT chk_client_distrib_or_broker
                    CHECK (NOT (distributor_id IS NOT NULL AND broker_id IS NOT NULL)),
                CONSTRAINT fk_client_distributor
                    FOREIGN KEY (distributor_id) REFERENCES client (id) ON DELETE SET NULL,
                CONSTRAINT fk_client_broker
                    FOREIGN KEY (broker_id) REFERENCES client (id) ON DELETE SET NULL,
                CONSTRAINT fk_client_tva_mode
                    FOREIGN KEY (tva_mode_id) REFERENCES tva_mode (id) ON DELETE RESTRICT,
                CONSTRAINT fk_client_payment_delay
                    FOREIGN KEY (payment_delay_id) REFERENCES payment_delay (id) ON DELETE RESTRICT,
                CONSTRAINT fk_client_payment_type
                    FOREIGN KEY (payment_type_id) REFERENCES payment_type (id) ON DELETE RESTRICT,
                CONSTRAINT fk_client_bank
                    FOREIGN KEY (bank_id) REFERENCES bank (id) ON DELETE RESTRICT,
                CONSTRAINT fk_client_created_by
                    FOREIGN KEY (created_by) REFERENCES "user" (id) ON DELETE SET NULL,
                CONSTRAINT fk_client_updated_by
                    FOREIGN KEY (updated_by) REFERENCES "user" (id) ON DELETE SET NULL
            )
            SQL);

        $this->addSql('CREATE INDEX idx_client_is_archived ON client (is_archived)');
        $this->addSql('CREATE INDEX idx_client_deleted_at ON client (deleted_at)');
        $this->addSql('CREATE INDEX idx_client_distributor_id ON client (distributor_id)');
        $this->addSql('CREATE INDEX idx_client_broker_id ON client (broker_id)');
        $this->addSql('CREATE INDEX idx_client_created_by ON client (created_by)');
        $this->addSql('CREATE INDEX idx_client_updated_by ON client (updated_by)');

        // Index sur les FK des referentiels comptables — coherence avec les autres
        // FK deja indexees ci-dessus (Postgres n'indexe pas automatiquement les
        // colonnes portant une FOREIGN KEY).
        $this->addSql('CREATE INDEX idx_client_tva_mode_id ON client (tva_mode_id)');
        $this->addSql('CREATE INDEX idx_client_payment_delay_id ON client (payment_delay_id)');
        $this->addSql('CREATE INDEX idx_client_payment_type_id ON client (payment_type_id)');
        $this->addSql('CREATE INDEX idx_client_bank_id ON client (bank_id)');

        // Unicite metier partielle (Q4) : nom de societe insensible a la casse,
        // parmi les non-archives ET non soft-deletes uniquement. Pas d'index
        // unique sur siren ni email.
        $this->addSql(<<<'SQL'
            CREATE UNIQUE INDEX uq_client_company_name_active
            ON client (LOWER(company_name))
            WHERE is_archived = FALSE AND deleted_at IS NULL
            SQL);

        $this->comment('client', '_table', 'Repertoire clients (M1 Commercial) — entites archivables (is_archived) et soft-deletables (deleted_at, HP M2).');
        $this->comment('client', 'id', 'Identifiant interne auto-incremente.');
        $this->comment('client', '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).');
        $this->comment('client', 'first_name', 'Prenom du contact principal (capitalise serveur, RG-1.19). first_name OU last_name obligatoire (RG-1.01).');
        $this->comment('client', 'last_name', 'Nom du contact principal (capitalise serveur, RG-1.19). first_name OU last_name obligatoire (RG-1.01).');
        $this->comment('client', 'phone_primary', 'Telephone principal — stocke en chiffres uniquement (RG-1.20). Obligatoire.');
        $this->comment('client', 'phone_secondary', 'Telephone secondaire optionnel — chiffres uniquement (RG-1.20).');
        $this->comment('client', 'email', 'Email principal (lowercase serveur, RG-1.21). NON unique (RG-1.17 supprimee, Q4).');
        $this->comment('client', '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.');
        $this->comment('client', '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.');
        $this->comment('client', 'triage_service', 'Drapeau service triage active pour le client. Faux par defaut.');
        $this->comment('client', 'description', 'Onglet Information : description libre. Facultatif.');
        $this->comment('client', 'competitors', 'Onglet Information : concurrents identifies (texte libre ≤ 255). Facultatif.');
        $this->comment('client', 'founded_at', 'Onglet Information : date de creation de l entreprise. Facultatif.');
        $this->comment('client', 'employees_count', 'Onglet Information : effectif (entier >= 0). Facultatif.');
        $this->comment('client', 'revenue_amount', 'Onglet Information : chiffre d affaires (NUMERIC 15,2). Facultatif.');
        $this->comment('client', 'director_name', 'Onglet Information : nom du dirigeant. Facultatif.');
        $this->comment('client', 'profit_amount', 'Onglet Information : resultat / benefice (NUMERIC 15,2). Facultatif.');
        $this->comment('client', 'siren', 'Onglet Comptabilite : SIREN (9 chiffres attendus). NON unique — peut etre partage entre etablissements (RG-1.15 supprimee, Q4).');
        $this->comment('client', 'account_number', 'Onglet Comptabilite : numero de compte comptable du client.');
        $this->comment('client', 'tva_mode_id', 'Onglet Comptabilite : mode de TVA applique — FK -> tva_mode.id, ON DELETE RESTRICT.');
        $this->comment('client', 'n_tva', 'Onglet Comptabilite : numero de TVA intracommunautaire.');
        $this->comment('client', 'payment_delay_id', 'Onglet Comptabilite : delai de reglement — FK -> payment_delay.id, ON DELETE RESTRICT.');
        $this->comment('client', '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).');
        $this->comment('client', 'bank_id', 'Onglet Comptabilite : banque — FK -> bank.id, ON DELETE RESTRICT. Obligatoire si payment_type = VIREMENT (RG-1.12).');
        $this->comment('client', 'is_archived', 'Drapeau fonctionnel d archivage — masque par defaut dans la liste. Bascule via permission commercial.clients.archive (RG-1.22/23).');
        $this->comment('client', 'archived_at', 'Horodatage de l archivage — pose quand is_archived passe a vrai, remis a null a la restauration (RG-1.22/23).');
        $this->comment('client', 'deleted_at', 'Horodatage du soft-delete technique (HP M2) — non expose par l API au M1. Null = ligne active.');
        $this->addTimestampableBlamableComments('client');
    }

    // =================================================================
    // M2M client <-> category
    // =================================================================

    private function createClientCategory(): void
    {
        $this->addSql(<<<'SQL'
            CREATE TABLE client_category (
                client_id INT NOT NULL,
                category_id INT NOT NULL,
                PRIMARY KEY (client_id, category_id),
                CONSTRAINT fk_client_category_client
                    FOREIGN KEY (client_id) REFERENCES client (id) ON DELETE CASCADE,
                CONSTRAINT fk_client_category_category
                    FOREIGN KEY (category_id) REFERENCES category (id) ON DELETE RESTRICT
            )
            SQL);
        $this->addSql('CREATE INDEX idx_client_category_category ON client_category (category_id)');

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

    // =================================================================
    // Sous-collection : contacts (1:n)
    // =================================================================

    private function createClientContact(): void
    {
        $this->addSql(<<<'SQL'
            CREATE TABLE client_contact (
                id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
                client_id INT NOT NULL,
                first_name VARCHAR(120) DEFAULT NULL,
                last_name VARCHAR(120) DEFAULT NULL,
                job_title VARCHAR(120) DEFAULT NULL,
                phone_primary VARCHAR(20) DEFAULT NULL,
                phone_secondary VARCHAR(20) DEFAULT NULL,
                email VARCHAR(180) DEFAULT NULL,
                position INT DEFAULT 0 NOT NULL,
                created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL,
                updated_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL,
                created_by INT DEFAULT NULL,
                updated_by INT DEFAULT NULL,
                PRIMARY KEY (id),
                CONSTRAINT chk_client_contact_name
                    CHECK (first_name IS NOT NULL OR last_name IS NOT NULL),
                CONSTRAINT fk_client_contact_client
                    FOREIGN KEY (client_id) REFERENCES client (id) ON DELETE CASCADE,
                CONSTRAINT fk_client_contact_created_by
                    FOREIGN KEY (created_by) REFERENCES "user" (id) ON DELETE SET NULL,
                CONSTRAINT fk_client_contact_updated_by
                    FOREIGN KEY (updated_by) REFERENCES "user" (id) ON DELETE SET NULL
            )
            SQL);
        $this->addSql('CREATE INDEX idx_client_contact_client ON client_contact (client_id)');

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

    // =================================================================
    // Sous-collection : adresses (1:n)
    // =================================================================

    private function createClientAddress(): void
    {
        $this->addSql(<<<'SQL'
            CREATE TABLE client_address (
                id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
                client_id INT NOT NULL,
                is_prospect BOOLEAN DEFAULT FALSE NOT NULL,
                is_delivery BOOLEAN DEFAULT FALSE NOT NULL,
                is_billing BOOLEAN DEFAULT FALSE NOT NULL,
                country VARCHAR(80) DEFAULT 'France' NOT NULL,
                postal_code VARCHAR(20) NOT NULL,
                city VARCHAR(120) NOT NULL,
                street VARCHAR(255) NOT NULL,
                street_complement VARCHAR(255) DEFAULT NULL,
                billing_email VARCHAR(180) DEFAULT NULL,
                position INT DEFAULT 0 NOT NULL,
                created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL,
                updated_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL,
                created_by INT DEFAULT NULL,
                updated_by INT DEFAULT NULL,
                PRIMARY KEY (id),
                CONSTRAINT chk_client_address_prospect_exclusive
                    CHECK (NOT (is_prospect = TRUE AND (is_delivery = TRUE OR is_billing = TRUE))),
                CONSTRAINT chk_client_address_billing_email
                    CHECK ((is_billing = FALSE AND billing_email IS NULL)
                        OR (is_billing = TRUE AND billing_email IS NOT NULL)),
                CONSTRAINT fk_client_address_client
                    FOREIGN KEY (client_id) REFERENCES client (id) ON DELETE CASCADE,
                CONSTRAINT fk_client_address_created_by
                    FOREIGN KEY (created_by) REFERENCES "user" (id) ON DELETE SET NULL,
                CONSTRAINT fk_client_address_updated_by
                    FOREIGN KEY (updated_by) REFERENCES "user" (id) ON DELETE SET NULL
            )
            SQL);
        $this->addSql('CREATE INDEX idx_client_address_client ON client_address (client_id)');

        $this->comment('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).');
        $this->comment('client_address', 'id', 'Identifiant interne auto-incremente.');
        $this->comment('client_address', 'client_id', 'FK -> client.id, ON DELETE CASCADE — client proprietaire de l adresse.');
        $this->comment('client_address', 'is_prospect', 'Adresse de prospection — exclusive de is_delivery/is_billing (RG-1.06/07/08, chk_client_address_prospect_exclusive). Faux par defaut.');
        $this->comment('client_address', 'is_delivery', 'Adresse de livraison. Exclusive de is_prospect. Faux par defaut.');
        $this->comment('client_address', 'is_billing', 'Adresse de facturation. Exclusive de is_prospect. Impose billing_email (RG-1.11). Faux par defaut.');
        $this->comment('client_address', 'country', 'Pays de l adresse — defaut France.');
        $this->comment('client_address', 'postal_code', 'Code postal (4-5 chiffres attendus, RG-1.09).');
        $this->comment('client_address', 'city', 'Ville — preremplie depuis le code postal via API BAN cote front (RG-1.09).');
        $this->comment('client_address', 'street', 'Numero et voie de l adresse.');
        $this->comment('client_address', 'street_complement', 'Complement d adresse (etage, batiment...) — optionnel.');
        $this->comment('client_address', 'billing_email', 'Email de facturation — obligatoire si is_billing, null sinon (RG-1.11, chk_client_address_billing_email).');
        $this->comment('client_address', 'position', 'Ordre d affichage de l adresse dans la liste du client (croissant).');
        $this->addTimestampableBlamableComments('client_address');
    }

    // =================================================================
    // Jointures de client_address (M2M)
    // =================================================================

    private function createClientAddressJoinTables(): void
    {
        $this->addSql(<<<'SQL'
            CREATE TABLE client_address_site (
                client_address_id INT NOT NULL,
                site_id INT NOT NULL,
                PRIMARY KEY (client_address_id, site_id),
                CONSTRAINT fk_client_address_site_address
                    FOREIGN KEY (client_address_id) REFERENCES client_address (id) ON DELETE CASCADE,
                CONSTRAINT fk_client_address_site_site
                    FOREIGN KEY (site_id) REFERENCES site (id) ON DELETE RESTRICT
            )
            SQL);
        $this->comment('client_address_site', '_table', 'Jointure M2M client_address <-> site (Sites) — sites desservis par l adresse (>= 1 obligatoire, RG-1.10).');
        $this->comment('client_address_site', 'client_address_id', 'FK -> client_address.id, ON DELETE CASCADE — adresse concernee.');
        $this->comment('client_address_site', 'site_id', 'FK -> site.id, ON DELETE RESTRICT — site rattache a l adresse.');

        $this->addSql(<<<'SQL'
            CREATE TABLE client_address_contact (
                client_address_id INT NOT NULL,
                client_contact_id INT NOT NULL,
                PRIMARY KEY (client_address_id, client_contact_id),
                CONSTRAINT fk_client_address_contact_address
                    FOREIGN KEY (client_address_id) REFERENCES client_address (id) ON DELETE CASCADE,
                CONSTRAINT fk_client_address_contact_contact
                    FOREIGN KEY (client_contact_id) REFERENCES client_contact (id) ON DELETE CASCADE
            )
            SQL);
        $this->comment('client_address_contact', '_table', 'Jointure M2M client_address <-> client_contact — contacts associes a une adresse.');
        $this->comment('client_address_contact', 'client_address_id', 'FK -> client_address.id, ON DELETE CASCADE — adresse concernee.');
        $this->comment('client_address_contact', 'client_contact_id', 'FK -> client_contact.id, ON DELETE CASCADE — contact associe a l adresse.');

        $this->addSql(<<<'SQL'
            CREATE TABLE client_address_category (
                client_address_id INT NOT NULL,
                category_id INT NOT NULL,
                PRIMARY KEY (client_address_id, category_id),
                CONSTRAINT fk_client_address_category_address
                    FOREIGN KEY (client_address_id) REFERENCES client_address (id) ON DELETE CASCADE,
                CONSTRAINT fk_client_address_category_category
                    FOREIGN KEY (category_id) REFERENCES category (id) ON DELETE RESTRICT
            )
            SQL);
        $this->comment('client_address_category', '_table', 'Jointure M2M client_address <-> category — categories d adresse (types SECTEUR/AUTRE uniquement, RG-1.29).');
        $this->comment('client_address_category', 'client_address_id', 'FK -> client_address.id, ON DELETE CASCADE — adresse concernee.');
        $this->comment('client_address_category', 'category_id', 'FK -> category.id, ON DELETE RESTRICT — categorie d adresse (type SECTEUR ou AUTRE, RG-1.29).');
    }

    // =================================================================
    // Sous-collection : RIB (1:n)
    // =================================================================

    private function createClientRib(): void
    {
        $this->addSql(<<<'SQL'
            CREATE TABLE client_rib (
                id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
                client_id INT NOT NULL,
                label VARCHAR(120) NOT NULL,
                bic VARCHAR(20) NOT NULL,
                iban VARCHAR(34) NOT NULL,
                position INT DEFAULT 0 NOT NULL,
                created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL,
                updated_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL,
                created_by INT DEFAULT NULL,
                updated_by INT DEFAULT NULL,
                PRIMARY KEY (id),
                CONSTRAINT fk_client_rib_client
                    FOREIGN KEY (client_id) REFERENCES client (id) ON DELETE CASCADE,
                CONSTRAINT fk_client_rib_created_by
                    FOREIGN KEY (created_by) REFERENCES "user" (id) ON DELETE SET NULL,
                CONSTRAINT fk_client_rib_updated_by
                    FOREIGN KEY (updated_by) REFERENCES "user" (id) ON DELETE SET NULL
            )
            SQL);
        $this->addSql('CREATE INDEX idx_client_rib_client ON client_rib (client_id)');

        $this->comment('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).');
        $this->comment('client_rib', 'id', 'Identifiant interne auto-incremente.');
        $this->comment('client_rib', 'client_id', 'FK -> client.id, ON DELETE CASCADE — client proprietaire du RIB.');
        $this->comment('client_rib', 'label', 'Libelle du RIB (ex: compte principal).');
        $this->comment('client_rib', 'bic', 'Code BIC/SWIFT de la banque (8 ou 11 caracteres).');
        $this->comment('client_rib', 'iban', 'IBAN du compte (≤ 34 caracteres).');
        $this->comment('client_rib', 'position', 'Ordre d affichage du RIB dans la liste du client (croissant).');
        $this->addTimestampableBlamableComments('client_rib');
    }

    // =================================================================
    // Seed extension category_type (M0)
    // =================================================================

    private function seedCategoryTypes(): void
    {
        // Idempotent : la table category_type peut deja porter des donnees en
        // prod. ON CONFLICT (code) s appuie sur l index unique uq_category_type_code.
        // NB : la table M0 n a pas de colonne `position` (id/code/label seulement),
        // contrairement au pseudo-SQL de la spec § 3.3.
        $this->addSql(<<<'SQL'
            INSERT INTO category_type (code, label) VALUES
                ('DISTRIBUTEUR', 'Distributeur'),
                ('COURTIER',     'Courtier'),
                ('SECTEUR',      'Secteur'),
                ('AUTRE',        'Autre')
            ON CONFLICT (code) DO NOTHING
            SQL);
    }

    // =================================================================
    // Helpers
    // =================================================================

    /**
     * Pose les 4 commentaires standardises Timestampable/Blamable sur une table,
     * en reutilisant le catalogue partage (source unique, cf. ERP-67).
     */
    private function addTimestampableBlamableComments(string $table): void
    {
        foreach (ColumnCommentsCatalog::timestampableBlamableComments() as $column => $description) {
            $this->comment($table, $column, $description);
        }
    }

    /**
     * Emet un `COMMENT ON TABLE` (colonne speciale `_table`) ou
     * `COMMENT ON COLUMN` en dollar-quoting Postgres ($_$...$_$) pour eviter
     * tout echappement d apostrophe.
     */
    private function comment(string $table, string $column, string $description): void
    {
        $quotedTable = '"'.str_replace('"', '""', $table).'"';

        if ('_table' === $column) {
            $this->addSql(sprintf('COMMENT ON TABLE %s IS $_$%s$_$', $quotedTable, $description));

            return;
        }

        $this->addSql(sprintf(
            'COMMENT ON COLUMN %s.%s IS $_$%s$_$',
            $quotedTable,
            '"'.str_replace('"', '""', $column).'"',
            $description,
        ));
    }
}
