<?php

namespace GeminiLabs\SiteReviews\Database;

use GeminiLabs\SiteReviews\Commands\CreateReview;
use GeminiLabs\SiteReviews\Database;
use GeminiLabs\SiteReviews\Database\PostMeta;
use GeminiLabs\SiteReviews\Defaults\CustomFieldsDefaults;
use GeminiLabs\SiteReviews\Defaults\RatingDefaults;
use GeminiLabs\SiteReviews\Defaults\UpdateReviewDefaults;
use GeminiLabs\SiteReviews\Helper;
use GeminiLabs\SiteReviews\Helpers\Arr;
use GeminiLabs\SiteReviews\Helpers\Cast;
use GeminiLabs\SiteReviews\Modules\Queue;
use GeminiLabs\SiteReviews\Modules\Sanitizer;
use GeminiLabs\SiteReviews\Request;
use GeminiLabs\SiteReviews\Review;
use GeminiLabs\SiteReviews\Reviews;

class ReviewManager
{
    /**
     * Foreign indexes ensure that $postId is a valid Post ID.
     */
    public function assignPost(Review $review, int $postId): bool
    {
        if (!glsr()->can('assign_post', $postId)) {
            return false;
        }
        $data = [
            'is_published' => $this->isPublishedPost($postId),
            'post_id' => $postId,
            'rating_id' => $review->rating_id,
        ];
        if ($result = glsr(Database::class)->insert('assigned_posts', $data)) {
            glsr(Cache::class)->delete($review->ID, 'reviews');
            if (!defined('WP_IMPORTING')) {
                glsr(CountManager::class)->posts($postId);
            }
        }
        return Cast::toInt($result) > 0;
    }

    /**
     * Foreign indexes ensure that $termId is a valid Term ID.
     */
    public function assignTerm(Review $review, int $termId): bool
    {
        $data = [
            'rating_id' => $review->rating_id,
            'term_id' => $termId,
        ];
        if ($result = glsr(Database::class)->insert('assigned_terms', $data)) {
            glsr(Cache::class)->delete($review->ID, 'reviews');
            if (!defined('WP_IMPORTING')) {
                glsr(CountManager::class)->terms($termId);
            }
        }
        return Cast::toInt($result) > 0;
    }

    /**
     * Foreign indexes ensure that $userId is a valid User ID.
     */
    public function assignUser(Review $review, int $userId): bool
    {
        $data = [
            'rating_id' => $review->rating_id,
            'user_id' => $userId,
        ];
        if ($result = glsr(Database::class)->insert('assigned_users', $data)) {
            glsr(Cache::class)->delete($review->ID, 'reviews');
            if (!defined('WP_IMPORTING')) {
                glsr(CountManager::class)->users($userId);
            }
        }
        return Cast::toInt($result) > 0;
    }

    /**
     * @return Review|false
     */
    public function create(CreateReview $command, ?int $postId = null)
    {
        if (empty($postId)) {
            $postId = $this->createRaw($command);
        }
        if (empty($postId)) {
            return false;
        }
        $review = $this->get($postId);
        if ($review->isValid()) {
            glsr()->action('review/created', $review, $command);
            return $review->refresh();
        }
        return false;
    }

    /**
     * @return Review|false
     */
    public function createFromPost(int $postId, array $data = [])
    {
        if (!Review::isReview($postId)) {
            return false;
        }
        $command = new CreateReview(new Request($data));
        glsr()->action('review/create', $postId, $command);
        return $this->create($command, $postId);
    }

    /**
     * @return int|false
     */
    public function createRaw(CreateReview $command)
    {
        $args = glsr()->args($command->toArray()); // this filters the values
        $values = [
            'comment_status' => 'closed',
            'meta_input' => $command->meta(),
            'ping_status' => 'closed',
            'post_author' => $args->author_id,
            'post_content' => $args->content,
            'post_date' => $args->date,
            'post_date_gmt' => $args->date_gmt,
            'post_modified' => $args->date,
            'post_modified_gmt' => $args->date_gmt,
            'post_name' => uniqid($args->type),
            'post_status' => $this->postStatus($command),
            'post_title' => $args->title,
            'post_type' => glsr()->post_type,
        ];
        $values = glsr()->filterArray('review/create/post_data', $values, $command);
        $postId = wp_insert_post($values, true);
        if (is_wp_error($postId)) {
            glsr_log()->error($postId->get_error_message())->debug($values);
            return false;
        }
        glsr()->action('review/create', $postId, $command);
        return $postId;
    }

    public function deleteRating(int $reviewId): bool
    {
        $result = glsr(Database::class)->delete('ratings', ['review_id' => $reviewId]);
        if ($result) {
            glsr(Cache::class)->delete($reviewId, 'reviews');
        }
        return Cast::toInt($result) > 0;
    }

