<?php

namespace Entity\Model\Metadata;

use API\GovZaCsd\GovZaCsdPartenaire;
use Entity\Model\AoEntet;
use Entity\Model\FacDematFlow;
use Entity\Model\Fourniss;
use Entity\Model\Paravrac;
use Helpers\Outil\ArrayHelper;
use Helpers\QueryBuilder\QueryBuilder;
use Modules\Facture\Repository\FactureDematRepo;

class EntityMetaQb
{
    protected \PDO $pdo;

    protected ?QueryBuilder $queryBuilder = null;

    const MAP_ENTITY = [
        Paravrac::class => 'APP',
        Fourniss::class => 'FOUR',
        AoEntet::class => 'AO_E',
        FacDematFlow::class => 'FADF',
        FactureDematRepo::class => 'FADP',// TODO : create a class for the partenaire instead of using FactureDematRepo
        GovZaCsdPartenaire::class => 'GZCP',
    ];

    const MAP_STRUCT = [
        PartnFournissDemModif::class => 'PFDM',
        PartnFournissExtId::class => 'PFEI',
        AoEntetExt::class => '', // Root
        FacDematBatchData::class => 'FDBD',
        FacDematMappage::class => 'MAPP',
        FacDematRefus::class => 'FDRR',
        BatchGovZaCsdData::class => '',
    ];

//_____________________________________________________________________________
    
    public function __construct(\PDO $pdo)
    {
        $this->pdo = $pdo;
    }

//______________________________________________________________________________
// Accessor

    public function getQueryBuilder() :QueryBuilder
    {
        if ($this->queryBuilder == null) {
            $this->queryBuilder = new class($this->pdo) extends QueryBuilder {
                protected string $table = 'ENTITYMETA with (nolock)';
    
                /** @var array<string> */
                protected array $primKey = [
                    'ENTITY_TYPE',
                    'ENTITY_ID',
                    'STRUCT_TYPE',
                    'STRUCT_ID',
                    'FIELD_KEY',
                ];
                public function __construct(\PDO $pdo)
                {
                    $this->pdo = $pdo;
                }
            };
        }
        return $this->queryBuilder;
    }

//______________________________________________________________________________
// Modifier

    /**
     * @param class-string<QueryBuilder> $parentClass
     * @param string $parentId
     * @param class-string<EntityMeta> $structClass
     * @param int $structId
     * @return void
     */
    public function delete(
        string $parentClass,
        string $parentId,
        string $structClass,
        int $structId
    ) :void {
        $this->getQueryBuilder()->delete([
            'ENTITY_TYPE' => self::getEntityType($parentClass),
            'ENTITY_ID' => $parentId,
            'STRUCT_TYPE' => self::getStructKey($structClass),
            'STRUCT_ID' => $structId,
        ]);
    }

    /**
     * @param class-string<QueryBuilder> $parentClass
     * @param string $parentId
     * @param class-string<EntityMeta> $structClass
     * @return void
     */
    public function deleteByStruct(
        string $parentClass,
        string $parentId,
        string $structClass
    ) :void {
        $this->getQueryBuilder()->delete([
            'ENTITY_TYPE' => self::getEntityType($parentClass),
            'ENTITY_ID' => $parentId,
            'STRUCT_TYPE' => self::getStructKey($structClass),
        ]);
    }

    
    /**
     * @param array<EntityMeta> $listEntityMeta
     */
    public function persist(array $listEntityMeta) :void
    {
        // Merge
        $listListRow = array_map(function ($meta) {
            return $this->normalize($meta);
        }, $listEntityMeta);

        $isIntransac = $this->pdo->inTransaction();
        if (! $isIntransac) {
            $this->pdo->beginTransaction();
        }
        foreach ($listListRow as $listRow) {
            foreach ($listRow as $row) {
                $this->getQueryBuilder()->merge($row);// TODO : merge multiple entity
            }
        }
        if (! $isIntransac) {
            $this->pdo->commit();
        }

        // Delete fields
        foreach ($listEntityMeta as $entity) {
            foreach ($entity->toBeDeleted() as $field) {
                $this->getQueryBuilder()->delete([// TODO : merge query
                    'ENTITY_TYPE' => $entity->getEntityType(),
                    'ENTITY_ID' => $entity->getEntityId(),
                    'STRUCT_TYPE' => self::getStructKey(get_class($entity)),
                    'STRUCT_ID' => $entity->getStructId(), // TODO : handle STRUCT_ID == null -> autoincrement
                    'FIELD_KEY' => $field,
                ]);
            }
        }
    }

//______________________________________________________________________________
// Fetcher

