<?php

use GuzzleHttp\{Client, HandlerStack, Middleware, RetryMiddleware};
use OpenAPI\Client\Api\AgendaApi;
use OpenAPI\Client\Api\ZiberTeamZoneApi;
use OpenAPI\Client\Api\NewsApi;
use OpenAPI\Client\Configuration;
use OpenAPI\Client\Model\AgendaItemDto;
use OpenAPI\Client\Model\AgendaItemDtoPagedListDto;
use OpenAPI\Client\Model\ZiberTeamZoneDtoPagedListDto;
use OpenAPI\Client\Model\NewsArticleDto;
use OpenAPI\Client\Model\NewsArticleDtoPagedListDto;
use OpenAPI\Client\Model\TeamMemberDto;
use Psr\Log\LoggerInterface;


final class Zbr_Loader
{
    const DATE_FORMAT = "Y-m-d H:i:s";

    protected Configuration $config;
    protected Client $client;
    protected LoggerInterface $logger;

    public function __construct(Client $client, Configuration $config, LoggerInterface $logger)
    {
        $this->config = $config;
        $this->client = $client;
        $this->logger = $logger;
    }


    public function load_data($run_id)
    {
        global $wpdb;

        $tableName = $wpdb->prefix . 'zbr_loading_logs';

        $wpdb->insert($tableName, array(
            'createdAt' => $this->get_now(),
            'status' => 0,
        ));
        $id = $wpdb->insert_id;

        $where = array(
            'id' => $id,
        );

        try {
            $promises = array(
                'news' => $this->load_news(),
                'agenda' => $this->load_agenda(),
                'zones' => $this->load_zones(),
            );

            GuzzleHttp\Promise\Utils::unwrap($promises);

            $this->logger->info("all done");
            $wpdb->update($tableName, array(
                'status' => 10,
                'finishedAt' => $this->get_now(),
            ), $where);
        } catch (Throwable $exception) {
            $this->logger->error('Failed to load API data', array("exception" => $exception->getMessage(), "stackTrace" => $exception->getTraceAsString()));
            $wpdb->update($tableName, array(
                'status' => 100,
                'finishedAt' => $this->get_now(),
                'exception' => $exception->getMessage(),
            ), $where);
        }
    }

    private function get_now(): string
    {
        return date(self::DATE_FORMAT);
    }

    private function load_news(): GuzzleHttp\Promise\PromiseInterface
    {
        $api = new NewsApi($this->client, $this->config);

        return $api->getNewsAsync(null, 100)
            ->then(function (NewsArticleDtoPagedListDto $items) {
                $this->persist_news_items($items->getData());
            });
    }

    // TODO: handle the exceptions
    private function load_agenda(): GuzzleHttp\Promise\PromiseInterface
    {
        $api = new AgendaApi($this->client, $this->config);
        return $api->getAgendaItemsAsync()
            ->then(function (AgendaItemDtoPagedListDto $items) {
                $this->persist_agenda_items($items->getData());
            });
    }

    private function load_zones(): GuzzleHttp\Promise\PromiseInterface
    {
        $api = new ZiberTeamZoneApi($this->client, $this->config);
        $this->logger->info('loading zones');
        return $api->getZiberTeamZoneAsync()
            ->then(function (ZiberTeamZoneDtoPagedListDto $items) {
                return $this->persist_zones($items->getData());
            });
    }

