<?php

declare(strict_types=1);

namespace DoctrineMigrations;

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

/**
 * M4 — Repertoire transporteurs (ERP-155/157) : creation du schema BDD du
 * repertoire transporteurs sous le module Transport (jumeau des M2/M3).
 *
 * Tables creees :
 *  - carrier : table principale (formulaire + lien QUALIMAT + archive + soft-delete
 *    + Timestampable/Blamable) ;
 *  - carrier_address / carrier_contact / carrier_price : sous-collections 1:n.
 *
 * Tables NON recrees (reutilisees) :
 *  - qualimat_carrier (ERP-39, Version20260612150000) : cible de la FK editable
 *    carrier.qualimat_carrier_id (§ 2.5) ;
 *  - uploaded_document (ERP-154, Version20260615130000) : cible de la FK
 *    carrier.discharge_document_id (Decharge, § 2.7) ;
 *  - client / client_address / supplier / supplier_address (M1/M2) et site (Sites) :
 *    cibles des FK de carrier_price (onglet Prix, RG-4.10/4.11).
 *
 * Namespace racine `DoctrineMigrations` (regle ABSOLUE n°11) et NON modulaire :
 * FK cross-module (user, client, client_address, supplier, supplier_address, site,
 * qualimat_carrier, uploaded_document). Le tri par timestamp au sein du namespace
 * racine garantit l'ordre apres la creation de ces tables sur base vide.
 *
 * Decision IDs (spec § 2.2, tranchee a ce ticket) : carrier et ses sous-tables
 * utilisent `INT GENERATED BY DEFAULT AS IDENTITY` (homogeneite globale Starseed
 * M1/M2/M3, evite la friction bigint->string de l'ORM). Seule
 * carrier.qualimat_carrier_id est BIGINT pour matcher qualimat_carrier.id (existant).
 * Horodatages `TIMESTAMP(0) WITHOUT TIME ZONE` (le TimestampableBlamableTrait mappe
 * `datetime_immutable`), pour que `schema:update --force` reste un no-op.
 *
 * Chaque colonne porte son `COMMENT ON COLUMN` (regle ABSOLUE n°12). Les 4
 * tables carrier* etant mappees par l'ORM des ce ticket, elles sont aussi ajoutees
 * a ColumnCommentsCatalog : `app:apply-column-comments` (test-db-setup) rejoue ces
 * COMMENT apres le `schema:update --force` qui les droperait sinon.
 */
final class Version20260615150000 extends AbstractMigration
{
    public function getDescription(): string
    {
        return 'ERP-155/157 (M4) : tables carrier + carrier_address + carrier_contact + carrier_price (repertoire transporteurs).';
    }

    public function up(Schema $schema): void
    {
        $this->createCarrierTable();
        $this->createCarrierAddress();
        $this->createCarrierContact();
        $this->createCarrierPrice();
    }

    public function down(Schema $schema): void
    {
        // Ordre inverse des dependances FK : sous-collections d'abord, puis carrier.
        $this->addSql('DROP TABLE IF EXISTS carrier_price');
        $this->addSql('DROP TABLE IF EXISTS carrier_contact');
        $this->addSql('DROP TABLE IF EXISTS carrier_address');
        $this->addSql('DROP TABLE IF EXISTS carrier');
    }

    // =================================================================
    // Table principale `carrier`
    // =================================================================

