<?php

namespace Silvanus\blocks\general\LatestPostsBlockGrid;

use Silvanus\includes\blocks\BlockModuleInterface;
use Timber\Timber;

final class Module implements BlockModuleInterface
{
    private const AJAX_ACTION = 'silvanus_latest_posts_block_grid';

    public function register(): void
    {
        // Populate taxonomy + terms choices dynamically for Latest Posts Block Grid
        add_filter('acf/load_field/key=field_taxonomy', [$this, 'acfPopulateLatestPostsTaxonomyChoices'], 20);
        add_filter('acf/load_field/key=field_terms', [$this, 'acfPopulateLatestPostsTermChoices'], 20);

        // Block context
        add_filter('timber/acf-gutenberg-blocks-data/latest-posts-block-grid', [$this, 'prepareLatestPostsBlockGridContext'], 20);

        // AJAX (logged-in + public)
        add_action('wp_ajax_' . self::AJAX_ACTION, [$this, 'ajax']);
        add_action('wp_ajax_nopriv_' . self::AJAX_ACTION, [$this, 'ajax']);
    }

    public function prepareLatestPostsBlockGridContext(array $context): array
    {
        $fields = $context['fields'] ?? [];
        if (!is_array($fields)) {
            return $context;
        }

        $fc = $fields['frontend_controls'] ?? [];
        if (!is_array($fc)) {
            $fc = [];
        }

        $context['ajax'] = [
            'action' => self::AJAX_ACTION,
            'nonce' => wp_create_nonce(self::AJAX_ACTION),
            'url' => admin_url('admin-ajax.php'),
        ];

        $allowedAuthors = $fc['allowed_authors'] ?? [];
        if (!is_array($allowedAuthors)) {
            $allowedAuthors = [];
        }

        $enableLoadMore = !empty($fc['enable_infinite_scroll__load_more']) || !empty($fc['enable_infinite_scroll_load_more']);

        $postsPerPage = isset($fc['posts_per_page']) ? (int) $fc['posts_per_page'] : 6;
        if ($postsPerPage < 1) {
            $postsPerPage = 6;
        }

        $context['latest_posts_base'] = [
            'use_manual_posts' => !empty($fields['use_manual_posts']),
            'manual_posts' => $fields['manual_posts'] ?? [],
            'query' => $fields['query'] ?? [],
            'max_posts_total' => isset($fields['query']['max_posts']) ? (int) $fields['query']['max_posts'] : 6,

            'enable_infinite_scroll_load_more' => $enableLoadMore ? 1 : 0,
            'posts_per_page' => $enableLoadMore ? $postsPerPage : (int) ($fields['query']['max_posts'] ?? 6),

            'allowed_author_ids' => array_values(array_filter(array_map('intval', $allowedAuthors))),
        ];

        $page = 1;

        // Ensure runQuery uses the same per-page value as base (so initial render matches load-more behaviour)
        $fieldsForQuery = $fields;
        $fieldsForQuery['posts_per_page'] = $context['latest_posts_base']['posts_per_page'];

        $result = $this->runQuery($fieldsForQuery, [], $page);

        $context['latest_posts'] = $result['posts'];
        $context['has_more'] = $result['has_more'];
        $context['next_page'] = $result['next_page'];

        $context['post_overrides'] = $this->buildOverridesMap($fields);
        $context['block_col_class'] = $fields['block_col_class'] ?? '';

        return $context;
    }

    public function ajax(): void
    {
        $nonce = isset($_POST['nonce']) ? (string) $_POST['nonce'] : '';
        if (!wp_verify_nonce($nonce, self::AJAX_ACTION)) {
            wp_send_json_error(['message' => 'Invalid nonce'], 403);
        }

        $page = isset($_POST['page']) ? (int) $_POST['page'] : 1;
        if ($page < 1) {
            $page = 1;
        }

        $loadedPages = isset($_POST['loaded_pages']) ? (int) $_POST['loaded_pages'] : 1;
        if ($loadedPages < 1) {
            $loadedPages = 1;
        }

        $base = $this->coercePostedJsonOrArray($_POST['base'] ?? []);
        $filters = $this->coercePostedJsonOrArray($_POST['filters'] ?? []);

        $fields = [
            'use_manual_posts' => !empty($base['use_manual_posts']),
            'manual_posts' => isset($base['manual_posts']) && is_array($base['manual_posts']) ? $base['manual_posts'] : [],
            'query' => isset($base['query']) && is_array($base['query']) ? $base['query'] : [],
            'posts_per_page' => isset($base['posts_per_page']) && is_numeric($base['posts_per_page'])
                ? max(1, (int) $base['posts_per_page'])
                : 6,
            'allowed_author_ids' => isset($base['allowed_author_ids']) && is_array($base['allowed_author_ids'])
                ? array_values(array_filter(array_map('intval', $base['allowed_author_ids'])))
                : [],
        ];

        $result = $this->runQuery($fields, $filters, $page, $loadedPages);

        $html = Timber::compile('blocks/general/latest_posts_block_grid/latest-posts-block-grid.items.twig', [
            'latest_posts' => $result['posts'],
            'post_overrides' => $this->buildOverridesMap($fields),
        ]);

        wp_send_json_success([
            'html' => $html,
            'hasMore' => $result['has_more'],
            'nextPage' => $result['next_page'],
        ]);
    }

