<?php

declare(strict_types=1);

/*
 * Chill is a software for social workers
 *
 * For the full copyright and license information, please view
 * the LICENSE file that was distributed with this source code.
 */

namespace Chill\PersonBundle\Actions\Remove;

use Chill\PersonBundle\Actions\ActionEvent;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Household\PersonHouseholdAddress;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Entity\Relationships\Relationship;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\ClassMetadata;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;

/**
 * Move or delete entities associated to a person to a new one, and delete the
 * old person. The data associated to a person (birthdate, name, ...) are left
 * untouched on the "new one".
 *
 * See `getSql` for details.
 */
class PersonMove
{
    public function __construct(
        private readonly EntityManagerInterface $em,
        private readonly PersonMoveManager $personMoveManager,
        private readonly EventDispatcherInterface $eventDispatcher,
    ) {}

    /**
     * Return the sql used to move or delete entities associated to a person to
     * a new one, and delete the old person. The data associated to a person
     * (birthdate, name, ...) are left untouched on the "new one".
     *
     * The accompanying periods associated to a person are always removed. The other
     * associated entity are updated: the new person id is associated to the entity.
     *
     * Optionnaly, you can ask for removing entity by passing them in $deleteEntities
     * parameters.
     *
     * The following events are triggered:
     * - `'CHILL_PERSON.DELETE_ASSOCIATED_ENTITY'` is triggered when an entity
     *     will be removed ;
     * - `'CHILL_PERSON.MOVE_ASSOCIATED_ENTITY'` is triggered when an entity
     *     will be moved ;
     *
     * Those events have the following metadata:
     *
     * - 'original_action' : always 'move' ;
     * - 'to': the person id to move ;
     *
     * @return type
     */
    public function getSQL(Person $from, Person $to, array $deleteEntities = [])
    {
        $sqls = [];
        $toDelete = \array_merge($deleteEntities, $this->getDeleteEntities());

        foreach ($this->em->getMetadataFactory()->getAllMetadata() as $metadata) {
            if ($metadata->isMappedSuperclass) {
                continue;
            }

            foreach ($metadata->getAssociationMappings() as $field => $mapping) {
                if ($this->personMoveManager->hasHandler($metadata->getName(), $field)) {
                    $sqls = \array_merge($sqls, $this->personMoveManager->getSqls($metadata->getName(), $field, $from, $to));
                    continue;
                }

                if (\in_array($mapping['sourceEntity'], $this->getIgnoredEntities(), true)) {
                    continue;
                }

                if (Person::class === $mapping['targetEntity'] and true === $mapping['isOwningSide']) {
                    if (\in_array($mapping['sourceEntity'], $toDelete, true)) {
                        $sql = $this->createDeleteSQL($metadata, $from, $field);
                        $event = new ActionEvent(
                            $from->getId(),
                            $metadata->getName(),
                            $sql,
                            ['to' => $to->getId(), 'original_action' => 'move']
                        );
                        $this->eventDispatcher->dispatch($event, ActionEvent::DELETE);
                        $sqls = \array_merge($sqls, $event->getPreSql(), [$event->getSqlStatement()], $event->getPostSql());
                    } else {
                        $sqls = \array_merge($sqls, $this->createMoveSQLs($metadata, $from, $to, $field));
                    }
                }
            }
        }

        $personMetadata = $this->em->getClassMetadata(Person::class);
        $sqls[] = sprintf(
            'DELETE FROM %s WHERE id = %d;',
            $this->getTableName($personMetadata),
            $from->getId()
        );

        return $sqls;
    }

    private function createDeleteSQL(ClassMetadata $metadata, Person $from, $field): string
    {
        $mapping = $metadata->getAssociationMapping($field);

        $conditions = [];

        if (array_key_exists('joinTable', $mapping)) {
            foreach ($mapping['joinTable']['joinColumns'] as $columns) {
                $conditions[] = sprintf('%s = %d', $columns['referencedColumnName'], $from->getId());
            }
        } elseif (array_key_exists('joinColumns', $mapping)) {
            foreach ($mapping['joinColumns'] as $columns) {
                $conditions[] = sprintf('%s = %d', $columns['name'], $from->getId());
            }
        }

        return sprintf(
            'DELETE FROM %s WHERE %s;',
            $this->getTableName($metadata),
            \implode(' AND ', $conditions)
        );
    }

    private function createMoveSQLs($metadata, Person $from, Person $to, $field): array
    {
        $mapping = $metadata->getAssociationMapping($field);

        // Set part of the query, aka <here> in  "UPDATE table SET <here>                                                                                                                                                                            "
        $sets = [];
        $conditions = [];
        $tableName = '';

        if (array_key_exists('joinTable', $mapping)) {
            // there is a join_table: we have to find conflict
            $tableName = (null !== ($mapping['joinTable']['schema'] ?? null) ? $mapping['joinTable']['schema'].'.' : '')
                .$mapping['joinTable']['name'];

            $sqlInsert = sprintf(
                'INSERT INTO %s (%s, %s) SELECT %d, %s FROM %s WHERE %s = %d ON CONFLICT DO NOTHING;',
                $tableName,
                $mapping['joinTable']['inverseJoinColumns'][0]['name'], // person_id
                $mapping['joinTable']['joinColumns'][0]['name'], // something_else_id
                $to->getId(),
                $mapping['joinTable']['joinColumns'][0]['name'], // something_else_id
                $tableName,
                $mapping['joinTable']['inverseJoinColumns'][0]['name'], // person_id
                $from->getId()
            );

            $deleteSql = sprintf(
                'DELETE FROM %s WHERE %s = %d;',
                $tableName,
                $mapping['joinTable']['inverseJoinColumns'][0]['name'], // person_id
                $from->getId()
            );

            foreach ($mapping['joinTable']['inverseJoinColumns'] as $columns) {
                $sets[] = sprintf('%s = %d', $columns['name'], $to->getId());
            }

            foreach ($mapping['joinTable']['inverseJoinColumns'] as $columns) {
                $conditions[] = sprintf('%s = %d', $columns['name'], $from->getId());
            }

            return [
                $sqlInsert, $deleteSql,
            ];
        }
        if (array_key_exists('joinColumns', $mapping)) {
            $tableName = $this->getTableName($metadata);
            foreach ($mapping['joinColumns'] as $columns) {
                $sets[] = sprintf('%s = %d', $columns['name'], $to->getId());
            }

            foreach ($mapping['joinColumns'] as $columns) {
                $conditions[] = sprintf('%s = %d', $columns['name'], $from->getId());
            }
        }

        return [sprintf(
            'UPDATE %s SET %s WHERE %s;',
            $tableName,
            \implode(' ', $sets),
            \implode(' AND ', $conditions)
        )];
    }

    /**
     * return an array of classes where entities should be deleted
     * instead of moved.
     */
    private function getDeleteEntities(): array
    {
        return [
            //            AccompanyingPeriod\AccompanyingPeriodWork::class,
            Relationship::class,
        ];
    }

    private function getIgnoredEntities(): array
    {
        return [
            Person\PersonCurrentAddress::class,
            PersonHouseholdAddress::class,
            Person\PersonCenterCurrent::class,
        ];
    }

    /**
     * get the full table name with schema if it exists.
     */
    private function getTableName(ClassMetadata $metadata): string
    {
        return empty($metadata->getSchemaName()) ?
                $metadata->getTableName() :
                $metadata->getSchemaName().'.'.$metadata->getTableName();
    }
}