    private function createCarrierTable(): void
    {
        $this->addSql(<<<'SQL'
            CREATE TABLE carrier (
                id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
                qualimat_carrier_id BIGINT DEFAULT NULL,
                name VARCHAR(255) NOT NULL,
                certification_type VARCHAR(20) DEFAULT NULL,
                is_chartered BOOLEAN DEFAULT FALSE NOT NULL,
                indexation_rate NUMERIC(5, 2) DEFAULT NULL,
                container_type VARCHAR(12) DEFAULT NULL,
                volume_m3 NUMERIC(10, 2) DEFAULT NULL,
                discharge_document_id INT DEFAULT NULL,
                liot_plates TEXT 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_carrier_certification_type
                    CHECK (certification_type IS NULL OR certification_type IN ('QUALIMAT', 'GMP_PLUS', 'OVOCOM', 'COMPTE_PROPRE', 'AUTRE')),
                CONSTRAINT chk_carrier_container_type
                    CHECK (container_type IS NULL OR container_type IN ('BENNE', 'FOND_MOUVANT')),
                CONSTRAINT fk_carrier_qualimat
                    FOREIGN KEY (qualimat_carrier_id) REFERENCES qualimat_carrier (id) ON DELETE SET NULL,
                CONSTRAINT fk_carrier_discharge_document
                    FOREIGN KEY (discharge_document_id) REFERENCES uploaded_document (id) ON DELETE SET NULL,
                CONSTRAINT fk_carrier_created_by
                    FOREIGN KEY (created_by) REFERENCES "user" (id) ON DELETE SET NULL,
                CONSTRAINT fk_carrier_updated_by
                    FOREIGN KEY (updated_by) REFERENCES "user" (id) ON DELETE SET NULL
            )
            SQL);

        $this->addSql('CREATE INDEX idx_carrier_is_archived ON carrier (is_archived)');
        $this->addSql('CREATE INDEX idx_carrier_deleted_at ON carrier (deleted_at)');
        $this->addSql('CREATE INDEX idx_carrier_qualimat ON carrier (qualimat_carrier_id)');
        $this->addSql('CREATE INDEX idx_carrier_discharge_document ON carrier (discharge_document_id)');
        $this->addSql('CREATE INDEX idx_carrier_created_by ON carrier (created_by)');
        $this->addSql('CREATE INDEX idx_carrier_updated_by ON carrier (updated_by)');

        // Unicite metier partielle : nom insensible a la casse, parmi les
        // non-archives ET non soft-deletes uniquement (§ 2.6). Inexprimable en ORM.
        $this->addSql(<<<'SQL'
            CREATE UNIQUE INDEX uq_carrier_name_active
            ON carrier (LOWER(name))
            WHERE is_archived = FALSE AND deleted_at IS NULL
            SQL);

        $this->comment('carrier', '_table', 'Repertoire transporteurs (M4 Transport) — entites editables, archivables (is_archived) et soft-deletables (deleted_at). Distinct du referentiel qualimat_carrier.');
        $this->comment('carrier', 'id', 'Identifiant interne auto-incremente.');
        $this->comment('carrier', '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.');
        $this->comment('carrier', '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).');
        $this->comment('carrier', '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).');
        $this->comment('carrier', 'is_chartered', '« Affreter » coche : declenche indexation/benne-fond mouvant/volume, obligatoires (RG-4.03). Faux par defaut.');
        $this->comment('carrier', 'indexation_rate', 'Taux d indexation en pourcentage (NUMERIC 5,2) — renseigne si affrete (RG-4.03).');
        $this->comment('carrier', 'container_type', 'Type de contenant BENNE|FOND_MOUVANT (chk_carrier_container_type) — renseigne si affrete (RG-4.03).');
        $this->comment('carrier', 'volume_m3', 'Volume en m3 (NUMERIC 10,2) — renseigne si affrete (RG-4.03).');
        $this->comment('carrier', '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.');
        $this->comment('carrier', 'liot_plates', 'Immatriculations LIOT separees par « ; » (cas special nom=LIOT, RG-4.01). Les autres champs sont masques dans ce cas.');
        $this->comment('carrier', 'is_archived', 'Drapeau fonctionnel d archivage — masque par defaut dans la liste. Bascule via permission transport.carriers.archive (Admin seul).');
        $this->comment('carrier', 'archived_at', 'Horodatage de l archivage — pose quand is_archived passe a vrai, remis a null a la restauration.');
        $this->comment('carrier', 'deleted_at', 'Horodatage du soft-delete technique — non expose par l API au M4. Null = ligne active.');
        $this->addTimestampableBlamableComments('carrier');
    }

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