    public function deleteRevisions(int $reviewId): void
    {
        $revisionIds = glsr(Query::class)->revisionIds((int) $reviewId);
        foreach ($revisionIds as $revisionId) {
            wp_delete_post_revision($revisionId);
        }
    }

    /**
     * @return Review|false
     */
    public function duplicate(int $reviewId)
    {
        if (!Review::isReview($reviewId)) {
            return false;
        }
        $review = glsr_get_review($reviewId);
        if (!$review->isValid()) {
            return false;
        }
        $data = $review->toArray();
        $data['author_id'] = get_current_user_id();
        $data['is_approved'] = $data['is_approved'] && glsr()->can('publish_posts');
        if (!$duplicate = glsr_create_review($data)) {
            return false;
        }
        foreach ($review->meta() as $key => $value) {
            if (!str_starts_with($key, '_submitted')) {
                update_post_meta($duplicate->ID, $key, $value);
            }
        }
        glsr(PostMeta::class)->set($duplicate->ID, 'duplicated_from', $review->ID);
        return $duplicate;
    }

    public function get(int $reviewId, bool $bypassCache = false): Review
    {
        return glsr(Query::class)->review($reviewId, $bypassCache);
    }

    public function reviews(array $args = []): Reviews
    {
        $args = (new NormalizePaginationArgs($args))->toArray();
        $results = glsr(Query::class)->reviews($args);
        $total = $this->total($args, $results);
        $reviews = new Reviews($results, $total, $args);
        glsr()->action('get/reviews', $reviews, $args);
        return $reviews;
    }

    public function total(array $args = [], array $reviews = []): int
    {
        return glsr(Query::class)->totalReviews($args, $reviews);
    }

    public function unassignPost(Review $review, int $postId): bool
    {
        if (!glsr()->can('unassign_post', $postId)) {
            return false;
        }
        $where = [
            'post_id' => $postId,
            'rating_id' => $review->rating_id,
        ];
        if ($result = glsr(Database::class)->delete('assigned_posts', $where)) {
            glsr(Cache::class)->delete($review->ID, 'reviews');
            glsr(CountManager::class)->posts($postId);
        }
        return Cast::toInt($result) > 0;
    }

    public function unassignTerm(Review $review, int $termId): bool
    {
        $where = [
            'rating_id' => $review->rating_id,
            'term_id' => $termId,
        ];
        if ($result = glsr(Database::class)->delete('assigned_terms', $where)) {
            glsr(Cache::class)->delete($review->ID, 'reviews');
            glsr(CountManager::class)->terms($termId);
        }
        return Cast::toInt($result) > 0;
    }

    public function unassignUser(Review $review, int $userId): bool
    {
        $where = [
            'rating_id' => $review->rating_id,
            'user_id' => $userId,
        ];
        if ($result = glsr(Database::class)->delete('assigned_users', $where)) {
            glsr(Cache::class)->delete($review->ID, 'reviews');
            glsr(CountManager::class)->users($userId);
        }
        return Cast::toInt($result) > 0;
    }

    /**
     * @return Review|false
     */
    public function update(int $reviewId, array $data = [])
    {
        $oldPost = get_post($reviewId);
        if (-1 === $this->updateRating($reviewId, $data)) {
            glsr_log('update rating failed');
            return false;
        }
        if (-1 === $this->updateReview($reviewId, $data)) {
            glsr_log('update review failed');
            return false;
        }
        $this->updateCustom($reviewId, $data);
        $this->updateResponse($reviewId, $data);
        $review = $this->get($reviewId);
        if (isset($data['assigned_posts'])) {
            $assignedPosts = glsr(Sanitizer::class)->sanitizePostIds($data['assigned_posts']);
            glsr()->action('review/updated/post_ids', $review, $assignedPosts); // triggers a recount of assigned posts
        }
        if (isset($data['assigned_terms'])) {
            $assignedTerms = glsr(Sanitizer::class)->sanitizeTermIds($data['assigned_terms']);
            wp_set_object_terms($reviewId, $assignedTerms, glsr()->taxonomy); // triggers a recount of assigned_terms
        }
        if (isset($data['assigned_users'])) {
            $assignedUsers = glsr(Sanitizer::class)->sanitizeUserIds($data['assigned_users']);
            glsr()->action('review/updated/user_ids', $review, $assignedUsers); // triggers a recount of assigned posts
        }
        $review->refresh();
        glsr()->action('review/updated', $review, $data, $oldPost);
        return $review;
    }

    public function updateAssignedPost(int $postId): bool
    {
        $result = glsr(Database::class)->update('assigned_posts',
            ['is_published' => $this->isPublishedPost($postId)],
            ['post_id' => Cast::toInt($postId)]
        );
        return Cast::toInt($result) > 0;
    }

