<?php

namespace RtmBusiness\PostSync;

use DateTime;
use Exception;
use RtmBusiness\PostSync\Action\PermanentDeleteAction;
use RtmBusiness\PostSync\Action\PublishAction;
use RtmBusiness\PostSync\Action\SyncAction;

use RtmBusiness\PostSync\Action\TrashAction;
use RtmBusiness\PostSync\Action\RecoverAction;
use RtmBusiness\PostSync\Serializer\SyncableChildNormalizer;
use RtmBusiness\PostSync\Model\SyncData;
use RtmBusiness\PostSync\Model\SyncableChild;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer;
use WordpressModels\Traits\SingletonTrait;
use wpdb;

class SyncController
{

    use SingletonTrait;

    /**
     * @var wpdb Stores the wpdb instance
     */
    private wpdb $db;

    /**
     * @var string Stores the table name for the SyncData table
     */
    public string $tableName;

    private function __construct(protected Serializer $serializer = new Serializer())
    {
        global $wpdb;
        $this->db = $wpdb;
        $this->tableName = $wpdb->base_prefix . 'rtm_sync_data';

        $normalizers = [new ArrayDenormalizer(), new SyncableChildNormalizer(), new ObjectNormalizer()];
        $this->serializer = new Serializer($normalizers, [new JsonEncoder()]);

        $this->registerHook('rps_publish_post', new PublishAction());
        $this->registerHook('rps_permanent_delete_post', new PermanentDeleteAction());
        $this->registerHook('rps_recover_post', new RecoverAction());
        $this->registerHook('rps_trash_post', new TrashAction());

        $enabledPostTypes = get_site_option('postsync_enabled_post_types');
        foreach ($enabledPostTypes as $postType) {
            add_action("save_post_$postType", [$this, 'executePublishPostHook']);
        }

        add_action('before_delete_post', [$this, 'executePermanentDeleteHook']);
        add_action('trashed_post', [$this, 'executeTrashPostHook']);
        add_action('untrashed_post', [$this, 'executeRecoverHook']);

        //Posts table columns
        foreach ($enabledPostTypes as $postType) {
            add_filter("manage_{$postType}_posts_columns", [$this, 'addColumnsToPostsTable']);
            add_action("manage_{$postType}_posts_custom_column", [$this, 'renderColumn'], 10, 2);
        }

    }


    /**
     * Adds the postsync column to the posts table
     * @param $columns
     * @return mixed
     */
    public function addColumnsToPostsTable($columns): array
    {
        $columns['children'] = 'PostSync';
        return $columns;
    }

    /**
     * Renders the custom postsync column in the posts table
     * @param $column
     * @param $post_id
     * @return void
     */
    public function renderColumn($column, $post_id)
    {
        if ($column === 'children') {
            $parentOrChild = $this->parentOrChild(get_current_blog_id(), $post_id);
            if ($parentOrChild !== null) {
                $color = $parentOrChild == 'parent' ? 'green' : 'orange';
                echo '<span style="color: ' . $color . ';">' . ucfirst($parentOrChild) . '</span>';
            } else {
                echo 'X';
            }
        }

    }

    /**
     * Triggers the PublishAction for a post
     * @param int $postId
     * @return void
     */
    public function executePublishPostHook(int $postId)
    {
        do_action('rps_publish_post', $postId);
    }

    /**
     * Triggers the PermanentDeleteAction for a post
     * @param int $postId
     * @return void
     */
    public function executePermanentDeleteHook(int $postId)
    {
        do_action('rps_permanent_delete_post', $postId);
    }

    /**
     * Triggers the TrashAction for a post
     * @param int $postId
     * @return void
     */
    public function executeTrashPostHook(int $postId)
    {
        do_action('rps_trash_post', $postId);
    }

    /**
     * Trigger the RecoverAction for a post
     * @param int $postId
     * @return void
     */
    public function executeRecoverHook(int $postId)
    {
        do_action('rps_recover_post', $postId);
    }


    /**
     * Re-syncs all the posts that have a relationship stored in the database
     *
     * @return void
     */
    public function resyncPosts()
    {
        $allSyncRelations = $this->getAllSyncData();
        foreach ($allSyncRelations as $syncData) {
            $toSync = [];
            $slugOptions = [];
            foreach ($syncData->getLinkedChildren() as $child) {
                $toSync[] = $child->blogId;
                if ($child->isOverwriteSlug()) {
                    $slugOptions[] = $child->blogId;
                }
            }
            $_POST['children'] = $toSync;
            $_POST['overwriteDuplicateSlugs'] = implode(',', $slugOptions);
            $data = [
                'ID' => $syncData->parentPostId,
            ];
            //Resetting actions to simulate new request
            global $wp_actions;
            unset($wp_actions['rps_publish_post']);

            wp_update_post($data);
        }
    }