    private function createCarrierAddress(): void
    {
        $this->addSql(<<<'SQL'
            CREATE TABLE carrier_address (
                id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
                carrier_id INT NOT NULL,
                country VARCHAR(80) DEFAULT 'France' NOT NULL,
                postal_code VARCHAR(20) DEFAULT NULL,
                city VARCHAR(120) DEFAULT NULL,
                street VARCHAR(255) DEFAULT NULL,
                street_complement VARCHAR(255) 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 fk_carrier_address_carrier
                    FOREIGN KEY (carrier_id) REFERENCES carrier (id) ON DELETE CASCADE,
                CONSTRAINT fk_carrier_address_created_by
                    FOREIGN KEY (created_by) REFERENCES "user" (id) ON DELETE SET NULL,
                CONSTRAINT fk_carrier_address_updated_by
                    FOREIGN KEY (updated_by) REFERENCES "user" (id) ON DELETE SET NULL
            )
            SQL);
        $this->addSql('CREATE INDEX idx_carrier_address_carrier ON carrier_address (carrier_id)');
        $this->addSql('CREATE INDEX idx_carrier_address_created_by ON carrier_address (created_by)');
        $this->addSql('CREATE INDEX idx_carrier_address_updated_by ON carrier_address (updated_by)');

        $this->comment('carrier_address', '_table', 'Adresses d un transporteur (1:n) — onglet Adresse (M4). Pre-remplie depuis QUALIMAT si applicable (RG-4.05).');
        $this->comment('carrier_address', 'id', 'Identifiant interne auto-incremente.');
        $this->comment('carrier_address', 'carrier_id', 'FK -> carrier.id, ON DELETE CASCADE — transporteur proprietaire de l adresse.');
        $this->comment('carrier_address', 'country', 'Pays de l adresse — defaut France.');
        $this->comment('carrier_address', 'postal_code', 'Code postal (saisie assistee BAN cote front, RG-4.06).');
        $this->comment('carrier_address', 'city', 'Ville — preremplie depuis le code postal via API BAN cote front.');
        $this->comment('carrier_address', 'street', 'Numero et voie de l adresse.');
        $this->comment('carrier_address', 'street_complement', 'Complement d adresse (etage, batiment...) — optionnel.');
        $this->comment('carrier_address', 'position', 'Ordre d affichage de l adresse dans la liste du transporteur (croissant).');
        $this->addTimestampableBlamableComments('carrier_address');
    }

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

    private function createCarrierContact(): void
    {
        $this->addSql(<<<'SQL'
            CREATE TABLE carrier_contact (
                id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
                carrier_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_carrier_contact_filled
                    CHECK (first_name IS NOT NULL OR last_name IS NOT NULL OR job_title IS NOT NULL
                           OR phone_primary IS NOT NULL OR email IS NOT NULL),
                CONSTRAINT fk_carrier_contact_carrier
                    FOREIGN KEY (carrier_id) REFERENCES carrier (id) ON DELETE CASCADE,
                CONSTRAINT fk_carrier_contact_created_by
                    FOREIGN KEY (created_by) REFERENCES "user" (id) ON DELETE SET NULL,
                CONSTRAINT fk_carrier_contact_updated_by
                    FOREIGN KEY (updated_by) REFERENCES "user" (id) ON DELETE SET NULL
            )
            SQL);
        $this->addSql('CREATE INDEX idx_carrier_contact_carrier ON carrier_contact (carrier_id)');
        $this->addSql('CREATE INDEX idx_carrier_contact_created_by ON carrier_contact (created_by)');
        $this->addSql('CREATE INDEX idx_carrier_contact_updated_by ON carrier_contact (updated_by)');

        $this->comment('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.');
        $this->comment('carrier_contact', 'id', 'Identifiant interne auto-incremente.');
        $this->comment('carrier_contact', 'carrier_id', 'FK -> carrier.id, ON DELETE CASCADE — transporteur proprietaire du contact.');
        $this->comment('carrier_contact', 'first_name', 'Prenom du contact (capitalise serveur). Au moins un champ du contact est requis (RG-4.08).');
        $this->comment('carrier_contact', 'last_name', 'Nom du contact (capitalise serveur). Au moins un champ du contact est requis (RG-4.08).');
        $this->comment('carrier_contact', 'job_title', 'Fonction / intitule de poste du contact (≤ 120 caracteres).');
        $this->comment('carrier_contact', 'phone_primary', 'Telephone principal — chiffres uniquement (normalisation serveur).');
        $this->comment('carrier_contact', 'phone_secondary', 'Telephone secondaire — chiffres uniquement (max 2 telephones, RG-4.08).');
        $this->comment('carrier_contact', 'email', 'Email du contact (lowercase serveur).');
        $this->comment('carrier_contact', 'position', 'Ordre d affichage du contact dans la liste du transporteur (croissant).');
        $this->addTimestampableBlamableComments('carrier_contact');
    }

    // =================================================================
    // Sous-collection : prix (1:n) — onglet Prix (RG-4.09 -> RG-4.11)
    // =================================================================