    public function updateCustom(int $reviewId, array $values = []): void
    {
        $data = glsr(CustomFieldsDefaults::class)->merge($values);
        $data = Arr::prefixKeys($data, 'custom_');
        foreach ($data as $metaKey => $metaValue) {
            glsr(PostMeta::class)->set($reviewId, $metaKey, $metaValue);
        }
    }

    public function updateGeolocation(int $reviewId, array $values = []): void
    {
        if (!array_key_exists('ip_address', $values)) {
            return;
        }
        $review = $this->get($reviewId);
        if ($review->ip_address === $values['ip_address']) {
            return;
        }
        glsr(Database::class)->delete('stats', [
            'rating_id' => $review->rating_id,
        ]);
        glsr(PostMeta::class)->delete($review->ID, 'geolocation');
        glsr()->action('cache/flush', "review_{$review->ID}_geolocated", $review);
        if (!glsr_get_option('reviews.geolocation', false, 'bool')) {
            return;
        }
        if (empty($values['ip_address']) || Helper::isLocalIpAddress($values['ip_address'])) {
            return;
        }
        glsr(Queue::class)->once(time(), 'queue/geolocation', ['review_id' => $review->ID], true);
    }

    public function updateRating(int $reviewId, array $values = []): int
    {
        $review = $this->get($reviewId);
        glsr(Cache::class)->delete($reviewId, 'reviews');
        $sanitized = glsr(RatingDefaults::class)->restrict($values);
        $data = array_intersect_key($sanitized, $values);
        if (empty($data)) {
            return 0;
        }
        $result = glsr(Database::class)->update('ratings', $data, [
            'review_id' => $reviewId,
        ]);
        if (false === $result) {
            return -1;
        }
        $rating = $data['rating'] ?? '';
        if (is_numeric($rating) && $review->rating !== $data['rating']) {
            glsr(CountManager::class)->recalculate();
        }
        return Cast::toInt($result);
    }

    public function updateResponse(int $reviewId, array $values): int
    {
        if (!array_key_exists('response', $values)) {
            return 0;
        }
        $response = Arr::getAs('string', $values, 'response');
        $response = glsr(Sanitizer::class)->sanitizeTextHtml($response);
        $review = glsr_get_review($reviewId);
        if (empty($response) && empty($review->response)) {
            return 0;
        }
        glsr(PostMeta::class)->set($review->ID, 'response', $response); // prefixed metakey
        // This should run immediately after saving the response
        // but before adding the "response_by" meta_value!
        glsr()->action('review/responded', $review, $response);
        glsr(PostMeta::class)->set($review->ID, 'response_by', get_current_user_id()); // prefixed metakey
        glsr(Cache::class)->delete($review->ID, 'reviews');
        return 1;
    }

    public function updateReview(int $reviewId, array $values = []): int
    {
        if (glsr()->post_type !== get_post_type($reviewId)) {
            glsr_log()->error("Review update failed: Post ID [{$reviewId}] is not a review.");
            return -1;
        }
        glsr(Cache::class)->delete($reviewId, 'reviews');
        $sanitized = glsr(UpdateReviewDefaults::class)->restrict($values);
        if ($data = array_intersect_key($sanitized, $values)) {
            $data = array_filter([
                'post_content' => Arr::get($data, 'content'),
                'post_date' => Arr::get($data, 'date'),
                'post_date_gmt' => Arr::get($data, 'date_gmt'),
                'post_status' => Arr::get($data, 'status'),
                'post_title' => Arr::get($data, 'title'),
            ]);
        }
        if (empty($data)) {
            return 0;
        }
        $data = wp_parse_args(['ID' => $reviewId], $data);
        $result = wp_update_post($data, true);
        if (is_wp_error($result)) {
            glsr_log()->error($result->get_error_message())->debug($data);
            return -1;
        }
        return Cast::toInt($result);
    }

    protected function isPublishedPost(int $postId): bool
    {
        $isPublished = 'publish' === get_post_status($postId);
        return glsr()->filterBool('post/is-published', $isPublished, $postId);
    }

    protected function postStatus(CreateReview $command): string
    {
        $isApproved = $command->is_approved;
        $isFormSubmission = !defined('WP_IMPORTING') && !glsr()->retrieve('glsr_create_review', false);
        if ($isFormSubmission) {
            $requireApproval = glsr(OptionManager::class)->getBool('settings.general.require.approval');
            $requireApprovalForRating = glsr(OptionManager::class)->getInt('settings.general.require.approval_for', 5);
            $isApproved = !$requireApproval || $command->rating > $requireApprovalForRating;
        }
        return !$isApproved || $command->isBlacklisted()
            ? 'pending'
            : 'publish';
    }
}
