<?php
declare(strict_types=1);

namespace Ekesto\CacheInvalidator\Integrations\Structural\Providers\WordPress;

use Ekesto\CacheInvalidator\BlockReferenceScanner;
use Ekesto\CacheInvalidator\ContentVisibility;
use Ekesto\CacheInvalidator\Support\Debug;
use Ekesto\CacheInvalidator\Integrations\Multilingual\Internal\ObjectSiblingResolver;
use Ekesto\CacheInvalidator\Integrations\Structural\Providers\Contracts\StructuralContainerProviderInterface;
use Ekesto\CacheInvalidator\Integrations\Structural\Core\StructuralContainerReference;
use Ekesto\CacheInvalidator\Integrations\Structural\Core\StructuralContainerResolution;

/**
 * Structural provider for navigation entities.
 */
final class NavigationProvider implements StructuralContainerProviderInterface
{
    /**
     * Return the provider key.
     *
     * @return string
     */
    public function key(): string
    {
        return 'wp_navigation';
    }

    /**
     * Return structural post types classified by this provider.
     *
     * @return string[]
     */
    public function structuralPostTypes(): array
    {
        return ['wp_navigation'];
    }

    /**
     * Determine whether this provider owns the provided post.
     *
     * @param \WP_Post $post Post object.
     * @return bool
     */
    public function supports(\WP_Post $post): bool
    {
        return $post->post_type === 'wp_navigation';
    }

    /**
     * Resolve one provider-owned usage step.
     *
     * @param int $postId Structural entity ID.
     * @return StructuralContainerResolution
     */
    public function resolveUsage(int $postId): StructuralContainerResolution
    {
        $post = get_post($postId);
        if (!$post instanceof \WP_Post || !$this->supports($post)) {
            return new StructuralContainerResolution([], [], false, false, false);
        }

        [$directScanPostTypes, $structuralScanPostTypes] = $this->resolveScanPostTypes();
        $navigationIds = ObjectSiblingResolver::expandObjectIds($postId, 'wp_navigation');
        if (empty($navigationIds) || (empty($directScanPostTypes) && empty($structuralScanPostTypes))) {
            return new StructuralContainerResolution([], [], false, false, false);
        }

        $needles = $this->buildRefNeedles($navigationIds);
        $directCandidates = BlockReferenceScanner::findCandidatePostIdsByContentNeedles($directScanPostTypes, $needles);
        $structuralCandidates = BlockReferenceScanner::findCandidatePostIdsByContentNeedles($structuralScanPostTypes, $needles);
        $adapterCandidates = BlockReferenceScanner::collectBuilderContainerIds(
            $post,
            array_merge($directScanPostTypes, $structuralScanPostTypes)
        );

        $directTargetIds = [];
        $structuralReferences = [];
        $hasReferences = false;
        $uncertainDueToContext = false;
        $uncertainDueToScanGap = (bool) ($directCandidates['scan_gap'] ?? false)
            || (bool) ($structuralCandidates['scan_gap'] ?? false)
            || (bool) ($adapterCandidates['scan_gap'] ?? false);

        foreach ((array) ($directCandidates['post_ids'] ?? []) as $candidateId) {
            $candidate = get_post((int) $candidateId);
            if (!$candidate instanceof \WP_Post || !ContentVisibility::hasValidResolutionStatus($candidate)) {
                continue;
            }

            if (!$this->postReferencesNavigation($candidate, $navigationIds)) {
                continue;
            }

            $hasReferences = true;
            if (ContentVisibility::shouldTreatAsStructuralReference($candidate)) {
                continue;
            }

            if (ContentVisibility::hasStructuralReferenceUncertainty($candidate)) {
                $uncertainDueToContext = true;
                continue;
            }

            if (ContentVisibility::canEmitPreciseTarget($candidate)) {
                $directTargetIds[] = (int) $candidate->ID;
                continue;
            }

            $uncertainDueToContext = true;
        }

        foreach ((array) ($structuralCandidates['post_ids'] ?? []) as $candidateId) {
            $candidate = get_post((int) $candidateId);
            if (!$candidate instanceof \WP_Post || !ContentVisibility::hasValidResolutionStatus($candidate)) {
                continue;
            }

            if (!$this->postReferencesNavigation($candidate, $navigationIds)) {
                continue;
            }

            $hasReferences = true;
            if (ContentVisibility::shouldTreatAsStructuralReference($candidate)) {
                $structuralReferences[] = new StructuralContainerReference((int) $candidate->ID, (string) $candidate->post_type);
                continue;
            }

            if (ContentVisibility::hasStructuralReferenceUncertainty($candidate)) {
                $uncertainDueToContext = true;
                continue;
            }

            if (ContentVisibility::canEmitPreciseTarget($candidate)) {
                $directTargetIds[] = (int) $candidate->ID;
                continue;
            }

            $uncertainDueToContext = true;
        }

        $builderNeedles = $this->buildRefTextNeedles($navigationIds);
        $builderLocationsByPostId = isset($adapterCandidates['storage_locations_by_post_id']) && is_array($adapterCandidates['storage_locations_by_post_id'])
            ? $adapterCandidates['storage_locations_by_post_id']
            : [];
        foreach ((array) ($adapterCandidates['post_ids'] ?? []) as $candidateId) {
            $candidate = get_post((int) $candidateId);
            if (!$candidate instanceof \WP_Post || !ContentVisibility::hasValidResolutionStatus($candidate)) {
                continue;
            }

            $storageLocations = isset($builderLocationsByPostId[(int) $candidate->ID]) && is_array($builderLocationsByPostId[(int) $candidate->ID])
                ? $builderLocationsByPostId[(int) $candidate->ID]
                : [];
            if (!BlockReferenceScanner::postContainsNeedlesInDeclaredLocations($candidate, $storageLocations, $builderNeedles)) {
                continue;
            }

            $hasReferences = true;
            if (ContentVisibility::shouldTreatAsStructuralReference($candidate)) {
                $structuralReferences[] = new StructuralContainerReference((int) $candidate->ID, (string) $candidate->post_type);
                continue;
            }

            if (ContentVisibility::hasStructuralReferenceUncertainty($candidate)) {
                $uncertainDueToContext = true;
                continue;
            }

            if (ContentVisibility::canEmitPreciseTarget($candidate)) {
                $directTargetIds[] = (int) $candidate->ID;
                continue;
            }

            $uncertainDueToContext = true;
        }

        Debug::verbose(sprintf(
            'navigation provider: post_id=%d direct=%d structural=%d uncertain_context=%s uncertain_scan_gap=%s',
            $postId,
            count($directTargetIds),
            count($structuralReferences),
            $uncertainDueToContext ? 'yes' : 'no',
            $uncertainDueToScanGap ? 'yes' : 'no'
        ));

        return new StructuralContainerResolution(
            $directTargetIds,
            $structuralReferences,
            $hasReferences,
            $uncertainDueToContext,
            $uncertainDueToScanGap
        );
    }

