<?php

namespace WordpressModels\ORM\EntityListener;

use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener;
use Doctrine\Bundle\DoctrineBundle\Attribute\AsEntityListener;
use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Event\OnFlushEventArgs;
use Doctrine\ORM\Event\PostFlushEventArgs;
use Doctrine\ORM\Event\PreFlushEventArgs;
use Doctrine\ORM\Events;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\MappingException;
use WordpressModels\DependencyInjection\HookAttributes\Attributes\Action;
use WordpressModels\ORM\Entity\Post;
use WordpressModels\ORM\Entity\User;

/**
 * Doctrine entity listener for the Post entity.
 *
 * Passes changes to Wordpress built-in functions. All handler methods cancel the entity from the UnitOfWork.
 *
 * Also refreshes entities when a the `save_post` action is triggered.
 *
 */
#[AsDoctrineListener(event: 'onFlush'), AsDoctrineListener(event: 'postFlush')]
#[AsEntityListener(event: 'preFlush', entity: Post::class)]
class PostListener implements EventSubscriber
{

    /**
     * List of columns mapped to the WP_Post object.
     *
     * Date fields are not included, as they are handled by wordpress.
     * @var string[]
     */
    const POST_COLUMNS = [
        'post_author',
        'post_content',
        'post_content_filtered',
        'post_title',
        'post_excerpt',
        'post_status',
//        'post_type', // handled by the discriminator
        'comment_status',
        'ping_status',
        'post_password',
        'to_ping',
        'pinged',
        'post_parent',
        'menu_order',
        'guid',
    ];

    /**
     * @var Post[]
     */
    private array $postsToRefresh = [];

    public function __construct()
    {
    }

    private function formatValue($fieldName, $value)
    {
        $integerFields = [
            'post_author' => fn(?User $user) => $user?->getId() ?? 0,
            'post_parent' => fn(?Post $post) => $post?->getId() ?? 0,
            'menu_order' => 'intval',
        ];

        $mapper = $integerFields[$fieldName] ?? null;
        if ($mapper) {
            return $mapper($value);
        }

        return $value;
    }

    /**
     * Insert or update the post in the database.
     *
     * @param Post $post
     * @param PreFlushEventArgs $args
     * @return void
     * @throws MappingException
     */
    public function preFlush(Post $post, PreFlushEventArgs $args)
    {
        $manager = $args->getObjectManager();

        $cmd = $manager->getClassMetadata($post::class);
        $unitOfWork = $args->getObjectManager()->getUnitOfWork();
        $id = $post->getId();
        if (!$id) {
            $wpPost = new \WP_Post(new \stdClass());
            $wpPost->post_type = $cmd->discriminatorValue;
        } else {
            $wpPost = get_post($id);
        }

        $wpPost = $this->formatWpPost($cmd, $post, $wpPost);
        if (!$id) {
            $id = wp_insert_post($wpPost->to_array(), true);
            $unitOfWork->assignPostInsertId($post, $id);
        } else {
            $id = wp_update_post($wpPost->to_array(), true);
            $unitOfWork->assignPostInsertId($post, $id);
        }

        // temporary store the post to refresh it after the flush
        $this->postsToRefresh[] = $post;
    }

    /**
     * Clear all scheduled insertions and updates for the Post entity.
     *
     * @param OnFlushEventArgs $args
     * @return void
     */
    public function onFlush(OnFlushEventArgs $args)
    {
        // cancel the entity from the UnitOfWork and the scheduled insertions and updates
        $args->getObjectManager()->getUnitOfWork()->clear(Post::class);
    }

    /**
     * Ensure all previously tracked entities are refreshed after the flush.
     *
     * @param PostFlushEventArgs $args
     * @return void
     */
    public function postFlush(PostFlushEventArgs $args)
    {
        $objectManager = $args->getObjectManager();
        $unitOfWork = $objectManager->getUnitOfWork();
        foreach ($this->postsToRefresh as $post) {
            $unitOfWork->assignPostInsertId($post, $post->getId());
            $unitOfWork->refresh($post);
        }
        $this->postsToRefresh = [];
    }

    /**
     * @param ClassMetadata $classMetadata
     * @param Post $post
     * @param \WP_Post $wpPost
     * @return \WP_Post
     * @throws MappingException
     */
    public function formatWpPost(ClassMetadata $classMetadata, Post $post, \WP_Post $wpPost): \WP_Post
    {
        foreach (self::POST_COLUMNS as $column) {
            $fieldName = $classMetadata->getFieldForColumn($column);
            if ($fieldName) {
                $value = $classMetadata->getFieldValue($post, $fieldName);
                $value = $this->formatValue($column, $value);
                $wpPost->$column = $value;
            }
        }

        $wpPost->post_type = $classMetadata->discriminatorValue;

        return $wpPost;
    }

    /**
     * Sync post state for entities that are managed by Doctrine.
     *
     * @param int $postId
     * @param EntityManagerInterface $entityManager
     * @return void
     */
    #[Action('save_post', accepted_args: 1)]
    public function onPostSave(int $postId, EntityManagerInterface $entityManager)
    {
        // todo: we might need to handle the case where the post type is changed
//        $postClass = $entityManager->getClassMetadata(Post::class)->discriminatorMap[$post->post_type] ?? null;

        // refresh the entity if it is managed by Doctrine
        $entity = $entityManager->getUnitOfWork()->tryGetById($postId, Post::class);
        if ($entity) {
            $entityManager->refresh($entity);
        }
    }

    public function getSubscribedEvents()
    {
        return [
            Events::onFlush,
            Events::postFlush,
        ];
    }
}