    /** @var GnapZoneDtoPagedListDto[] $articles */
    private function persist_zones(array $zones): GuzzleHttp\Promise\PromiseInterface
    {
        $promises = [];


        $this->logger->info('persisting zones: ' . count($zones) . ' found');

        $zone_ids = array_map(function ($zone) {
            return $zone->getId();
        }, $zones);

        /** @var ZiberTeamZoneDto $zone */
        foreach ($zones as $zone) {
            // get post by meta:native_id
            $query = get_posts([
                'post_type' => 'zbr_zone',
                'meta_query' => [
                    [
                        'key' => '_zbr_native_id',
                        'value' => $zone->getId(),
                    ],
                ],
            ]);

            $post_id = 0;
            if (!empty($query)) {
                $post_id = $query[0]->ID;
            }

            $description = $zone->getDescription();
            if (!isset($description)) {
                $description = $zone->getName();
            }

            // if exists, update by id,
            // else insert
            $inserted = wp_insert_post(array(
                'ID' => $post_id,
                'import_id' => $zone->getId(),
                'post_type' => 'zbr_zone',
                'post_title' => $zone->getName(),
                'post_content' => $description,
                'post_modified' => $zone->getUpdatedAt()->format(self::DATE_FORMAT),
                'post_status' => 'publish',
                'comment_status' => 'closed',
                'ping_status' => 'closed',
                'meta_input' => array(
                    "_zbr_native_id" => $zone->getId(),
                    "_zbr_address" => $zone->getAddress(),
                    "_zbr_email_address" => $zone->getEmailAddress(),
                    "_zbr_telephone" => $zone->getTelephone(),
                    "_zbr_identity_color" => $zone->getIdentityColor(),
                    "_zbr_logo_background_color" => $zone->getLogoBackgroundColor()
                ),
            ));

            if ($inserted) {
                $post_id = $inserted;
            }

            $mediaFiles = array_merge($zone->getLogo(), $zone->getMedia());
            $files = array_map(function ($file) {
                return $file->getSource();
            }, $mediaFiles);

            // add the attachments

            $this->logger->info('Got attachments ' . count($zone->getAttachments()));
            $this->handle_media($files, $post_id);

            $promise = $this->load_zone_team($zone->getId(), $post_id);

            array_push($promises, $promise);
        }

        $this->purge_old_zones($zone_ids);

        return GuzzleHttp\Promise\Utils::all($promises);
    }

    private function load_zone_team($zone_id, $post_id): GuzzleHttp\Promise\PromiseInterface
    {
        $api = new ZiberTeamZoneApi($this->client, $this->config);
        $this->logger->info('loading team for zone ' . $zone_id);
        return $api->getTeamByZiberTeamZoneIdAsync($zone_id)
            ->then(function (array $items) use ($zone_id) {


                $team_ids = array_map(function ($item) {
                    return $item->getId();
                }, $items);

                /** @var TeamMemberDto $article */
                foreach ($items as $item) {
                    $this->logger->info("Importing team member {name}", array("name" => $item->getName()));

                    // get post by meta:native_id
                    $query = get_posts([
                        'post_type' => 'zbr_zone_team_member',
                        'meta_query' => [
                            [
                                'key' => '_zbr_native_id',
                                'value' => $item->getId(),
                            ],
                        ],
                    ]);

                    $isNew = true;
                    $post_id = 0;
                    if (!empty($query)) {
                        $post_id = $query[0]->ID;
                        $isNew = false;
                    }

                    $roles = array_map(function($role) {
                        return $role->getRole();
                    }, $item->getRoles());

                    // if exists, update by id,
                    // else insert
                    $inserted = wp_insert_post(array(
                        'ID' => $post_id,
                        'post_type' => 'zbr_zone_team_member',
                        'post_title' => $item->getName(),
                        'post_content' => $item->getBiography() ?? '',
                        'post_status' => 'publish',
                        'comment_status' => 'closed',
                        'ping_status' => 'closed',
                        'meta_input' => array(
                            "_zbr_native_id" => $item->getId(),
                            "_zbr_zone_id" => $zone_id,
                            "_roles" => implode(",", $roles),
                        ),
                    ));
                    if ($inserted) {
                        $post_id = $inserted;
                    }

                    if ($isNew) {
                        $this->handle_media([$item->getAvatar()], $post_id);
                    }
                }

                $this->purge_old_team($team_ids);
            });
    }

