<?php

declare(strict_types=1);

namespace DoctrineMigrations;

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

/**
 * ERP-78 — Refonte de la taxonomie Categories (M0/M1).
 *
 * Modele AVANT (merge via Version20260527164000 + Version20260601000000) :
 * DISTRIBUTEUR / COURTIER / SECTEUR / AUTRE sont des `category_type`.
 *
 * Modele APRES (decision produit 01/06) :
 *  - UN SEUL `category_type` : CLIENT (code CLIENT, label « Client ») ;
 *  - Distributeur / Courtier / Secteur / Autre (+ categories metier fines)
 *    deviennent des `Category` rattachees au type CLIENT ;
 *  - filtrage metier sur un `code` stable porte par la `Category` (et non plus
 *    par le type) : on reporte les codes DISTRIBUTEUR / COURTIER sur la categorie
 *    correspondante. RG-1.03 (distributor/broker) et RG-1.29 (categorie interdite
 *    sur adresse) s'appuient desormais sur `category.code`.
 *
 * Migration CORRECTIVE et NOUVELLE : la migration mergee Version20260601000000
 * (qui a pu tourner en CI / chez d'autres devs) n'est PAS editee.
 *
 * Namespace racine `DoctrineMigrations` (regle ABSOLUE n°11) et NON modulaire
 * Catalog : avec plusieurs migrations_paths, Doctrine Migrations 3.x trie par
 * FQCN alphabetique (AlphabeticalComparator). Introduire la 1re migration
 * modulaire `App\Module\Catalog\...` la ferait trier AVANT toutes les
 * `DoctrineMigrations\...` sur base vide -> elle s'executerait avant la creation
 * des tables et le seed dont elle depend. Le namespace racine garantit l'ordre
 * par timestamp.
 *
 * Idempotence : `ADD COLUMN IF NOT EXISTS`, `INSERT ... ON CONFLICT` / guards
 * `NOT EXISTS`, `CREATE UNIQUE INDEX IF NOT EXISTS`. En prod la table `category`
 * est vide (aucune fixture metier) : l'ajout de `code NOT NULL` est sur. En
 * dev/test, le purger Doctrine vide `category`/`category_type` avant les
 * fixtures, qui reproduisent le meme etat final (cf. CategoryTypeFixtures /
 * CategoryFixtures).
 */
final class Version20260602100000 extends AbstractMigration
{
    /**
     * Categories systeme reportees depuis les anciens types : nom => code.
     * Le code est la cle metier stable (RG-1.03 / RG-1.29).
     */
    private const array SYSTEM_CATEGORIES = [
        'Distributeur' => 'DISTRIBUTEUR',
        'Courtier'     => 'COURTIER',
        'Secteur'      => 'SECTEUR',
        'Autre'        => 'AUTRE',
    ];

    /** Anciens codes de `category_type` devenus inutiles. */
    private const array LEGACY_TYPE_CODES = ['DISTRIBUTEUR', 'COURTIER', 'SECTEUR', 'AUTRE'];

    public function getDescription(): string
    {
        return 'ERP-78 : Category.code + type unique CLIENT (categories Distributeur/Courtier/Secteur/Autre codees, anciens types supprimes).';
    }

