<?php
declare(strict_types=1);

namespace Ekesto\CacheInvalidator;

/**
 * Shared template-part block matching helpers.
 */
final class TemplatePartReferenceHelper
{
    /**
     * Match state: no reference found.
     *
     * @var string
     */
    public const STATE_NONE = 'none';

    /**
     * Match state: reference confirmed deterministically.
     *
     * @var string
     */
    public const STATE_CONFIRMED = 'confirmed';

    /**
     * Match state: reference-like signal exists but deterministic mapping is impossible.
     *
     * @var string
     */
    public const STATE_AMBIGUOUS = 'ambiguous';

    /**
     * Return candidate LIKE needles for template-part references.
     *
     * @param \WP_Post $templatePart Template part entity.
     * @return string[]
     */
    public static function candidateNeedles(\WP_Post $templatePart): array
    {
        $slug = self::resolveSlug($templatePart);
        if ($slug === '') {
            return [];
        }

        return ['%"slug":"' . $slug . '"%'];
    }

    /**
     * Resolve the reference state for one container post.
     *
     * @param \WP_Post $container Container post.
     * @param \WP_Post $templatePart Template part entity.
     * @return string One of the STATE_* constants.
     */
    public static function referenceState(\WP_Post $container, \WP_Post $templatePart): string
    {
        return self::contentReferenceState((string) $container->post_content, $templatePart);
    }

    /**
     * Resolve the reference state for raw block-template content.
     *
     * @param string   $content Raw block content.
     * @param \WP_Post $templatePart Template part entity.
     * @return string One of the STATE_* constants.
     */
    public static function contentReferenceState(string $content, \WP_Post $templatePart): string
    {
        $blocks = parse_blocks($content);
        if (!is_array($blocks) || empty($blocks)) {
            return self::STATE_NONE;
        }

        return self::blocksReferenceState($blocks, $templatePart);
    }

    /**
     * Recursively resolve the reference state for parsed blocks.
     *
     * @param array<int,array<string,mixed>> $blocks Parsed blocks.
     * @param \WP_Post $templatePart Template part entity.
     * @return string One of the STATE_* constants.
     */
    private static function blocksReferenceState(array $blocks, \WP_Post $templatePart): string
    {
        $hasAmbiguous = false;

        foreach ($blocks as $block) {
            if (!is_array($block)) {
                continue;
            }

            $state = self::blockReferenceState($block, $templatePart);
            if ($state === self::STATE_CONFIRMED) {
                return self::STATE_CONFIRMED;
            }

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

            $innerBlocks = $block['innerBlocks'] ?? [];
            if (!is_array($innerBlocks) || empty($innerBlocks)) {
                continue;
            }

            $innerState = self::blocksReferenceState($innerBlocks, $templatePart);
            if ($innerState === self::STATE_CONFIRMED) {
                return self::STATE_CONFIRMED;
            }

            if ($innerState === self::STATE_AMBIGUOUS) {
                $hasAmbiguous = true;
            }
        }

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

    /**
     * Resolve the reference state for one block.
     *
     * @param array<string,mixed> $block Parsed block.
     * @param \WP_Post $templatePart Template part entity.
     * @return string One of the STATE_* constants.
     */
    private static function blockReferenceState(array $block, \WP_Post $templatePart): string
    {
        $blockName = (string) ($block['blockName'] ?? '');
        if ($blockName !== 'core/template-part') {
            return self::STATE_NONE;
        }

        $attrs = $block['attrs'] ?? [];
        if (!is_array($attrs)) {
            return self::STATE_NONE;
        }

        $slug = sanitize_title((string) ($attrs['slug'] ?? ''));
        if ($slug === '' || $slug !== self::resolveSlug($templatePart)) {
            return self::STATE_NONE;
        }

        $entityThemes = self::resolveThemeSlugs($templatePart);
        $blockTheme = sanitize_key((string) ($attrs['theme'] ?? ''));
        if ($blockTheme !== '') {
            return in_array($blockTheme, $entityThemes, true)
                ? self::STATE_CONFIRMED
                : self::STATE_NONE;
        }

        if (empty($entityThemes)) {
            return self::STATE_AMBIGUOUS;
        }

        return self::matchesActiveTheme($entityThemes)
            ? self::STATE_CONFIRMED
            : self::STATE_AMBIGUOUS;
    }

    /**
     * Resolve the template-part slug key.
     *
     * @param \WP_Post $templatePart Template part entity.
     * @return string
     */
    private static function resolveSlug(\WP_Post $templatePart): string
    {
        $slug = sanitize_title((string) $templatePart->post_name);
        if ($slug !== '') {
            return $slug;
        }

        return sanitize_title((string) $templatePart->post_title);
    }

    /**
     * Resolve theme slugs assigned to the template part.
     *
     * @param \WP_Post $templatePart Template part entity.
     * @return string[]
     */
    private static function resolveThemeSlugs(\WP_Post $templatePart): array
    {
        $terms = get_the_terms((int) $templatePart->ID, 'wp_theme');
        if (!is_array($terms)) {
            return [];
        }

        return ContentVisibility::normalizePostTypes(array_map(
            static fn($term): string => $term instanceof \WP_Term ? (string) $term->slug : '',
            $terms
        ));
    }

    /**
     * Determine whether the template part belongs to the active theme context.
     *
     * @param string[] $entityThemes Template part theme slugs.
     * @return bool
     */
    private static function matchesActiveTheme(array $entityThemes): bool
    {
        $activeThemes = ContentVisibility::normalizePostTypes([
            (string) get_stylesheet(),
            (string) get_template(),
        ]);

        foreach ($activeThemes as $theme) {
            if (in_array($theme, $entityThemes, true)) {
                return true;
            }
        }

        return false;
    }
}
