<?php

namespace Acms\Services\HtmlPurifier;

use HTMLPurifier;
use HTMLPurifier_Config;
use HTMLPurifier_HTMLDefinition;
use Acms\Services\Common\HookFactory;
use Acms\Services\Facades\LocalStorage;

class Helper
{
    /**
     * キャッシュディレクトリのパス
     *
     * @var string
     */
    protected $cacheDir = '';

    /**
     * HTMLPurifierのインスタンス
     *
     * @var HTMLPurifier
     */
    protected $htmlPurifier;

    public function __construct()
    {
        $this->cacheDir = CACHE_DIR . 'html-purifier/';
        LocalStorage::makeDirectory($this->cacheDir);

        $dangerousTags = configArray('dangerous_tags');
        $dangerousTags = $dangerousTags ? $dangerousTags : ['script', 'iframe'];
        $this->htmlPurifier = PurifierFactory::create(
            IframePolicy::fromConfig(),
            $dangerousTags,
            $this->cacheDir,
            $this->dispatchConfigHook(...),
            $this->dispatchDefinitionHook(...)
        );
    }

    /**
     * HTMLPurifier の config 拡張フックを発火する。
     *
     * フック発火（グローバルな HookFactory へのアクセス）を本クラスの private に閉じ、
     * {@see PurifierFactory} 側はコールバックを受け取るだけにすることで、Factory の単体テスト可能性を保つ。
     * Hook 無効時（HOOK_ENABLE が未定義 / false）は何もしない。
     *
     * @param HTMLPurifier_Config $config カスタム HTML 定義の取得前の config
     * @return void
     */
    private function dispatchConfigHook(HTMLPurifier_Config $config): void
    {
        if (!defined('HOOK_ENABLE') || !HOOK_ENABLE) {
            return;
        }
        HookFactory::singleton()->call('extendsHtmlPurifierConfig', [$config]);
    }

    /**
     * HTMLPurifier のカスタム HTML 定義拡張フックを発火する。
     *
     * @param HTMLPurifier_HTMLDefinition $def カスタム HTML 定義（addAttribute / addElement で拡張する）
     * @param HTMLPurifier_Config $config 取得済みの config
     * @return void
     */
    private function dispatchDefinitionHook(HTMLPurifier_HTMLDefinition $def, HTMLPurifier_Config $config): void
    {
        if (!defined('HOOK_ENABLE') || !HOOK_ENABLE) {
            return;
        }
        HookFactory::singleton()->call('extendsHtmlPurifierDefinition', [$def, $config]);
    }

    /**
     * HTMLをクリーンアップする
     *
     * @param string|array $html
     * @return string
     */
    public function clean(string|array $html): string
    {
        if (is_array($html)) {
            $html = implode($html);
        }
        if (!$html) {
            return strval($html);
        }
        $html = $this->codeEscape($html); // コード表示のエスケープ
        $unescaped = html_entity_decode($html, ENT_QUOTES | ENT_HTML5, 'UTF-8');

        return $this->htmlPurifier->purify($unescaped);
    }

    /**
     * HTMLPurifierのキャッシュをクリアする
     *
     * @return void
     */
    public function clearCache(): void
    {
        LocalStorage::removeDirectory($this->cacheDir);
    }

    /**
     * HTMLの<pre><code>内のコードをエスケープする
     *
     * @param string $html
     * @return string
     */
    protected function codeEscape(string $html): string
    {
        $html = preg_replace_callback(
            '#<pre><code>(.*?)</code></pre>#is',
            function ($matches) {
                // HTMLをエスケープ
                $escaped = htmlspecialchars($matches[1], ENT_QUOTES | ENT_HTML5, 'UTF-8');
                $escaped = str_replace(['{', '}'], ['&#123;', '&#125;'], $escaped);
                return "<pre><code>{$escaped}</code></pre>";
            },
            $html
        );
        if ($html) {
            // インライン code の中身をエスケープ（ただし pre > code には二重でかからないように）
            $html = preg_replace_callback(
                '#(?<!<pre>)<code>(.*?)</code>#is',
                function ($matches) {
                    $escaped = htmlspecialchars($matches[1], ENT_QUOTES | ENT_HTML5, 'UTF-8');
                    $escaped = str_replace(['{', '}'], ['&#123;', '&#125;'], $escaped);
                    return "<code>{$escaped}</code>";
                },
                $html
            );
        }
        return $html ?? '';
    }
}
