<?php
declare(strict_types=1);

namespace Ekesto\CacheInvalidator\Integrations\Forms;

use Ekesto\CacheInvalidator\BlockReferenceScanner;
use Ekesto\CacheInvalidator\Integrations\Builders\BuilderDetector;
use Ekesto\CacheInvalidator\Core\Config;
use Ekesto\CacheInvalidator\Support\Debug;

/**
 * Resolves posts that embed a changed form.
 */
final class FormsUsageResolver
{
    private BuilderDetector $builderDetector;

    /**
     * Build resolver with dependencies.
     *
     * @param BuilderDetector|null $builderDetector Builder detector dependency.
     */
    public function __construct(?BuilderDetector $builderDetector = null)
    {
        $this->builderDetector = $builderDetector ?? new BuilderDetector();
    }

    /**
     * Resolve posts referencing a form ID for one adapter.
     *
     * @param FormAdapterInterface $adapter Adapter instance.
     * @param string|int           $formId Form ID value.
     * @param array<string,mixed>  $context Event context.
     * @return FormsResolutionResult
     */
    public function resolveReferencedPostIds(FormAdapterInterface $adapter, $formId, array $context = []): FormsResolutionResult
    {
        Debug::verbose(sprintf(
            'forms resolver: start adapter=%s form_id=%s',
            $adapter->key(),
            (string) $formId
        ));

        $targetIdentifiers = $this->resolveTargetIdentifiers($adapter, $formId, $context);
        if (empty($targetIdentifiers)) {
            Debug::verbose(sprintf(
                'forms resolver: skipped adapter=%s reason=empty_form_id',
                $adapter->key()
            ));
            return new FormsResolutionResult([], true, true, false, 0, 0, [], []);
        }
        $targetFormId = $targetIdentifiers[0];

        $scanPostTypes = $this->getScanPostTypes();
        if (empty($scanPostTypes)) {
            Debug::verbose(sprintf(
                'forms resolver: skipped adapter=%s reason=no_scan_post_types',
                $adapter->key()
            ));
            return new FormsResolutionResult([], true, true, false, 0, 0, [], $targetIdentifiers);
        }

        $candidateSummary = $this->collectCandidatePostIds(
            $adapter,
            $scanPostTypes,
            $this->resolveBuilderBridgeEntity($targetIdentifiers)
        );
        $candidateIds = $candidateSummary['post_ids'];
        $scanGap = (bool) ($candidateSummary['scan_gap'] ?? false);

        if (empty($candidateIds)) {
            Debug::verbose(sprintf(
                'forms resolver: no candidates adapter=%s form_id=%s scan_gap=%s candidates_total=%d',
                $adapter->key(),
                $targetFormId,
                $scanGap ? 'yes' : 'no',
                (int) ($candidateSummary['candidates_total'] ?? 0)
            ));
            return new FormsResolutionResult([], $scanGap, $scanGap, false, 0, 0, [], $targetIdentifiers);
        }

        $matchedPostIds = [];
        $uncertainDueToMetaTooLarge = false;
        $fieldsScanned = [];
        $candidatesScanned = 0;
        $builderKeys = [];
        $builderConfidence = [];

        foreach ($candidateIds as $postId) {
            $post = get_post($postId);
            if (!$post instanceof \WP_Post) {
                continue;
            }

            $candidatesScanned++;

            $profile = $this->builderDetector->detectForPost($post);
            $builderKeys[] = $profile->builderKey();
            $builderConfidence[] = $profile->confidence();
            $scanSummary = BlockReferenceScanner::collectCandidateScanTexts(
                $post,
                $profile,
                Config::FORMS_MAX_META_FIELD_BYTES
            );
            $fields = is_array($scanSummary['fields_scanned'] ?? null) ? $scanSummary['fields_scanned'] : [];
            $fieldsScanned = array_merge($fieldsScanned, $fields);
            $uncertainDueToMetaTooLarge = $uncertainDueToMetaTooLarge || (bool) ($scanSummary['meta_too_large'] ?? false);
            $scanTexts = is_array($scanSummary['scan_texts'] ?? null) ? $scanSummary['scan_texts'] : [];

            $matched = false;
            foreach ($scanTexts as $scanText) {
                $field = (string) ($scanText['field'] ?? '');
                $text = (string) ($scanText['text'] ?? '');
                $matchedToken = FormsReferenceMatcher::findMatchingFormId($adapter, $text, $targetIdentifiers);
                if ($matchedToken !== null) {
                    Debug::verbose(sprintf(
                        'forms resolver match: adapter=%s form_id=%s post_id=%d field=%s match_token=%s',
                        $adapter->key(),
                        $targetFormId,
                        (int) $post->ID,
                        $field,
                        $matchedToken
                    ));
                    $matchedPostIds[] = (int) $post->ID;
                    $matched = true;
                    break;
                }
            }

            if ($matched) {
                continue;
            }
        }

        $fieldsScanned = array_values(array_unique(array_filter(array_map('strval', $fieldsScanned))));

        $uncertain = $scanGap || $uncertainDueToMetaTooLarge;
        $builderCounts = array_count_values(array_filter(array_map('strval', $builderKeys)));
        $confidenceCounts = array_count_values(array_filter(array_map('strval', $builderConfidence)));

        Debug::verbose(sprintf(
            'forms resolver: done adapter=%s form_id=%s matches=%d uncertain=%s scan_gap=%s meta_too_large=%s targets=%d scanned=%d/%d',
            $adapter->key(),
            $targetFormId,
            count($matchedPostIds),
            $uncertain ? 'yes' : 'no',
            $scanGap ? 'yes' : 'no',
            $uncertainDueToMetaTooLarge ? 'yes' : 'no',
            count($targetIdentifiers),
            $candidatesScanned,
            (int) ($candidateSummary['candidates_total'] ?? 0)
        ), [
            'builders' => $builderCounts,
            'confidence' => $confidenceCounts,
            'target_identifiers' => $targetIdentifiers,
        ]);

        return new FormsResolutionResult(
            $matchedPostIds,
            $uncertain,
            $scanGap,
            $uncertainDueToMetaTooLarge,
            (int) ($candidateSummary['candidates_total'] ?? 0),
            $candidatesScanned,
            $fieldsScanned,
            $targetIdentifiers
        );
    }

