<?php
declare(strict_types=1);

namespace Ekesto\CacheInvalidator\Integrations\Structural\Core;

use Ekesto\CacheInvalidator\ContentVisibility;
use Ekesto\CacheInvalidator\Integrations\Structural\Providers\Contracts\StructuralContainerProviderInterface;
use Ekesto\CacheInvalidator\Support\Debug;
use Ekesto\CacheInvalidator\UsageResolutionResult;

/**
 * Shared orchestrator for structural-container traversal and normalization.
 */
final class StructuralContainerResolver
{
    private const DEFAULT_TRAVERSAL_CAP = 64;

    private StructuralContainerRegistry $registry;

    /**
     * Build the structural resolver.
     *
     * @param StructuralContainerRegistry|null $registry Optional provider registry.
     */
    public function __construct(?StructuralContainerRegistry $registry = null)
    {
        $this->registry = $registry ?? new StructuralContainerRegistry();
    }

    /**
     * Resolve precise front-end post IDs affected by a structural entity change.
     *
     * @param \WP_Post $post Changed structural entity.
     * @return array{post_ids:int[],target_urls:string[],has_references:bool,uncertain:bool,uncertain_due_to_context:bool,uncertain_due_to_scan_gap:bool,direct_matches_count:int,direct_url_matches_count:int,structural_matches_count:int,fallback_reason_hint:string}
     */
    public function resolveReferencedPostIds(\WP_Post $post): array
    {
        $provider = $this->providerForPost($post);
        if ($provider === null) {
            return $this->buildResult([], [], [], false, false, false, '');
        }

        $directTargetIds = [];
        $directTargetUrls = [];
        $structuralReferences = [];
        $hasReferences = false;
        $uncertainDueToContext = false;
        $uncertainDueToScanGap = false;
        $fallbackReasonHint = '';
        $processedCount = 0;
        $traversalCap = $this->resolveTraversalCap();

        $pending = [new StructuralContainerReference((int) $post->ID, (string) $post->post_type)];
        $visited = [];
        $queued = [
            sanitize_key((string) $post->post_type) . ':' . (int) $post->ID => true,
        ];

        while (!empty($pending)) {
            /** @var StructuralContainerReference $reference */
            $reference = array_shift($pending);
            $referenceKey = $reference->canonicalKey();
            unset($queued[$referenceKey]);

            if (isset($visited[$referenceKey])) {
                continue;
            }

            $visited[$referenceKey] = true;
            $processedCount++;
            if ($processedCount > $traversalCap) {
                $uncertainDueToContext = true;
                break;
            }

            $current = get_post($reference->postId());
            if (!$current instanceof \WP_Post) {
                continue;
            }

            $currentProvider = $this->providerForPost($current);
            if ($currentProvider === null) {
                $uncertainDueToContext = true;
                continue;
            }

            $resolution = $currentProvider->resolveUsage((int) $current->ID);
            $hasReferences = $hasReferences || $resolution->hasReferences();
            $uncertainDueToContext = $uncertainDueToContext || $resolution->uncertainDueToContext();
            $uncertainDueToScanGap = $uncertainDueToScanGap || $resolution->uncertainDueToScanGap();
            $directTargetIds = array_merge($directTargetIds, $resolution->directTargetIds());
            $directTargetUrls = array_merge($directTargetUrls, $resolution->directTargetUrls());
            if ($fallbackReasonHint === '' && $resolution->fallbackReasonHint() !== '') {
                $fallbackReasonHint = $resolution->fallbackReasonHint();
            }

            foreach ($resolution->structuralReferences() as $structuralReference) {
                $structuralKey = $structuralReference->canonicalKey();
                $structuralReferences[$structuralKey] = $structuralReference;
                $hasReferences = true;

                if (!$this->isStructuralPostType($structuralReference->postType())) {
                    $uncertainDueToContext = true;
                    continue;
                }

                if (isset($visited[$structuralKey]) || isset($queued[$structuralKey])) {
                    continue;
                }

                if ($processedCount + count($pending) >= $traversalCap) {
                    $uncertainDueToContext = true;
                    continue;
                }

                $pending[] = $structuralReference;
                $queued[$structuralKey] = true;
            }
        }

        Debug::verbose(sprintf(
            'structural traversal: entity_id=%d entity_type=%s processed=%d pending=%d cap=%d cap_hit=%s structural_refs=%d direct_targets=%d direct_url_targets=%d uncertain_context=%s uncertain_scan_gap=%s',
            (int) $post->ID,
            sanitize_key((string) $post->post_type),
            $processedCount,
            count($pending),
            $traversalCap,
            $processedCount > $traversalCap ? 'yes' : 'no',
            count($structuralReferences),
            count($directTargetIds),
            count($directTargetUrls),
            $uncertainDueToContext ? 'yes' : 'no',
            $uncertainDueToScanGap ? 'yes' : 'no'
        ));

        return $this->buildResult(
            $directTargetIds,
            $directTargetUrls,
            array_values($structuralReferences),
            $hasReferences,
            $uncertainDueToContext,
            $uncertainDueToScanGap,
            $fallbackReasonHint
        );
    }

