<?php

declare(strict_types=1);

namespace CliqThemes\StoreLocator\Frontend;

defined('ABSPATH') || exit;

/**
 * The markup this plugin is allowed to hand back to WordPress.
 *
 * A shortcode, a block render callback and a `the_content` filter all RETURN
 * markup, so there is no echo for an escaping function to wrap. Every value is
 * already escaped where it is interpolated, but that is a property a reader has
 * to verify by tracing; running the finished markup through an allowlist makes
 * it a property of the output itself.
 *
 * The list is the vocabulary the renderers actually emit. Notably absent:
 * `style` (settings become generated stylesheet rules, never style attributes),
 * `script`, `iframe`, `object`, `embed`, `form` action/method, and every `on*`
 * handler — kses drops those regardless.
 */
final class SafeHtml
{
    /**
     * Filter returned markup down to the allowlist.
     *
     * @param string $html markup produced by this plugin's renderers
     */
    public static function filter(string $html): string
    {
        if ($html === '') {
            return '';
        }

        return wp_kses($html, self::allowed());
    }

    /**
     * ARIA attributes, enumerated because kses has no prefix match for them.
     *
     * @return array<string, bool>
     */
    private static function ariaAttributes(): array
    {
        $names = [
            'activedescendant', 'atomic', 'autocomplete', 'busy', 'checked', 'colcount',
            'colindex', 'colspan', 'controls', 'current', 'describedby', 'details',
            'disabled', 'errormessage', 'expanded', 'flowto', 'haspopup', 'hidden',
            'invalid', 'keyshortcuts', 'label', 'labelledby', 'level', 'live', 'modal',
            'multiline', 'multiselectable', 'orientation', 'owns', 'placeholder',
            'posinset', 'pressed', 'readonly', 'relevant', 'required', 'roledescription',
            'rowcount', 'rowindex', 'rowspan', 'selected', 'setsize', 'sort',
            'valuemax', 'valuemin', 'valuenow', 'valuetext',
        ];

        $attributes = [];
        foreach ($names as $name) {
            $attributes['aria-' . $name] = true;
        }

        return $attributes;
    }

    /**
     * @return array<string, array<string, bool>>
     */
    private static function allowed(): array
    {
        // Attributes every element may carry. kses matches `data-*` as a prefix,
        // which is how the frontend app finds its mount points. It has no such
        // wildcard for ARIA, so those are listed one by one below — leaving them
        // out silently strips them, which is an accessibility regression rather
        // than a visible break.
        $common = [
            'class' => true,
            'id' => true,
            'role' => true,
            'tabindex' => true,
            'title' => true,
            'lang' => true,
            'dir' => true,
            'hidden' => true,
            'data-*' => true,
        ] + self::ariaAttributes();

        $svgShape = $common + [
            'fill' => true, 'stroke' => true, 'stroke-width' => true,
            'stroke-linecap' => true, 'stroke-linejoin' => true, 'stroke-dasharray' => true,
            'opacity' => true, 'transform' => true,
            'x' => true, 'y' => true, 'x1' => true, 'y1' => true, 'x2' => true, 'y2' => true,
            'cx' => true, 'cy' => true, 'r' => true, 'rx' => true, 'ry' => true,
            'd' => true, 'points' => true, 'width' => true, 'height' => true,
        ];

        $tags = [];

        // Structure and text.
        foreach (
            [
            'div', 'span', 'p', 'section', 'article', 'aside', 'header', 'footer',
            'nav', 'main', 'figure', 'figcaption', 'ul', 'ol', 'li', 'dl', 'dt', 'dd',
            'strong', 'em', 'b', 'i', 'u', 's', 'small', 'sup', 'sub', 'abbr', 'mark',
            'br', 'hr', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'code', 'pre',
            'address', 'time', 'label', 'fieldset', 'legend', 'template', 'noscript',
            ] as $tag
        ) {
            $tags[$tag] = $common;
        }
        $tags['time'] += ['datetime' => true];
        $tags['label'] += ['for' => true];

        // Tables.
        foreach (['table', 'thead', 'tbody', 'tfoot', 'tr', 'caption', 'colgroup', 'col'] as $tag) {
            $tags[$tag] = $common;
        }
        $tags['th'] = $common + ['colspan' => true, 'rowspan' => true, 'scope' => true];
        $tags['td'] = $common + ['colspan' => true, 'rowspan' => true];

        // Links and media. kses already restricts href/src to safe protocols.
        $tags['a'] = $common + ['href' => true, 'target' => true, 'rel' => true, 'download' => true];
        $tags['img'] = $common + [
            'src' => true, 'srcset' => true, 'sizes' => true, 'alt' => true,
            'width' => true, 'height' => true, 'loading' => true, 'decoding' => true,
        ];
        $tags['picture'] = $common;
        $tags['source'] = $common + ['srcset' => true, 'sizes' => true, 'type' => true, 'media' => true];

        // Controls the search form and result actions render.
        $tags['button'] = $common + ['type' => true, 'value' => true, 'name' => true, 'disabled' => true];
        $tags['form'] = $common + ['method' => true, 'role' => true];
        $tags['input'] = $common + [
            'type' => true, 'name' => true, 'value' => true, 'placeholder' => true,
            'checked' => true, 'disabled' => true, 'readonly' => true, 'required' => true,
            'min' => true, 'max' => true, 'step' => true, 'autocomplete' => true,
            'inputmode' => true, 'maxlength' => true, 'list' => true,
        ];
        $tags['select'] = $common + ['name' => true, 'multiple' => true, 'disabled' => true, 'required' => true];
        $tags['option'] = $common + ['value' => true, 'selected' => true, 'disabled' => true];
        $tags['optgroup'] = $common + ['label' => true, 'disabled' => true];
        $tags['textarea'] = $common + ['name' => true, 'rows' => true, 'cols' => true, 'placeholder' => true];
        $tags['datalist'] = $common;

        // Inline icons.
        $tags['svg'] = $svgShape + ['xmlns' => true, 'viewbox' => true, 'viewBox' => true, 'preserveaspectratio' => true];
        foreach (['path', 'circle', 'rect', 'line', 'polyline', 'polygon', 'ellipse', 'g', 'defs', 'use', 'title', 'desc'] as $tag) {
            $tags[$tag] = $svgShape;
        }

        /**
         * Filter the markup vocabulary the plugin may return.
         *
         * A feature that renders an element this list does not carry adds it
         * here, keyed by tag name with its permitted attributes.
         *
         * @param array<string, array<string, bool>> $tags
         */
        return apply_filters('cliq_sl_allowed_html', $tags);
    }
}
