<?php
declare(strict_types=1);

namespace Ekesto\CacheInvalidator;

use Ekesto\CacheInvalidator\Support\Debug;

/**
 * Resolves post IDs from layout element → page template declarations.
 *
 * Queries _wp_page_template postmeta to find published posts assigned to
 * declared templates when a layout element (e.g. a sidebar) changes.
 */
final class LayoutTargetResolver
{
    /**
     * Resolve post IDs for changed layout elements via template mapping.
     *
     * @param string[]             $changedLayoutElements Changed layout element identifiers.
     * @param array<string,string> $context               Trigger context payload.
     * @return array{post_ids:int[],templates_resolved:string[],matched:bool}
     */
    public static function resolve(array $changedLayoutElements, array $context): array
    {
        $emptyResult = [
            'post_ids' => [],
            'templates_resolved' => [],
            'matched' => false,
        ];

        if (empty($changedLayoutElements)) {
            Debug::log('layout_target_resolver: no changed layout elements provided');
            return $emptyResult;
        }

        /**
         * Declare layout element → page template associations for targeted invalidation.
         *
         * Expected shape:
         * - element_id => string[] template slugs (as stored in _wp_page_template postmeta)
         * - '*' => string[] template slugs applied to any changed element (optional)
         *
         * @param array<string,string[]> $mappings              Layout element to template slug mappings.
         * @param string[]               $changedLayoutElements Changed layout element identifiers.
         * @param array<string,string>   $context               Trigger context (trigger_class, trigger_event).
         */
        $raw = apply_filters(
            'ekesto_ci_widget_template_mappings',
            [],
            $changedLayoutElements,
            $context
        );

        $mappings = self::normalizeFilter($raw);

        if (empty($mappings)) {
            Debug::log('layout_target_resolver: no template mappings declared');
            return $emptyResult;
        }

        $templates = self::collectTemplatesForElements($mappings, $changedLayoutElements);

        if (empty($templates)) {
            Debug::log(sprintf(
                'layout_target_resolver: mappings declared (entries=%d) but no templates matched changed elements',
                count($mappings)
            ));
            return $emptyResult;
        }

        Debug::log(sprintf(
            'layout_target_resolver: resolving templates=%d for changed_elements=%d',
            count($templates),
            count($changedLayoutElements)
        ));

        $postIds = self::queryPostIdsByTemplates($templates);

        $matched = !empty($postIds);

        Debug::log(sprintf(
            'layout_target_resolver: templates_resolved=%d posts_resolved=%d matched=%s',
            count($templates),
            count($postIds),
            $matched ? 'yes' : 'no'
        ));

        return [
            'post_ids' => $postIds,
            'templates_resolved' => $templates,
            'matched' => $matched,
        ];
    }

    /**
     * Normalize the filter return value.
     *
     * @param mixed $raw Raw filter return.
     * @return array<string,string[]> Validated mappings.
     */
    private static function normalizeFilter($raw): array
    {
        if (!is_array($raw)) {
            return [];
        }

        $normalized = [];
        foreach ($raw as $key => $templates) {
            $key = (string) $key;
            if ($key === '' || !is_array($templates)) {
                continue;
            }

            $validTemplates = [];
            foreach ($templates as $template) {
                $template = trim((string) $template);
                if ($template !== '') {
                    $validTemplates[] = $template;
                }
            }

            if (!empty($validTemplates)) {
                $normalized[$key] = $validTemplates;
            }
        }

        return $normalized;
    }

    /**
     * Collect template slugs for the changed elements, including '*' global.
     *
     * @param array<string,string[]> $mappings        Normalized mappings.
     * @param string[]               $changedElements Changed element identifiers.
     * @return string[] Unique template slugs to resolve.
     */
    private static function collectTemplatesForElements(array $mappings, array $changedElements): array
    {
        $templates = [];

        // Global '*' key applies to any changed element.
        if (isset($mappings['*'])) {
            $templates = array_merge($templates, $mappings['*']);
        }

        foreach ($changedElements as $elementId) {
            $elementId = (string) $elementId;
            if ($elementId !== '' && isset($mappings[$elementId])) {
                $templates = array_merge($templates, $mappings[$elementId]);
            }
        }

        return array_values(array_unique(array_filter($templates)));
    }

    /**
     * Query published post IDs assigned to the given page templates.
     *
     * @param string[] $templates Template slugs to match against _wp_page_template.
     * @return int[] Resolved post IDs.
     */
    private static function queryPostIdsByTemplates(array $templates): array
    {
        global $wpdb;

        if (empty($templates)) {
            return [];
        }

        $placeholders = implode(',', array_fill(0, count($templates), '%s'));

        // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- Placeholder list is count-derived from a fixed %s template and values are bound by prepare(); this resolver must read live template assignments.
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Template assignment resolver must read live postmeta.
        $results = $wpdb->get_col(
            $wpdb->prepare(
                "SELECT DISTINCT p.ID
                 FROM {$wpdb->posts} p
                 INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id
                 WHERE p.post_status = 'publish'
                   AND pm.meta_key = '_wp_page_template'
                   AND pm.meta_value IN ({$placeholders})",
                ...$templates
            )
        );
        // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare

        if (!is_array($results)) {
            return [];
        }

        return array_values(array_unique(array_filter(array_map('absint', $results), static fn(int $id): bool => $id > 0)));
    }
}