    /**
     * Resolve normalized target identifiers for one changed form.
     *
     * @param FormAdapterInterface $adapter Adapter instance.
     * @param string|int           $formId Changed form identifier.
     * @param array<string,mixed>  $context Event context.
     * @return string[]
     */
    private function resolveTargetIdentifiers(FormAdapterInterface $adapter, $formId, array $context): array
    {
        $base = FormsReferenceMatcher::normalizeFormId((string) $formId);
        if ($base === '') {
            return [];
        }

        $targets = [$base];

        if ($adapter instanceof FormIdentifierAliasProviderInterface) {
            $aliases = $adapter->expandTargetIdentifiers($base, $context);
            if (is_array($aliases)) {
                $targets = array_merge($targets, $aliases);
            }
        }

        return array_values(array_unique(array_filter(array_map(
            static fn($id): string => FormsReferenceMatcher::normalizeFormId((string) $id),
            $targets
        ))));
    }

    /**
     * Return post types scanned for form references.
     *
     * @return string[]
     */
    private function getScanPostTypes(): array
    {
        $publicPostTypes = get_post_types(['public' => true], 'names');
        if (!is_array($publicPostTypes)) {
            return [];
        }

        $default = array_values(array_unique(array_filter(array_map(
            static fn($postType): string => sanitize_key((string) $postType),
            $publicPostTypes
        ))));

        /**
         * Filter post types scanned for forms references.
         *
         * @param string[] $default Default public post types.
         */
        $scanPostTypes = (array) apply_filters('ekesto_ci_forms_scan_post_types', $default);

        return array_values(array_unique(array_filter(array_map(
            static fn($postType): string => sanitize_key((string) $postType),
            $scanPostTypes
        ))));
    }

