<?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;
use Ekesto\CacheInvalidator\TemplatePartReferenceHelper;

/**
 * Structural provider for template-part entities.
 */
final class TemplatePartProvider implements StructuralContainerProviderInterface
{
    /**
     * Return the provider key.
     *
     * @return string
     */
    public function key(): string
    {
        return 'wp_template_part';
    }

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

    /**
     * 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_template_part';
    }

    /**
     * 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();
        $templateParts = $this->resolveTemplatePartVariants($postId);
        $needles = $this->candidateNeedles($templateParts);
        if (empty($templateParts) || (empty($directScanPostTypes) && empty($structuralScanPostTypes))) {
            return new StructuralContainerResolution([], [], false, false, false);
        }

        $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;
            }

            $state = $this->referenceState($candidate, $templateParts);
            if ($state === TemplatePartReferenceHelper::STATE_NONE) {
                continue;
            }

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

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

            if ($state === TemplatePartReferenceHelper::STATE_CONFIRMED && 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;
            }

            $state = $this->referenceState($candidate, $templateParts);
            if ($state === TemplatePartReferenceHelper::STATE_NONE) {
                continue;
            }

            $hasReferences = true;
            if ($state === TemplatePartReferenceHelper::STATE_AMBIGUOUS) {
                $uncertainDueToContext = true;
            }

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

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

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

            $uncertainDueToContext = true;
        }

        $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, $needles)) {
                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;
        }

        $activeTemplateCoverage = $this->resolveActiveTemplateCoverage($templateParts, $structuralReferences);
        $hasReferences = $hasReferences || $activeTemplateCoverage['has_references'];
        $uncertainDueToContext = $uncertainDueToContext || $activeTemplateCoverage['uncertain_due_to_context'];
        $uncertainDueToScanGap = $uncertainDueToScanGap || $activeTemplateCoverage['uncertain_due_to_scan_gap'];
        $fallbackReasonHint = $activeTemplateCoverage['uncertain_due_to_scan_gap']
            ? 'uncovered_block_template_reference'
            : '';

        Debug::verbose(sprintf(
            'template_part 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,
            [],
            $fallbackReasonHint
        );
    }

    /**
     * Check active WordPress templates for references outside database traversal.
     *
     * @param \WP_Post[]                     $templateParts Logical template-part variants.
     * @param StructuralContainerReference[] $structuralReferences Database-backed references.
     * @return array{has_references:bool,uncertain_due_to_context:bool,uncertain_due_to_scan_gap:bool}
     */
    private function resolveActiveTemplateCoverage(array $templateParts, array $structuralReferences): array
    {
        if (!function_exists('get_block_templates')) {
            return [
                'has_references' => false,
                'uncertain_due_to_context' => false,
                'uncertain_due_to_scan_gap' => true,
            ];
        }

        $coveredPostIds = [];
        foreach ($structuralReferences as $reference) {
            if ($reference instanceof StructuralContainerReference && $reference->postType() === 'wp_template') {
                $coveredPostIds[$reference->postId()] = true;
            }
        }

        try {
            $templates = get_block_templates([], 'wp_template');
        } catch (\Throwable $throwable) {
            Debug::warn('active block-template coverage lookup failed', [
                'exception' => $throwable->getMessage(),
            ]);

            return [
                'has_references' => false,
                'uncertain_due_to_context' => false,
                'uncertain_due_to_scan_gap' => true,
            ];
        }

        if (!is_array($templates)) {
            return [
                'has_references' => false,
                'uncertain_due_to_context' => false,
                'uncertain_due_to_scan_gap' => true,
            ];
        }

        $hasReferences = false;
        $uncertainDueToContext = false;
        $uncertainDueToScanGap = false;

        foreach ($templates as $template) {
            if (!$template instanceof \WP_Block_Template) {
                $uncertainDueToScanGap = true;
                continue;
            }

            $state = $this->blockTemplateReferenceState($template, $templateParts);
            if ($state === TemplatePartReferenceHelper::STATE_NONE) {
                continue;
            }

            $hasReferences = true;
            if ($state === TemplatePartReferenceHelper::STATE_AMBIGUOUS) {
                $uncertainDueToContext = true;
                continue;
            }

            $templatePostId = absint($template->wp_id ?? 0);
            if ($templatePostId <= 0 || !isset($coveredPostIds[$templatePostId])) {
                $uncertainDueToScanGap = true;
            }
        }

        return [
            'has_references' => $hasReferences,
            'uncertain_due_to_context' => $uncertainDueToContext,
            'uncertain_due_to_scan_gap' => $uncertainDueToScanGap,
        ];
    }