    /** @var NewsArticleDto[] $articles */
    private function persist_news_items(array $articles)
    {
        // TODO: query all of the current posts and see which ones need to be deleted
        $isNew = true;
        $isModified = false;
        $article_ids = array_map(function ($article) {
            return $article->getId();
        }, $articles);

        /** @var NewsArticleDto $article */
        foreach ($articles as $article) {

            $this->logger->info("Importing news article {title}", array("title" => $article->getTitle()));

            // get post by meta:native_id
            $query = get_posts([
                'post_type' => 'zbr_news',
                'meta_query' => [
                    [
                        'key' => '_zbr_native_id',
                        'value' => $article->getId(),
                    ],
                ],
            ]);

            $post_id = 0;
            $post_modified = $article->getUpdatedAt()->format(self::DATE_FORMAT);
            if (!empty($query)) {

                $post_id = $query[0]->ID;
                $isNew = false;

                // log the post_modified date
                $this->logger->info('Post' . $query[0]->ID . ' modified date: ' . $query[0]->post_modified . ' article modified date: ' . $article->getUpdatedAt()->format(self::DATE_FORMAT));

                $isModified = $query[0]->post_modified != $article->getUpdatedAt()->format(self::DATE_FORMAT);
            }

            $attachments = $article->getAttachments();
            $mediaFiles = $article->getMedia();
            $owner = $article->getOwner();

            // if exists, update by id,
            // else insert
            $inserted = wp_insert_post(array(
                'ID' => $post_id,
                'post_type' => 'zbr_news',
                'post_title' => $article->getTitle(),
                'post_content' => $this->convert_content($article->getContent()),
                'post_date' => $article->getPublishedAt()->format(self::DATE_FORMAT),
                'post_status' => 'publish',
                'comment_status' => 'closed',
                'ping_status' => 'closed',
                'meta_input' => array(
                    "_zbr_native_id" => $article->getId(),
                    "_zbr_sub_title" => $article->getSubTitle(),
                    "_zbr_media" => $mediaFiles,
                    "_zbr_attachments" => $attachments,
                    "_zbr_language_code" => $article->getLanguageCode(),
                    "_zbr_location" => $article->getLocation(),
                    "_zbr_authors" => $article->getAuthors(),
                    "_zbr_zone_id" => $owner->getId()
                ),
            ));

            $this->changePostLastModifiedTime($post_id, $post_modified);

            if ($inserted) {
                $post_id = $inserted;
            }

            if ($isNew) {
                foreach ($attachments as $attachment) {
                    $this->logger->info('Got attachment' . $attachment);
                    $fileName = $attachment->getFile();

                    $attachmentData = array(
                        'guid' => '',
                        'post_mime_type' => $attachment->getMimeType(),
                        'post_title' => $attachment->getLabel(),
                        'post_content' => '',
                        'post_status' => 'inherit',
                        'meta_input' => [
                            '_zbr_is_attachment' => true,
                        ],
                    );

                    wp_insert_attachment($attachmentData, $fileName, $post_id);
                }
            } else {
                $this->logger->info("Skipping import of attachments for {PostId} as it is not new", array("PostId" => $post_id));
            }

            if ($isNew || $isModified) {
                $files = array_map(function ($file) {
                    return $file->getSource();
                }, $mediaFiles);
                $this->handle_media($files, $post_id);
            }

            $tags = array_map(function ($tag) {
                return $tag->getTag();
            }, $article->getTags());

            wp_set_post_terms($post_id, $tags, 'post_tag'); // replace the tags
        }

        $this->purge_old_news_articles($article_ids);
    }

    private function purge_old_news_articles(array $active_article_ids)
    {
        $this->logger->info('purging old news articles');
        $this->purge_old_posts($active_article_ids, 'zbr_news');
    }

    private function purge_old_zones(array $active_zone_ids)
    {
        $this->logger->info('purging old zones');
        $this->purge_old_posts($active_zone_ids, 'zbr_zone');
    }

    private function purge_old_agenda_items(array $active_item_ids)
    {
        $this->logger->info('purging old agenda items');
        $this->purge_old_posts($active_item_ids, 'zbr_agenda');
    }