    private function createCarrierPrice(): void
    {
        $this->addSql(<<<'SQL'
            CREATE TABLE carrier_price (
                id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
                carrier_id INT NOT NULL,
                direction VARCHAR(12) NOT NULL,
                client_id INT DEFAULT NULL,
                client_delivery_address_id INT DEFAULT NULL,
                departure_site_id INT DEFAULT NULL,
                supplier_id INT DEFAULT NULL,
                supplier_supply_address_id INT DEFAULT NULL,
                delivery_site_id INT DEFAULT NULL,
                container_type VARCHAR(12) NOT NULL,
                pricing_unit VARCHAR(8) NOT NULL,
                price NUMERIC(12, 2) NOT NULL,
                price_state VARCHAR(12) 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 chk_carrier_price_direction CHECK (direction IN ('CLIENT', 'FOURNISSEUR')),
                CONSTRAINT chk_carrier_price_container CHECK (container_type IN ('BENNE', 'FOND_MOUVANT')),
                CONSTRAINT chk_carrier_price_unit CHECK (pricing_unit IN ('FORFAIT', 'TONNE')),
                CONSTRAINT chk_carrier_price_state CHECK (price_state IN ('EN_COURS', 'VALIDE', 'NON_VALIDE')),
                CONSTRAINT chk_carrier_price_client_branch
                    CHECK (direction <> 'CLIENT' OR (client_id IS NOT NULL AND supplier_id IS NULL)),
                CONSTRAINT chk_carrier_price_supplier_branch
                    CHECK (direction <> 'FOURNISSEUR' OR (supplier_id IS NOT NULL AND client_id IS NULL)),
                CONSTRAINT fk_carrier_price_carrier
                    FOREIGN KEY (carrier_id) REFERENCES carrier (id) ON DELETE CASCADE,
                CONSTRAINT fk_carrier_price_client
                    FOREIGN KEY (client_id) REFERENCES client (id) ON DELETE RESTRICT,
                CONSTRAINT fk_carrier_price_client_address
                    FOREIGN KEY (client_delivery_address_id) REFERENCES client_address (id) ON DELETE RESTRICT,
                CONSTRAINT fk_carrier_price_departure_site
                    FOREIGN KEY (departure_site_id) REFERENCES site (id) ON DELETE RESTRICT,
                CONSTRAINT fk_carrier_price_supplier
                    FOREIGN KEY (supplier_id) REFERENCES supplier (id) ON DELETE RESTRICT,
                CONSTRAINT fk_carrier_price_supplier_address
                    FOREIGN KEY (supplier_supply_address_id) REFERENCES supplier_address (id) ON DELETE RESTRICT,
                CONSTRAINT fk_carrier_price_delivery_site
                    FOREIGN KEY (delivery_site_id) REFERENCES site (id) ON DELETE RESTRICT,
                CONSTRAINT fk_carrier_price_created_by
                    FOREIGN KEY (created_by) REFERENCES "user" (id) ON DELETE SET NULL,
                CONSTRAINT fk_carrier_price_updated_by
                    FOREIGN KEY (updated_by) REFERENCES "user" (id) ON DELETE SET NULL
            )
            SQL);
        $this->addSql('CREATE INDEX idx_carrier_price_carrier ON carrier_price (carrier_id)');
        $this->addSql('CREATE INDEX idx_carrier_price_client ON carrier_price (client_id)');
        $this->addSql('CREATE INDEX idx_carrier_price_client_address ON carrier_price (client_delivery_address_id)');
        $this->addSql('CREATE INDEX idx_carrier_price_departure_site ON carrier_price (departure_site_id)');
        $this->addSql('CREATE INDEX idx_carrier_price_supplier ON carrier_price (supplier_id)');
        $this->addSql('CREATE INDEX idx_carrier_price_supplier_address ON carrier_price (supplier_supply_address_id)');
        $this->addSql('CREATE INDEX idx_carrier_price_delivery_site ON carrier_price (delivery_site_id)');
        $this->addSql('CREATE INDEX idx_carrier_price_created_by ON carrier_price (created_by)');
        $this->addSql('CREATE INDEX idx_carrier_price_updated_by ON carrier_price (updated_by)');

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

    // =================================================================
    // Helpers (identiques au M2 Version20260605130000)
    // =================================================================

    /**
     * Pose les 4 commentaires standardises Timestampable/Blamable sur une table,
     * en reutilisant le catalogue partage (source unique, 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,
        ));
    }
}