    /**
     * @return array{posts: array, has_more: bool, next_page: int}
     */
    private function runQuery(array $fields, array $filters, int $page, int $loadedPages = 1): array
    {
        $query = $fields['query'] ?? [];
        if (!is_array($query)) {
            $query = [];
        }

        $useManual = !empty($fields['use_manual_posts']);
        $manualPosts = $fields['manual_posts'] ?? [];

        $maxTotal = isset($query['max_posts']) ? (int) $query['max_posts'] : 6;
        if ($maxTotal < 1) {
            $maxTotal = 6;
        }

        $perPageBase = !empty($fields['posts_per_page']) ? (int) $fields['posts_per_page'] : $maxTotal;
        if ($perPageBase < 1) {
            $perPageBase = 6;
        }

        // NEW: If refetching page 1 after filters change, return "loadedPages * perPageBase"
        if ($page === 1 && $loadedPages > 1) {
            $offset = 0;
            $perPage = min($maxTotal, $loadedPages * $perPageBase);
        } else {
            $perPage = $perPageBase;
            $offset = ($page - 1) * $perPageBase;

            if ($offset >= $maxTotal) {
                return ['posts' => [], 'has_more' => false, 'next_page' => $page];
            }

            $perPage = min($perPage, $maxTotal - $offset);
        }

        if ($useManual && is_array($manualPosts) && !empty($manualPosts)) {
            $ids = array_values(array_filter(array_map('intval', $manualPosts)));
            $slice = array_slice($ids, $offset, $perPage);

            $posts = Timber::get_posts([
                'post_type' => 'any',
                'post__in' => $slice,
                'orderby' => 'post__in',
                'posts_per_page' => count($slice),
                'post_status' => 'publish',
            ]);

            $hasMore = (count($slice) === $perPage) && (($offset + $perPage) < min($maxTotal, count($ids)));

            return [
                'posts' => $posts,
                'has_more' => $hasMore,
                'next_page' => $hasMore ? (($page === 1 && $loadedPages > 1) ? ($loadedPages + 1) : ($page + 1)) : $page,
            ];
        }

        // ... existing code that resolves $postType ...

        $postType = $query['post_type'] ?? 'post';
        if (is_array($postType)) {
            $postType = reset($postType) ?: 'post';
        }
        $postTypeSlug = $query['post_type_slug'] ?? '';
        if (is_array($postTypeSlug)) {
            $postTypeSlug = reset($postTypeSlug) ?: '';
        }
        if ($postType === 'other') {
            $postType = $postTypeSlug ?: 'post';
        }
        $postType = (is_string($postType) && $postType !== '') ? $postType : 'post';

        $args = [
            'post_type' => $postType,
            'posts_per_page' => $perPage,
            'offset' => $offset,
            'post_status' => 'publish',
            'orderby' => 'date',
            'order' => 'DESC',
            'ignore_sticky_posts' => true,
        ];

        /**
         * Taxonomy filtering (single, whitelisted)
         * - If the block has configured terms, constrain results to those terms.
         * - If a frontend term filter is provided AND is in the configured term list, narrow to that term only.
         */
        $taxonomy = $query['taxonomy'] ?? '';
        if (is_array($taxonomy)) {
            $taxonomy = reset($taxonomy) ?: '';
        }

        $terms = $query['terms'] ?? [];
        if (!is_array($terms)) {
            $terms = [];
        }
        $configuredTermIds = array_values(array_filter(array_map('intval', $terms)));

        $frontendTermRaw = $filters['term'] ?? null;
        $frontendTerm = is_numeric($frontendTermRaw) ? (int) $frontendTermRaw : 0;
        $frontendTermAllowed = $frontendTerm > 0 && in_array($frontendTerm, $configuredTermIds, true);

        if (
            is_string($taxonomy) && $taxonomy !== '' &&
            taxonomy_exists($taxonomy) &&
            is_object_in_taxonomy($postType, $taxonomy) &&
            !empty($configuredTermIds)
        ) {
            $args['tax_query'] = [
                [
                    'taxonomy' => $taxonomy,
                    'field' => 'term_id',
                    'terms' => $frontendTermAllowed ? [$frontendTerm] : $configuredTermIds,
                ],
            ];
        }

        /**
         * Author filtering (frontend pills; whitelisted)
         * "All" should remove any author constraint.
         */
        $allowedAuthorIds = $fields['allowed_author_ids'] ?? [];
        if (!is_array($allowedAuthorIds)) {
            $allowedAuthorIds = [];
        }
        $allowedAuthorIds = array_values(array_filter(array_map('intval', $allowedAuthorIds)));

        $frontendAuthorRaw = $filters['author'] ?? null;
        $frontendAuthor = is_numeric($frontendAuthorRaw) ? (int) $frontendAuthorRaw : 0;

        if ($frontendAuthor > 0) {
            if (empty($allowedAuthorIds) || in_array($frontendAuthor, $allowedAuthorIds, true)) {
                $args['author'] = $frontendAuthor;
            }
        } else {
            unset($args['author'], $args['author__in'], $args['author__not_in']);
        }

        // Dates from block settings still apply
        $dateFrom = $query['date_from'] ?? '';
        if (is_array($dateFrom)) {
            $dateFrom = reset($dateFrom) ?: '';
        }

        $dateTo = $query['date_to'] ?? '';
        if (is_array($dateTo)) {
            $dateTo = reset($dateTo) ?: '';
        }

        if ($dateFrom || $dateTo) {
            $args['date_query'] = [
                [
                    'after' => $dateFrom ?: null,
                    'before' => $dateTo ?: null,
                    'inclusive' => true,
                ],
            ];
        }

        $posts = Timber::get_posts($args);

        // Conservative hasMore: if we got fewer than requested, there aren't more
        $hasMore = (is_countable($posts) ? count($posts) : 0) === $perPage && (($offset + $perPage) < $maxTotal);

        return [
            'posts' => $posts,
            'has_more' => $hasMore,
            'next_page' => $hasMore ? (($page === 1 && $loadedPages > 1) ? ($loadedPages + 1) : ($page + 1)) : $page,
        ];
    }