    /**
     * Deletes a post from a specific blog.
     *
     * @param int $blogId The ID of the blog from which to delete the post.
     * @param int $postId The ID of the post to be deleted.
     * @return void
     */
    public function deletePost(int $blogId, int $postId)
    {
        switch_to_blog($blogId);
        $this->updateChildDeletedOn($blogId, $postId, new DateTime());
        wp_delete_post($postId);
        restore_current_blog();
    }

    /**
     * Marks a post as deleted
     *
     * @param int $blogId The ID of the blog from which to delete the post.
     * @param int $postId The ID of the post to be deleted.
     * @return void
     */
    public function updateChildDeletedOn(int $blogId, int $postId, ?DateTime $deletedOn)
    {
        $data = $this->getSyncDataWithChild('post', $blogId, $postId);
        if ($data) {
            $child = new SyncableChild($blogId, $postId);
            $this->updateDeletedOn($data, $child, $deletedOn);
        }
    }

    /**
     * Retrieves all sync data from the database
     *
     * @return SyncData[] An array of retrieved SyncData objects from the database
     */
    public function getAllSyncData(): array
    {
        $query = $this->db->prepare("SELECT * FROM $this->tableName ORDER BY id");

        $results = $this->db->get_results($query);

        // Initialize an empty array to store the SyncData objects
        $syncDataObjects = [];

        // Iterate through each row in the result set and create a SyncData object
        foreach ($results as $result) {
            // Decode & Deserialize
            $children = $this->serializer->deserialize($result->children, SyncableChild::class . '[]', 'json');

            // Create a SyncData object and add it to the array
            $syncDataObjects[] = new SyncData($result->sync_type, $result->blog_id, $result->post_id, $children);
        }

        // Return the array of SyncData objects
        return $syncDataObjects;
    }

    /**
     * Registers a SyncAction to a hook
     *
     * @param string $hook The hook to bind on
     * @param SyncAction $syncAction The action that should be registered to the hook
     * @return void
     */
    public function registerHook(string $hook, SyncAction $syncAction)
    {
        add_action($hook, [$syncAction, 'execute'], 10, 1);
    }

    /**
     * Retrieves the sync data from the database or creates a new SyncData object if not exists
     *
     * @param string $syncType
     * @param int $blogId
     * @param int $postId
     * @return SyncData The retrieved syncdata from the database or new SyncData object if no entry was found
     */
    public function getSyncData(string $syncType, int $blogId, int $postId): SyncData
    {
        $query = $this->db->prepare(
            "SELECT * FROM $this->tableName WHERE blog_id = %d AND post_id = %s ORDER BY id",
            $blogId,
            $postId
        );

        $result = $this->db->get_row($query);

        // Check if the query returned a row, otherwise return new syncdata object
        if ($result == null) {
            return new SyncData($syncType, $blogId, $postId);
        }

        //Decode & Deserialize
        $children = $this->serializer->deserialize($result->children, SyncableChild::class . '[]', 'json');

        return new SyncData($syncType, $result->blog_id, $result->post_id, $children);
    }

    /**
     * Retrieves the first sync data from the database which contain a specific SyncableChild
     *
     * @param string $syncType
     * @param int $childBlogId
     * @param int $childPostId
     * @return SyncData|null The first retrieved syncdata from the database containing specific SyncableChild, or null if none found
     */
    public function getSyncDataWithChild(string $syncType, int $childBlogId, int $childPostId): ?SyncData
    {
        // Prepare the child string for the query
        $childString = "\"blogId\":$childBlogId,\"postId\":$childPostId,";

        $query = $this->db->prepare(
            "SELECT * FROM $this->tableName WHERE children LIKE %s ORDER BY id LIMIT 1",
            '%' . $this->db->esc_like($childString) . '%'
        );

        $result = $this->db->get_row($query);

        // Check if the query returned a row, otherwise return null
        if ($result == null) {
            return null;
        }

        //Decode & Deserialize
        $children = $this->serializer->deserialize($result->children, SyncableChild::class . '[]', 'json');

        $syncData = new SyncData($syncType, $result->blog_id, $result->post_id, $children);
        $syncData->setId($result->id);
        return $syncData;
    }

    /**
     * Checks if the given post is either a parent or child in any SyncData
     *
     * @param int $blogId
     * @param int $postId
     * @return bool True if the post is a parent or child, false otherwise
     */
    public function isParentOrChild(int $blogId, int $postId): bool
    {
        // Prepare the child string for the query
        $childString = "\"blogId\":$blogId,\"postId\":$postId,";

        $query = $this->db->prepare(
            "SELECT * FROM $this->tableName WHERE children LIKE %s OR (blog_id = %d AND post_id = %d) LIMIT 1",
            '%' . $this->db->esc_like($childString) . '%',
            $blogId,
            $postId
        );

        $result = $this->db->get_row($query);

        // Return true if the query returned a row, otherwise return false
        return $result != null;
    }