    /**
     * Collect candidate IDs via base content prefiltering plus builder bridge.
     *
     * @param FormAdapterInterface $adapter Adapter instance.
     * @param string[]             $scanPostTypes Post types to scan.
     * @param \WP_Post|null        $builderBridgeEntity Changed form entity for builder hints.
     * @return array{post_ids:int[],scan_gap:bool,candidates_total:int}
     */
    private function collectCandidatePostIds(
        FormAdapterInterface $adapter,
        array $scanPostTypes,
        ?\WP_Post $builderBridgeEntity
    ): array
    {
        $needles = array_values(array_unique(array_filter(array_map(
            static fn($needle): string => trim((string) $needle),
            $adapter->candidateNeedles()
        ))));

        if (empty($needles) || empty($scanPostTypes)) {
            Debug::verbose(sprintf(
                'forms resolver candidates skipped: adapter=%s reason=%s',
                $adapter->key(),
                empty($needles) ? 'empty_needles' : 'empty_scan_post_types'
            ));
            return [
                'post_ids' => [],
                'scan_gap' => false,
                'candidates_total' => 0,
            ];
        }

        $baseCandidates = $this->collectBaseCandidatePostIds($scanPostTypes, $needles);
        $builderCandidates = $this->collectBuilderCandidatePostIds($builderBridgeEntity, $scanPostTypes);

        $merged = array_values(array_unique(array_filter(array_map(
            'absint',
            array_merge(
                $baseCandidates['post_ids'] ?? [],
                $builderCandidates['post_ids'] ?? []
            )
        ))));

        $candidatesTotal = count($merged);
        $scanGap = (bool) ($baseCandidates['scan_gap'] ?? false)
            || (bool) ($builderCandidates['scan_gap'] ?? false);

        if ($candidatesTotal > Config::FORMS_MAX_CANDIDATES) {
            $merged = array_slice($merged, 0, Config::FORMS_MAX_CANDIDATES);
            $scanGap = true;
        }

        Debug::verbose(sprintf(
            'forms resolver candidates: adapter=%s total=%d selected=%d capped=%s scan_gap=%s',
            $adapter->key(),
            $candidatesTotal,
            count($merged),
            $candidatesTotal > Config::FORMS_MAX_CANDIDATES ? 'yes' : 'no',
            $scanGap ? 'yes' : 'no'
        ));

        return [
            'post_ids' => $merged,
            'scan_gap' => $scanGap,
            'candidates_total' => $candidatesTotal,
        ];
    }

    /**
     * Resolve a changed form entity that can drive builder bridge hints.
     *
     * @param string[] $targetIdentifiers Resolved target identifiers.
     * @return \WP_Post|null
     */
    private function resolveBuilderBridgeEntity(array $targetIdentifiers): ?\WP_Post
    {
        foreach ($targetIdentifiers as $identifier) {
            $identifier = trim((string) $identifier);
            if ($identifier === '' || !ctype_digit($identifier)) {
                continue;
            }

            $post = get_post(absint($identifier));
            if ($post instanceof \WP_Post) {
                return $post;
            }
        }

        return null;
    }

    /**
     * Collect base candidates from post_content LIKE prefiltering only.
     *
     * @param string[] $scanPostTypes Post types to scan.
     * @param string[] $needles Candidate needles.
     * @return array{post_ids:int[],scan_gap:bool}
     */
    private function collectBaseCandidatePostIds(array $scanPostTypes, array $needles): array
    {
        global $wpdb;

        // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Placeholder lists are count-derived from fixed %s templates and all values are bound through $wpdb->prepare(); form usage scans must use live post content.
        $postTypePlaceholders = implode(',', array_fill(0, count($scanPostTypes), '%s'));

        $contentLikeSql = [];
        $contentParams = [];
        foreach ($needles as $needle) {
            $contentLikeSql[] = 'p.post_content LIKE %s';
            $contentParams[] = '%' . $wpdb->esc_like($needle) . '%';
        }

        $contentSql = sprintf('(%s)', implode(' OR ', $contentLikeSql));

        $contentMatches = $wpdb->get_col(
            $wpdb->prepare(
                "SELECT p.ID
                 FROM {$wpdb->posts} p
                 WHERE p.post_type IN ($postTypePlaceholders)
                   AND p.post_status = 'publish'
                   AND $contentSql",
                ...array_merge($scanPostTypes, $contentParams)
            )
        );
        // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare,PluginCheck.Security.DirectDB.UnescapedDBParameter

        if (!is_array($contentMatches)) {
            return [
                'post_ids' => [],
                'scan_gap' => true,
            ];
        }

        return [
            'post_ids' => array_values(array_unique(array_filter(array_map('absint', $contentMatches)))),
            'scan_gap' => false,
        ];
    }

    /**
     * Collect builder candidates through the shared builder bridge.
     *
     * @param \WP_Post|null $builderBridgeEntity Changed form entity for builder hints.
     * @param string[]      $scanPostTypes Post types to scan.
     * @return array{post_ids:int[],scan_gap:bool}
     */
    private function collectBuilderCandidatePostIds(?\WP_Post $builderBridgeEntity, array $scanPostTypes): array
    {
        if (!$builderBridgeEntity instanceof \WP_Post) {
            return [
                'post_ids' => [],
                'scan_gap' => false,
            ];
        }

        $summary = BlockReferenceScanner::collectBuilderContainerIds($builderBridgeEntity, $scanPostTypes);

        return [
            'post_ids' => isset($summary['post_ids']) && is_array($summary['post_ids'])
                ? array_values(array_unique(array_filter(array_map('absint', $summary['post_ids']))))
                : [],
            'scan_gap' => (bool) ($summary['scan_gap'] ?? false),
        ];
    }
}