    public function up(Schema $schema): void
    {
        // 1. Colonne `code` (nullable d'abord pour pouvoir backfiller, NOT NULL ensuite).
        $this->addSql('ALTER TABLE category ADD COLUMN IF NOT EXISTS code VARCHAR(50) DEFAULT NULL');

        // 2. Type unique CLIENT (idempotent via l'index unique uq_category_type_code).
        $this->addSql(<<<'SQL'
            INSERT INTO category_type (code, label) VALUES ('CLIENT', 'Client')
            ON CONFLICT (code) DO NOTHING
            SQL);

        // 3. Re-pointer toute categorie pre-existante (rattachee a un ancien type)
        //    vers le type CLIENT, en lui donnant un code derive du nom si absent.
        //    En prod la table est vide -> no-op ; defensif pour les envs qui
        //    auraient deja seede des categories sous les anciens types. Le slug
        //    SQL est le miroir EXACT de CategoryCodeGenerator::slugify (cf.
        //    CategoryCodeSql + CategoryCodeSqlSlugTest) : un nom accentue produit
        //    le meme code que la generation applicative (« Independant » ->
        //    INDEPENDANT, et non IND_PENDANT).
        $this->addSql(
            'UPDATE category c '
            ."SET category_type_id = (SELECT id FROM category_type WHERE code = 'CLIENT'), "
            .'code = COALESCE(c.code, '.CategoryCodeSql::slugExpression('c.name').') '
            .'WHERE c.category_type_id IN (SELECT id FROM category_type WHERE code IN (:legacyCodes))',
            ['legacyCodes' => self::LEGACY_TYPE_CODES],
            ['legacyCodes' => \Doctrine\DBAL\ArrayParameterType::STRING],
        );

        // 4. Creer les 4 categories systeme sous CLIENT (si leur code est libre
        //    parmi les actifs). created_at/updated_at NOT NULL -> now() ; le blame
        //    reste null (seed hors contexte HTTP, libelle « Systeme » cote front).
        foreach (self::SYSTEM_CATEGORIES as $name => $code) {
            $this->addSql(<<<'SQL'
                INSERT INTO category (name, code, category_type_id, created_at, updated_at)
                SELECT :name, :code, ct.id, NOW(), NOW()
                FROM category_type ct
                WHERE ct.code = 'CLIENT'
                  AND NOT EXISTS (
                      SELECT 1 FROM category c WHERE c.code = :code AND c.deleted_at IS NULL
                  )
                SQL, ['name' => $name, 'code' => $code]);
        }

        // 5. Backfill defensif : toute categorie encore sans code recoit un slug
        //    de son nom (garantit que le SET NOT NULL passe). Meme expression de
        //    slug fidele au generateur applicatif (CategoryCodeSql).
        $this->addSql(
            'UPDATE category SET code = '.CategoryCodeSql::slugExpression('name').' WHERE code IS NULL',
        );

        // 6. Index unique partiel sur le code parmi les actifs (non exprimable en
        //    ORM : recree aussi dans `test-db-setup` apres schema:update).
        $this->addSql('CREATE UNIQUE INDEX IF NOT EXISTS uq_category_code ON category (code) WHERE deleted_at IS NULL');

        // 7. Code desormais obligatoire.
        $this->addSql('ALTER TABLE category ALTER COLUMN code SET NOT NULL');

        // 8. Documentation SQL (regle ABSOLUE n°12). Dollar-quoting Postgres.
        $this->addSql(<<<'SQL'
            COMMENT ON COLUMN category.code IS $_$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.$_$
            SQL);

        // 9. Supprimer les anciens types devenus orphelins (aucune categorie ne
        //    les reference plus apres le re-pointage de l'etape 3). Le guard
        //    NOT EXISTS evite de casser sur la FK RESTRICT category.category_type_id.
        $this->addSql(<<<'SQL'
            DELETE FROM category_type
            WHERE code IN (:legacyCodes)
              AND NOT EXISTS (
                  SELECT 1 FROM category c WHERE c.category_type_id = category_type.id
              )
            SQL, ['legacyCodes' => self::LEGACY_TYPE_CODES], ['legacyCodes' => \Doctrine\DBAL\ArrayParameterType::STRING]);

        // 10. Realigner la doc SQL de client_address_category (migration mergee
        //     Version20260601000000, non editable) sur le nouveau modele RG-1.29.
        $this->addSql(<<<'SQL'
            COMMENT ON TABLE client_address_category IS $_$Jointure M2M client_address <-> category — codes DISTRIBUTEUR/COURTIER interdits sur une adresse (RG-1.29).$_$
            SQL);
        $this->addSql(<<<'SQL'
            COMMENT ON COLUMN client_address_category.category_id IS $_$FK -> category.id, ON DELETE RESTRICT — categorie d adresse (tout code sauf DISTRIBUTEUR/COURTIER, RG-1.29).$_$
            SQL);
    }

    public function down(Schema $schema): void
    {
        // Best-effort : rollback du modele CLIENT vers les 4 anciens types.
        // 1. Retirer l'index unique sur le code.
        $this->addSql('DROP INDEX IF EXISTS uq_category_code');

        // 2. Recreer les 4 anciens types.
        $this->addSql(<<<'SQL'
            INSERT INTO category_type (code, label) VALUES
                ('DISTRIBUTEUR', 'Distributeur'),
                ('COURTIER',     'Courtier'),
                ('SECTEUR',      'Secteur'),
                ('AUTRE',        'Autre')
            ON CONFLICT (code) DO NOTHING
            SQL);

        // 3. Re-pointer les categories systeme (par code) vers leur type d'origine.
        //    Codes inlines : constantes controlees (self::SYSTEM_CATEGORIES), pas
        //    d'entree utilisateur — evite le binding d'un parametre nomme repete.
        foreach (self::SYSTEM_CATEGORIES as $name => $code) {
            $this->addSql(sprintf(
                "UPDATE category SET category_type_id = (SELECT id FROM category_type WHERE code = '%s') WHERE code = '%s'",
                $code,
                $code,
            ));
        }

        // 4. Supprimer le type CLIENT s'il ne reference plus aucune categorie.
        $this->addSql(<<<'SQL'
            DELETE FROM category_type
            WHERE code = 'CLIENT'
              AND NOT EXISTS (
                  SELECT 1 FROM category c WHERE c.category_type_id = category_type.id
              )
            SQL);

        // 5. Retirer la colonne code (les categories libres sans type d'origine
        //    restent sous CLIENT si encore presentes — rollback uniquement
        //    pertinent en prod ou seules les 4 categories systeme existent).
        $this->addSql('ALTER TABLE category DROP COLUMN IF EXISTS code');
    }
}