    private function purge_old_team(array $active_item_ids)
    {
        $this->logger->info('purging old team members');
        $this->purge_old_posts($active_item_ids, 'zbr_zone_team_member');
    }

    private function purge_old_posts(array $nativeIds, string $postType)
    {

        $args = [
            'post_type' => $postType,
            'meta_query' => [
                [
                    'key' => '_zbr_native_id',
                    'value' => $nativeIds,
                    'compare' => 'NOT IN',
                    'type' => 'numeric',
                ],
            ],
            'posts_per_page' => -1
        ];

        $query = new WP_Query($args);
        while ($query->have_posts()) {
            $query->the_post();
            $id = get_the_ID();

            $this->logger->info('deleting post ' . $id);
            $result = wp_delete_post($id, true);
            if ($result == false || $result == null) {
                $this->logger->error("Failed to delete post" . $id);
            }
        }
        wp_reset_postdata();
    }

    /** @var AgendaItemDto[] $items */
    private function persist_agenda_items(array $items)
    {
        $item_ids = array_map(function ($item) {
            return $item->getId();
        }, $items);

        $isNew = true;
        foreach ($items as $item) {
            $nativeId = $item->getId();
            $this->logger->info("Importing agenda item {title}", array("title" => $item->getTitle()));

            /** @var WP_Post[] $query */
            $query = get_posts([
                'post_type' => 'zbr_agenda',
                'numberposts' => 1,
                'post_status' => ['publish', 'future'],
                'meta_query' => [
                    [
                        'key' => '_zbr_native_id',
                        'value' => $nativeId,
                    ],
                ],
            ]);

            $post_id = 0;
            if (!empty($query)) {
                $post_id = $query[0]->ID;
                $isNew = false;
            }
            $this->logger->info("agenda item with id " . $nativeId . " has post_id " . $post_id);

            $mediaFiles = [$item->getImage()];
            $owner = $item->getOwner();

            // if exists, update by id,
            // else insert
            $inserted = wp_insert_post(array(
                'ID' => $post_id,
                'post_type' => 'zbr_agenda',
                'post_title' => $item->getTitle(),
                'post_content' => $this->convert_content($item->getContent()),
                'post_date' => $item->getStartsAt()->format(self::DATE_FORMAT),
                'post_modified' => $item->getUpdatedAt()->format(self::DATE_FORMAT),
                'post_status' => 'publish',
                'comment_status' => 'closed',
                'ping_status' => 'closed',
                'meta_input' => array(
                    "_zbr_native_id" => $nativeId,
                    "_zbr_language_code" => $item->getLanguageCode(),
                    "_zbr_location" => $item->getLocation(),
                    "_zbr_agenda_start_date" => $item->getStartsAt()->format(self::DATE_FORMAT),
                    "_zbr_agenda_end_date" => $item->getEndsAt()->format(self::DATE_FORMAT),
                    "_zbr_agenda_is_all_day" => $item->getIsAllDayEvent(),
                    "_zbr_authors" => $item->getAuthors(),
                    "_zbr_zone_id" => $owner->getId()
                ),
            ));

            if ($inserted) {
                $post_id = $inserted;
            }

            if ($isNew) {
                $this->handle_media($mediaFiles, $post_id);
            } else {
                $this->logger->info("Skipping media import for {PostId} as it is not new", array("PostId" => $post_id));
            }

            $tags = array_map(function ($tag) {
                return $tag->getTag();
            }, $item->getTags());


            wp_set_post_terms($post_id, $tags);
        }

        $this->purge_old_agenda_items($item_ids);
    }