    /**
     * Resolve the defensive traversal cap.
     *
     * @return int
     */
    private function resolveTraversalCap(): int
    {
        /**
         * Filter the structural traversal cap.
         *
         * @param int $cap Defensive traversal cap.
         */
        return max(1, (int) apply_filters('ekesto_ci_structural_traversal_cap', self::DEFAULT_TRAVERSAL_CAP));
    }

    /**
     * Build the normalized public resolver payload.
     *
     * @param int[]                          $directTargetIds Direct precise target IDs.
     * @param string[]                       $directTargetUrls Direct precise target URLs.
     * @param StructuralContainerReference[] $structuralReferences Unique structural references.
     * @param bool                           $hasReferences Whether any references were detected.
     * @param bool                           $uncertainDueToContext Context uncertainty.
     * @param bool                           $uncertainDueToScanGap Scan-gap uncertainty.
     * @param string                         $fallbackReasonHint Diagnostic fallback reason hint.
     * @return array{post_ids:int[],target_urls:string[],has_references:bool,uncertain:bool,uncertain_due_to_context:bool,uncertain_due_to_scan_gap:bool,direct_matches_count:int,direct_url_matches_count:int,structural_matches_count:int,fallback_reason_hint:string}
     */
    private function buildResult(
        array $directTargetIds,
        array $directTargetUrls,
        array $structuralReferences,
        bool $hasReferences,
        bool $uncertainDueToContext,
        bool $uncertainDueToScanGap,
        string $fallbackReasonHint
    ): array {
        $directTargetIds = ContentVisibility::normalizePostIds($directTargetIds);
        $directTargetUrls = ContentVisibility::normalizeUrls($directTargetUrls);
        $structuralCount = 0;

        foreach ($structuralReferences as $reference) {
            if ($reference instanceof StructuralContainerReference) {
                $structuralCount++;
            }
        }

        if (empty($directTargetIds) && empty($directTargetUrls) && $structuralCount > 0) {
            $uncertainDueToContext = true;
        }

        $result = new UsageResolutionResult(
            $directTargetIds,
            $hasReferences || !empty($directTargetIds) || $structuralCount > 0,
            $uncertainDueToContext,
            $uncertainDueToScanGap,
            count($directTargetIds),
            $structuralCount,
            $directTargetUrls,
            count($directTargetUrls),
            $fallbackReasonHint
        );

        return $result->toArray();
    }

    /**
     * Resolve the owning provider for one post.
     *
     * @param \WP_Post $post Post object.
     * @return StructuralContainerProviderInterface|null
     */
    private function providerForPost(\WP_Post $post): ?StructuralContainerProviderInterface
    {
        $postType = sanitize_key((string) $post->post_type);
        if ($postType === '') {
            return null;
        }

        // Synced-pattern ownership depends on the specific wp_block instance, not just post type.
        if ($postType === 'wp_block') {
            return $this->registry->providerForPost($post);
        }

        return $this->registry->providerForPost($post);
    }

    /**
     * Determine whether the post type is structural.
     *
     * @param string $postType Post type key.
     * @return bool
     */
    private function isStructuralPostType(string $postType): bool
    {
        $postType = sanitize_key($postType);
        if ($postType === '') {
            return false;
        }

        return $this->registry->isStructuralPostType($postType);
    }
}