    /**
     * Resolve direct and structural scan post types.
     *
     * @return array{0:string[],1:string[]}
     */
    private function resolveScanPostTypes(): array
    {
        $default = ContentVisibility::getViewablePublicPostTypes();

        /**
         * Filter post types scanned for navigation references.
         *
         * @param string[] $default Default viewable post types.
         */
        $filtered = (array) apply_filters('ekesto_ci_navigation_scan_post_types', $default);
        $filtered = ContentVisibility::normalizePostTypes($filtered);

        $direct = [];
        $structural = ContentVisibility::structuralPostTypes();

        foreach ($filtered as $postType) {
            if (ContentVisibility::isViewablePublicPostType($postType)) {
                $direct[] = $postType;
                continue;
            }

            $structural[] = $postType;
        }

        return [
            ContentVisibility::normalizePostTypes($direct),
            ContentVisibility::normalizePostTypes($structural),
        ];
    }

    /**
     * Build raw post_content LIKE needles for numeric block refs.
     *
     * @param int[] $refIds Reference IDs.
     * @return string[]
     */
    private function buildRefNeedles(array $refIds): array
    {
        return array_values(array_map(
            static fn(int $refId): string => '%"ref":' . $refId . '%',
            ContentVisibility::normalizePostIds($refIds)
        ));
    }

    /**
     * Build raw text needles for declared builder storage locations.
     *
     * @param int[] $refIds Reference IDs.
     * @return string[]
     */
    private function buildRefTextNeedles(array $refIds): array
    {
        return array_values(array_map(
            static fn(int $refId): string => '"ref":' . $refId,
            ContentVisibility::normalizePostIds($refIds)
        ));
    }

    /**
     * Determine whether a post references a navigation entity.
     *
     * @param \WP_Post $post Container post.
     * @param int[]    $navigationIds Navigation entity IDs.
     * @return bool
     */
    private function postReferencesNavigation(\WP_Post $post, array $navigationIds): bool
    {
        $navigationIds = ContentVisibility::normalizePostIds($navigationIds);
        if (empty($navigationIds)) {
            return false;
        }

        return BlockReferenceScanner::postContainsMatchingBlock(
            $post,
            static function (array $block) use ($navigationIds): bool {
                $name = (string) ($block['blockName'] ?? '');
                $attrs = $block['attrs'] ?? [];
                if ($name !== 'core/navigation' || !is_array($attrs)) {
                    return false;
                }

                return in_array(absint($attrs['ref'] ?? 0), $navigationIds, true);
            }
        );
    }
}
