<?php
declare(strict_types=1);

namespace Ekesto\CacheInvalidator;

use Ekesto\CacheInvalidator\Core\Elementor\ElementorDocumentClassification;
use Ekesto\CacheInvalidator\Core\Elementor\ElementorIntegrationRegistry;
use Ekesto\CacheInvalidator\Integrations\Structural\Core\StructuralContainerRegistry;

/**
 * Shared visibility and normalization helpers for precise invalidation targets.
 */
final class ContentVisibility
{
    /**
     * Internal post types excluded from front-end direct targeting.
     *
     * @var string[]
     */
    private const INTERNAL_EXCLUDED_POST_TYPES = ['attachment', 'custom_css', 'customize_changeset', 'nav_menu_item', 'revision'];

    /**
     * Post statuses ignored during resolver scans.
     *
     * @var string[]
     */
    private const INVALID_POST_STATUSES = ['auto-draft', 'inherit', 'trash'];

    /**
     * Return structural post types.
     *
     * @return string[]
     */
    public static function structuralPostTypes(): array
    {
        return (new StructuralContainerRegistry())->structuralPostTypes();
    }

    /**
     * Normalize post IDs into a unique ascending list.
     *
     * @param array<int,mixed> $postIds Raw post IDs.
     * @return int[]
     */
    public static function normalizePostIds(array $postIds): array
    {
        $normalized = array_values(array_unique(array_filter(array_map('absint', $postIds), static fn(int $id): bool => $id > 0)));
        sort($normalized, SORT_NUMERIC);

        return $normalized;
    }

    /**
     * Normalize post types into a unique sorted list.
     *
     * @param array<int,mixed> $postTypes Raw post types.
     * @return string[]
     */
    public static function normalizePostTypes(array $postTypes): array
    {
        $normalized = array_values(array_unique(array_filter(array_map(
            static fn($postType): string => sanitize_key((string) $postType),
            $postTypes
        ))));
        sort($normalized, SORT_STRING);

        return $normalized;
    }

    /**
     * Normalize URLs into a unique sorted list.
     *
     * @param array<int,mixed> $urls Raw URLs.
     * @return string[]
     */
    public static function normalizeUrls(array $urls): array
    {
        $normalized = array_values(array_unique(array_filter(array_map(
            static fn($url): string => trim((string) $url),
            $urls
        ))));
        sort($normalized, SORT_STRING);

        return $normalized;
    }

    /**
     * Determine whether the post type is structural.
     *
     * @param string $postType Post type key.
     * @return bool
     */
    public static function isStructuralPostType(string $postType): bool
    {
        return in_array(sanitize_key($postType), self::structuralPostTypes(), true);
    }

    /**
     * Determine whether the post type is eligible for direct front-end scans.
     *
     * This mirrors the global invalidation baseline: public post types filtered
     * through `is_post_type_viewable()` semantics and excluding internal types.
     *
     * @param string $postType Post type key.
     * @return bool
     */
    public static function isViewablePublicPostType(string $postType): bool
    {
        $postType = sanitize_key($postType);
        if ($postType === '' || in_array($postType, self::INTERNAL_EXCLUDED_POST_TYPES, true)) {
            return false;
        }

        if ($postType !== 'elementor_library' && self::isStructuralPostType($postType)) {
            return false;
        }

        return self::isPostTypeViewable($postType);
    }

    /**
     * Determine whether the post type is viewable using core semantics.
     *
     * @param string $postType Post type key.
     * @return bool
     */
    public static function isPostTypeViewable(string $postType): bool
    {
        $postType = sanitize_key($postType);
        if ($postType === '') {
            return false;
        }

        if (function_exists('is_post_type_viewable')) {
            return is_post_type_viewable($postType);
        }

        $postTypeObject = get_post_type_object($postType);
        return $postTypeObject instanceof \WP_Post_Type && !empty($postTypeObject->public);
    }

    /**
     * Return all viewable public post types used for direct targeting.
     *
     * @return string[]
     */
    public static function getViewablePublicPostTypes(): array
    {
        $postTypes = get_post_types(['public' => true], 'names');
        if (!is_array($postTypes) || empty($postTypes)) {
            return [];
        }

        $postTypes = self::normalizePostTypes($postTypes);
        $postTypes = array_values(array_diff($postTypes, self::INTERNAL_EXCLUDED_POST_TYPES));

        $postTypes = array_values(array_filter($postTypes, static function (string $postType): bool {
            return self::isViewablePublicPostType($postType);
        }));

        return self::normalizePostTypes($postTypes);
    }