    /**
     * Checks if the given post is either a parent or child in any SyncData
     *
     * @param int $blogId
     * @param int $postId
     * @return string|null 'parent' or 'child' if the post is a parent or child, null otherwise
     */
    public function parentOrChild(int $blogId, int $postId): ?string
    {
        // Prepare the child string for the query
        $childString = "\"blogId\":$blogId,\"postId\":$postId,";

        // Query for parent
        $parentQuery = $this->db->prepare(
            "SELECT * FROM $this->tableName WHERE (blog_id = %d AND post_id = %d) LIMIT 1",
            $blogId,
            $postId
        );
        $parentResult = $this->db->get_row($parentQuery);

        if ($parentResult != null) {
            return 'parent';
        }

        // Query for child
        $childQuery = $this->db->prepare(
            "SELECT * FROM $this->tableName WHERE children LIKE %s LIMIT 1",
            '%' . $this->db->esc_like($childString) . '%'
        );
        $childResult = $this->db->get_row($childQuery);

        if ($childResult != null) {
            return 'child';
        }

        // If neither query returned a result, return null
        return null;
    }


    /**
     * Marks a SyncableChild as deleted and updates it in the database
     *
     * @param SyncData $syncData The sync data object
     * @param SyncableChild $child The child to mark as deleted
     * @return bool The result of the update operation
     * @throws Exception
     */
    private function updateDeletedOn(SyncData $syncData, SyncableChild $child, ?DateTime $deletedOn): bool
    {
        $child->setDeletedOn($deletedOn);

        $children = $syncData->getLinkedChildren();
        foreach ($children as $index => $syncChild) {
            if ($syncChild->blogId === $child->blogId && $syncChild->postId === $child->postId) {
                $children[$index] = $child;
                break;
            }
        }

        $childrenJson = json_encode($children);

        $query = $this->db->prepare(
            "UPDATE $this->tableName SET children = %s WHERE id = %d",
            $childrenJson,
            $syncData->id
        );

        // Execute the query and return the result
        return $this->db->query($query);
    }

    /**
     * Inserts or updates sync data into the database.
     *
     * If sync data for the specified blog and post ID already exists, it will be updated.
     * Otherwise, a new row will be inserted into the table.
     *
     * @param SyncData $syncData
     * @return bool Whether the insert or update was successful
     */
    public function insertOrUpdateSyncData(SyncData $syncData): bool
    {
        $data = $this->serializer->serialize($syncData->getLinkedChildren(), 'json');
        $existing_row = $this->db->get_row(
            $this->db->prepare(
                "SELECT * FROM $this->tableName WHERE sync_type = %s AND blog_id = %d AND post_id = %d",
                $syncData->syncType,
                $syncData->parentBlogId,
                $syncData->parentPostId
            )
        );
        if ($existing_row) {
            // If the row already exists, update the `children` column
            $update_data = ['children' => $data];
            $where = [
                'sync_type' => $syncData->syncType,
                'blog_id' => $syncData->parentBlogId,
                'post_id' => $syncData->parentPostId,
            ];
            $result = $this->db->update($this->tableName, $update_data, $where);
        } else {
            // If the row does not exist, insert a new row
            $insert_data = [
                'sync_type' => $syncData->syncType,
                'blog_id' => $syncData->parentBlogId,
                'post_id' => $syncData->parentPostId,
                'children' => $data,
            ];
            $result = $this->db->insert($this->tableName, $insert_data);
        }
        return $result !== false;
    }

    /**
     * Updates the sync data in the database
     * @param SyncData $syncData
     * @return bool Whether the update query was successful
     */
    public function updateSyncData(SyncData $syncData): bool
    {
        $data = $this->serializer->serialize($syncData->getLinkedChildren(), 'json');
        $where = [
            'sync_type' => $syncData->syncType,
            'blog_id' => $syncData->parentBlogId,
            'post_id' => $syncData->parentPostId,
        ];
        $updated = $this->db->update(
            $this->tableName,
            [
                'children' => $data,
            ],
            $where
        );
        return $updated !== false;
    }

    /**
     * Deletes the sync data from the database
     * @param int $blog_id The parent blog id
     * @param int|null $post_id Optional: The parent post id
     * @return bool Whether the deletion was successful
     */
    public function deleteSyncData(int $blog_id, int $post_id = null): bool
    {
        $where = ['blog_id' => $blog_id];
        if ($post_id !== null) {
            $where['post_id'] = $post_id;
        }
        return $this->db->delete($this->tableName, $where) !== false;
    }
}