    /**
     * Resolve one active block template against all logical part variants.
     *
     * @param \WP_Block_Template $template Active block template.
     * @param \WP_Post[]         $templateParts Logical template-part variants.
     * @return string One of the TemplatePartReferenceHelper STATE_* constants.
     */
    private function blockTemplateReferenceState(\WP_Block_Template $template, array $templateParts): string
    {
        $hasAmbiguous = false;

        foreach ($templateParts as $templatePart) {
            if (!$templatePart instanceof \WP_Post) {
                continue;
            }

            $state = TemplatePartReferenceHelper::contentReferenceState((string) $template->content, $templatePart);
            if ($state === TemplatePartReferenceHelper::STATE_CONFIRMED) {
                return TemplatePartReferenceHelper::STATE_CONFIRMED;
            }

            if ($state === TemplatePartReferenceHelper::STATE_AMBIGUOUS) {
                $hasAmbiguous = true;
            }
        }

        return $hasAmbiguous ? TemplatePartReferenceHelper::STATE_AMBIGUOUS : TemplatePartReferenceHelper::STATE_NONE;
    }

    /**
     * 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 template-part references.
         *
         * @param string[] $default Default viewable post types.
         */
        $filtered = (array) apply_filters('ekesto_ci_template_part_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),
        ];
    }

    /**
     * Resolve multilingual template-part variants for one changed entity.
     *
     * @param int $templatePartId Changed template part ID.
     * @return \WP_Post[]
     */
    private function resolveTemplatePartVariants(int $templatePartId): array
    {
        $resolved = [];

        foreach (ObjectSiblingResolver::expandObjectIds($templatePartId, 'wp_template_part') as $candidateId) {
            $candidate = get_post($candidateId);
            if (!$candidate instanceof \WP_Post || $candidate->post_type !== 'wp_template_part') {
                continue;
            }

            $resolved[(int) $candidate->ID] = $candidate;
        }

        if (empty($resolved)) {
            $candidate = get_post($templatePartId);
            if ($candidate instanceof \WP_Post && $candidate->post_type === 'wp_template_part') {
                $resolved[(int) $candidate->ID] = $candidate;
            }
        }

        return array_values($resolved);
    }

    /**
     * Build candidate LIKE needles for all logical template-part variants.
     *
     * @param \WP_Post[] $templateParts Template-part variants.
     * @return string[]
     */
    private function candidateNeedles(array $templateParts): array
    {
        $needles = [];

        foreach ($templateParts as $templatePart) {
            if (!$templatePart instanceof \WP_Post) {
                continue;
            }

            $needles = array_merge($needles, TemplatePartReferenceHelper::candidateNeedles($templatePart));
        }

        return ContentVisibility::normalizeUrls($needles);
    }

    /**
     * Resolve the template-part reference state across all logical variants.
     *
     * @param \WP_Post   $container Container post.
     * @param \WP_Post[] $templateParts Template-part variants.
     * @return string
     */
    private function referenceState(\WP_Post $container, array $templateParts): string
    {
        $hasAmbiguous = false;

        foreach ($templateParts as $templatePart) {
            if (!$templatePart instanceof \WP_Post) {
                continue;
            }

            $state = TemplatePartReferenceHelper::referenceState($container, $templatePart);
            if ($state === TemplatePartReferenceHelper::STATE_CONFIRMED) {
                return TemplatePartReferenceHelper::STATE_CONFIRMED;
            }

            if ($state === TemplatePartReferenceHelper::STATE_AMBIGUOUS) {
                $hasAmbiguous = true;
            }
        }

        return $hasAmbiguous ? TemplatePartReferenceHelper::STATE_AMBIGUOUS : TemplatePartReferenceHelper::STATE_NONE;
    }
}