    /**
     * Classify one Elementor library post instance.
     *
     * @param \WP_Post $post Post object.
     * @return string
     */
    public static function classifyElementorLibraryPost(\WP_Post $post): string
    {
        if (sanitize_key((string) $post->post_type) !== 'elementor_library') {
            return ElementorDocumentClassification::NON_STRUCTURAL;
        }

        return ElementorIntegrationRegistry::current()->classifyLibraryPost((int) $post->ID);
    }

    /**
     * Determine whether a resolved post should traverse as a structural reference.
     *
     * @param \WP_Post $post Post object.
     * @return bool
     */
    public static function shouldTreatAsStructuralReference(\WP_Post $post): bool
    {
        $postType = sanitize_key((string) $post->post_type);
        if ($postType === 'elementor_library') {
            return self::classifyElementorLibraryPost($post) === ElementorDocumentClassification::STRUCTURAL_CONTAINER;
        }

        return self::isStructuralPostType($postType);
    }

    /**
     * Determine whether structural-reference classification is uncertain for a post.
     *
     * @param \WP_Post $post Post object.
     * @return bool
     */
    public static function hasStructuralReferenceUncertainty(\WP_Post $post): bool
    {
        if (sanitize_key((string) $post->post_type) !== 'elementor_library') {
            return false;
        }

        return in_array(
            self::classifyElementorLibraryPost($post),
            [
                ElementorDocumentClassification::PRESENTATION_TEMPLATE,
                ElementorDocumentClassification::UNKNOWN,
            ],
            true
        );
    }

    /**
     * Determine whether a post is valid for resolver scans.
     *
     * @param \WP_Post $post Post object.
     * @return bool
     */
    public static function hasValidResolutionStatus(\WP_Post $post): bool
    {
        $status = sanitize_key((string) $post->post_status);
        if ($status === '' || in_array($status, self::INVALID_POST_STATUSES, true)) {
            return false;
        }

        return !wp_is_post_revision((int) $post->ID) && !wp_is_post_autosave((int) $post->ID);
    }

    /**
     * Determine whether a post is a deterministic direct precise target.
     *
     * @param \WP_Post $post Post object.
     * @return bool
     */
    public static function canEmitPreciseTarget(\WP_Post $post): bool
    {
        return self::resolvePreciseUrlForPost($post) !== '';
    }

    /**
     * Resolve a deterministic warmable front-end URL for a post.
     *
     * @param \WP_Post $post Post object.
     * @return string
     */
    public static function resolvePreciseUrlForPost(\WP_Post $post): string
    {
        if (!self::hasValidResolutionStatus($post)) {
            return '';
        }

        $postType = sanitize_key((string) $post->post_type);
        if ($postType === 'elementor_library') {
            if (!self::isPostTypeViewable($postType)) {
                return '';
            }

            if (self::classifyElementorLibraryPost($post) !== ElementorDocumentClassification::NON_STRUCTURAL) {
                return '';
            }
        } elseif (!self::isViewablePublicPostType($postType)) {
            return '';
        }

        if (!self::isPubliclyViewablePost($post)) {
            return '';
        }

        $url = trim((string) get_permalink((int) $post->ID));
        if ($url === '' || self::isRejectedPreciseUrl($url)) {
            return '';
        }

        return $url;
    }

    /**
     * Determine whether a post is publicly viewable for front-end URL emission.
     *
     * @param \WP_Post $post Post object.
     * @return bool
     */
    public static function isPubliclyViewablePost(\WP_Post $post): bool
    {
        if (function_exists('is_post_publicly_viewable')) {
            return is_post_publicly_viewable($post);
        }

        if (sanitize_key((string) $post->post_status) !== 'publish') {
            return false;
        }

        return self::isPostTypeViewable((string) $post->post_type);
    }

    /**
     * Determine whether a URL is unsuitable for precise front-end emission.
     *
     * @param string $url Absolute URL.
     * @return bool
     */
    private static function isRejectedPreciseUrl(string $url): bool
    {
        $url = trim($url);
        if ($url === '') {
            return true;
        }

        $adminBase = trailingslashit((string) admin_url());
        if ($adminBase !== '' && str_starts_with($url, $adminBase)) {
            return true;
        }

        $restBase = trailingslashit((string) rest_url());
        if ($restBase !== '' && str_starts_with($url, $restBase)) {
            return true;
        }

        $parts = wp_parse_url($url);
        if (!is_array($parts)) {
            return true;
        }

        $path = (string) ($parts['path'] ?? '');
        if ($path !== '' && preg_match('#/(wp-admin|wp-json)(/|$)#', $path)) {
            return true;
        }

        $query = [];
        parse_str((string) ($parts['query'] ?? ''), $query);
        if (!empty($query['preview']) || !empty($query['preview_id']) || !empty($query['rest_route'])) {
            return true;
        }

        return false;
    }
}