    /**
     * @template T of EntityMeta
     * @param class-string<QueryBuilder>|string $parentClass
     * @param class-string<T> $structClass
     * @return T|null
     */
    public function find(
        string $parentClass,
        string $parentId,
        string $structClass,
        int $structId
    ) :?EntityMeta {
        /** @var T */
        return $this->denormalize($this->getQueryBuilder()->findAll([
            'ENTITY_TYPE' => self::getEntityType($parentClass),
            'ENTITY_ID' => $parentId,
            'STRUCT_TYPE' => self::getStructKey($structClass),
            'STRUCT_ID' => $structId,
        ]))[0] ?? null;
    }

    /**
     * @template T of EntityMeta
     * @param class-string<T> $structClass
     * @return array<T>
     */
    public function findAllByStruct(string $structClass) :array
    {
        /** @var array<T> */
        return $this->denormalize($this->getQueryBuilder()->findAll([
            'STRUCT_TYPE' => self::getStructKey($structClass),
        ]));
    }

    /**
     * @param class-string<EntityMeta> $structClass
     */
    public function countAllByStruct(string $structClass) :int
    {
        return $this->getQueryBuilder()->count([
            'STRUCT_TYPE' => self::getStructKey($structClass),
        ], [], ['ENTITY_TYPE', 'ENTITY_ID', 'STRUCT_ID']);
    }

    /**
     * @template T of EntityMeta
     * @param class-string<QueryBuilder>|string $entityType
     * @param class-string<T> $structClass
     * @return array<T>
     */
    public function findByEntityByStruct(
        string $entityType,
        string $entityId,
        string $structClass
    ) :array {
        /** @var array<T> */
        return $this->denormalize($this->getQueryBuilder()->findAll([
            'ENTITY_TYPE' => self::getEntityType($entityType),
            'ENTITY_ID' => $entityId,
            'STRUCT_TYPE' => self::getStructKey($structClass),
        ]));
    }

//______________________________________________________________________________
// Sub-routine

    /**
     * @param \Entity\Model\Metadata\EntityMeta $entity
     * @return array<mixed|string>[]
     */
    private function normalize(EntityMeta $entity)
    {
        $a = iterator_to_array($entity);
        return array_map(function ($key, $value) use ($entity) {
            return [
                'ENTITY_TYPE' => $entity->getEntityType(),
                'ENTITY_ID' => $entity->getEntityId(),
                'STRUCT_TYPE' => self::getStructKey(get_class($entity)),
                'STRUCT_ID' => $entity->getStructId(), // TODO : handle null -> autoincrement
                'FIELD_KEY' => $key,
                'VALUE' => $value,
            ];
        }, array_keys($a), $a);
    }

    /**
     * @param array<array<string>> $listRow
     * @return array<EntityMeta>
     */
    private function denormalize(array $listRow) :array
    {
        $mapListField = ArrayHelper::indexBy(
            array_map(function ($row) {
                return $row + ['_id' => implode('|', [
                    $row['ENTITY_TYPE'],
                    $row['ENTITY_ID'],
                    $row['STRUCT_TYPE'],
                    $row['STRUCT_ID'],
                ]) ];
            }, $listRow),
            '_id'
        );
        return array_values(array_map(function ($listRow) {
            $row = reset($listRow);
            if ($row === false) {
                throw new \Exception('invalid listrow');
            }
            $class = self::getStructClass($row['STRUCT_TYPE']);
            $reflector = new \ReflectionClass($class);
            return $reflector->newInstanceWithoutConstructor()->hydrate(
                $row['ENTITY_TYPE'],
                $row['ENTITY_ID'],
                $row['STRUCT_ID'],
                array_column($listRow, 'VALUE', 'FIELD_KEY')
            );
        }, $mapListField));
    }

//______________________________________________________________________________
// Tools

    /**
     * @return class-string<EntityMeta>
     */
    public static function getStructClass(string $structId) :string
    {
        return array_flip(self::MAP_STRUCT)[ $structId ];
    }

    /**
     * @param class-string<EntityMeta> $class
     */
    public static function getStructKey(string $class) :string
    {
        if ($class == 'APP') {
            return 'APP';
        }
        return self::MAP_STRUCT[ $class ];
    }
    /**
     * @param class-string<QueryBuilder>|string $class
     */
    public static function getEntityType(string $class) :string
    {
        return self::MAP_ENTITY[ $class ];
    }
}