    /**
     * Accepts:
     * - array (already parsed)
     * - JSON string
     * - JSON string with slashes (e.g. contains \" because of request escaping)
     *
     * @return array<string,mixed>
     */
    private function coercePostedJsonOrArray(mixed $value): array
    {
        if (is_array($value)) {
            return $value;
        }

        if (!is_string($value) || $value === '') {
            return [];
        }

        // WP may add slashes to POSTed JSON strings.
        $raw = wp_unslash($value);

        $decoded = json_decode($raw, true);
        if (is_array($decoded)) {
            return $decoded;
        }

        /**
         * Sometimes values end up double-encoded (e.g. "\"{...}\"").
         * If the decoded value is a JSON string, decode again.
         */
        $decoded2 = json_decode($raw, true);
        if (is_string($decoded2) && $decoded2 !== '') {
            $decoded3 = json_decode($decoded2, true);
            if (is_array($decoded3)) {
                return $decoded3;
            }
        }

        return [];
    }

    private function buildOverridesMap(array $fields): array
    {
        $overrideMap = [];
        $overrides = $fields['post_overrides'] ?? [];

        if (is_array($overrides)) {
            foreach ($overrides as $row) {
                $postId = isset($row['post']) ? (int) $row['post'] : 0;
                if ($postId <= 0) {
                    continue;
                }

                $bgId = isset($row['background_image']) ? (int) $row['background_image'] : 0;

                $descHtml = isset($row['description']) && is_string($row['description'])
                    ? trim($row['description'])
                    : '';

                $textVertAlign = isset($row['text_vertical_alignment']) && is_string($row['text_vertical_alignment'])
                    ? trim($row['text_vertical_alignment'])
                    : '';

                $hideReadMore = $row['hide_read_more_button'] == true;

                $overrideMap[$postId] = [
                    'background_url' => $bgId > 0 ? wp_get_attachment_url($bgId) : '',
                    'description_html' => $descHtml,
                    'hide_read_more_button' => $hideReadMore,
                    'text_vertical_alignment' => $textVertAlign,
                ];
            }
        }

        return $overrideMap;
    }

    public function acfPopulateLatestPostsTaxonomyChoices(array $field): array
    {
        $field['choices'] = [];

        $acfPosted = $_POST['acf'] ?? [];
        $postType = $acfPosted['field_post_type'] ?? 'post';
        $postTypeSlug = $acfPosted['field_post_type_slug'] ?? '';

        if ($postType === 'other' && is_string($postTypeSlug) && $postTypeSlug !== '') {
            $postType = $postTypeSlug;
        }

        if (!is_string($postType) || $postType === '') {
            $postType = 'post';
        }

        $taxObjects = get_object_taxonomies($postType, 'objects');

        foreach ($taxObjects as $tax) {
            if (!empty($tax->public)) {
                $field['choices'][$tax->name] = $tax->labels->singular_name ?? $tax->name;
            }
        }

        return $field;
    }

    public function acfPopulateLatestPostsTermChoices(array $field): array
    {
        $field['choices'] = [];

        $acfPosted = $_POST['acf'] ?? [];
        $taxonomy = $acfPosted['field_taxonomy'] ?? '';

        if (!is_string($taxonomy) || $taxonomy === '' || !taxonomy_exists($taxonomy)) {
            return $field;
        }

        $terms = get_terms([
            'taxonomy' => $taxonomy,
            'hide_empty' => false,
        ]);

        if (is_wp_error($terms)) {
            return $field;
        }

        foreach ($terms as $term) {
            $field['choices'][(string) $term->term_id] = $term->name;
        }

        return $field;
    }
}