    /** @var string[] $mediaFiles */
    private function handle_media(array $mediaFiles, $post_id)
    {
        // remove all attachments
        $attachments = get_attached_media('', $post_id);
        foreach ($attachments as $attachment) {
            $this->logger->info('Removing attachment ' . $attachment->ID . ' from post ' . $post_id);
            wp_delete_attachment($attachment->ID, true);
        }


        # media_handle_sideload requires these files to be included if used outside of /wp-admin calls,
        # which we do whenever the API import is being called in a CRON job
        require_once ABSPATH . 'wp-admin/includes/media.php';
        require_once ABSPATH . 'wp-admin/includes/file.php';
        require_once ABSPATH . 'wp-admin/includes/image.php';

        $mimes = new \Mimey\MimeTypes;

        $first = true;
        $youtubeIds = [];

        foreach ($mediaFiles as $mediaFile) {

            // if the $mediaFile contains youtube.com, do not import it as media
            // but attach it as a meta field
            if (preg_match('/youtube.com\/watch\?v\=(.*)$/i', $mediaFile, $matches)) {
                $this->logger->info('Found a youtube media file');
                array_push($youtubeIds, $matches[1]);
                continue;
            }

            $this->logger->info('Found a media file ' . $mediaFile);
            $url = $mediaFile;
            $fileName = wp_basename($url);
            $extension = pathinfo($fileName, PATHINFO_EXTENSION);
            $mimeType = $mimes->getMimeType($extension);
            $tmp_name = download_url($url);

            if (is_wp_error($tmp_name)) {
                $this->logger->error($tmp_name->get_error_message());
                continue;
            }

            $array = array( //array to mimic $_FILES
                'name' => $fileName, //isolates and outputs the file name from its absolute path
                'type' => $mimeType, // get mime type of image file
                'tmp_name' => $tmp_name, //this field passes the actual path to the image
                'error' => 0, //normally, this is used to store an error, should the upload fail. but since this isn't actually an instance of $_FILES we can default it to zero here
                'size' => filesize($tmp_name), //returns image filesize in bytes
            );

            $attachment_id = media_handle_sideload($array, $post_id, null, [
                'meta_input' => [
                    '_zbr_is_attachment' => true,
                ],
            ]);

            // remove our temporary downloaded file
            if (file_exists($tmp_name)) {
                unlink($tmp_name);
            }

            // it could be that some attachments can't be sideloaded, this could be because of security reasons
            if (is_wp_error($attachment_id)) {
                $this->logger->error($attachment_id);
                continue;
            }

            if ($first) {
                set_post_thumbnail($post_id, $attachment_id);
                $first = false;
            }
        }

        add_post_meta($post_id, '_zbr_youtube_ids', $youtubeIds, true);
    }

    /** @var string $content */
    private function convert_content($content)
    {
        if (!isset($content))
            return $content;

        if (preg_match_all("/\[url\|([^\|]+)\|([^\|]+)\|([^\|]+)\]/i", $content, $matches)) {
            $replacements = [];
            for ($i = 0; $i < count($matches[1]); $i++) {

                //here you can implement an if/else to check if the ID exist
                $url = $matches[1][$i];
                $text = $matches[2][$i];
                $intExt = $matches[3][$i];

                $target = 'self';
                if ($intExt == 'ext')
                    $target = "_blank";

                $replacement = "<a href=\"" . $url . "\" target=\"" . $target . "\" rel=\"nofollow\">" . $text . "</a>";

                $replacements[$matches[0][$i]] = $replacement;
            }

            foreach ($replacements as $key => $value) {
                $content = str_replace($key, $value, $content);
            }
        }
        return $content;
    }

    function changePostLastModifiedTime($postID, $time)
    {
        global $wpdb;

        // Get the post object by ID
        $post = get_post($postID);

        // Return false if post not found
        if (!$post) {
            return false;
        }

        // Get the name of the posts table
        $table_name = $wpdb->prefix . 'posts';

        // Update the post_modified and post_modified_gmt fields in the database
        $result = $wpdb->update(
            $table_name,
            array(
                'post_modified' => $time,
                'post_modified_gmt' => $time
            ),
            array('ID' => $postID)
        );

        // Return false if the update query fails
        if ($result === false) {
            return false;
        }

        // Set the post_modified and post_modified_gmt values in the post object
        $post->post_modified = $time;
        $post->post_modified_gmt = $time;

        // Clear the post cache to ensure that the updated values are used
        clean_post_cache($postID);

        // Return true upon success
        return true;
    }
}
