<?php

if (!defined('DOKU_INC')) die();

use dokuwiki\Action\Exception\ActionDisabledException;

class action_plugin_extranet extends DokuWiki_Action_Plugin
{
    /** @var helper_plugin_extranet|null */
    private $helper = null;

    protected function getHelper(): ?helper_plugin_extranet
    {
        if ($this->helper === null) {
            $this->helper = plugin_load('helper', 'extranet');
        }
        return $this->helper;
    }

    public function register(Doku_Event_Handler $controller)
    {
        $controller->register_hook('DOKUWIKI_STARTED', 'AFTER', $this, 'injectJsInfo');
        $controller->register_hook('TPL_METAHEADER_OUTPUT', 'BEFORE', $this, 'addProsemirrorPolyfillAsset');
        $controller->register_hook('PROSEMIRROR_RENDER_PLUGIN', 'BEFORE', $this, 'handleRenderForProsemirror');
        $controller->register_hook('ACTION_ACT_PREPROCESS', 'AFTER', $this, 'syncTextFromProsemirrorState');
        $controller->register_hook('IO_WIKIPAGE_WRITE', 'BEFORE', $this, 'syncMacroFromProsemirrorStateBeforeWrite');
        $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'handleProsemirrorSwitchToText');

        $helper = $this->getHelper();
        if ($helper && $helper->isExtranetRequest()) {
            $controller->register_hook('ACTION_ACT_PREPROCESS', 'BEFORE', $this, 'blockConfiguredActions');
            $controller->register_hook('AUTH_LOGIN_CHECK', 'AFTER', $this, 'disableActions');
            $controller->register_hook('PARSER_CACHE_USE', 'BEFORE', $this, 'createExtranetCache');
            $controller->register_hook('IO_WIKIPAGE_READ', 'AFTER', $this, 'displayHideMessageIfRestricted');
            $controller->register_hook('FETCH_MEDIA_STATUS', 'BEFORE', $this, 'hideMediaIfRestricted');
        }
    }

    public function handleRenderForProsemirror(Doku_Event $event, $param): void
    {
        $data = $event->data;
        $name = strtolower(trim((string)($data['name'] ?? '')));
        if ($name !== 'extranet') return;

        $match = trim((string)($data['match'] ?? ''));
        if (!preg_match('/^~~\s*(NOEXTRANET|EXTRANET)\s*~~$/i', $match, $matches)) return;

        $renderer = $data['renderer'] ?? null;
        if (!is_object($renderer) || !isset($renderer->nodestack) || !method_exists($renderer->nodestack, 'getDocNode')) {
            return;
        }

        $macro = strtoupper($matches[1]);
        $docNode = $renderer->nodestack->getDocNode();
        if ($macro === 'NOEXTRANET') {
            $docNode->attr('noextranet', true);
            $docNode->attr('extranet', false);
        } else {
            $docNode->attr('extranet', true);
            $docNode->attr('noextranet', false);
        }

        $event->preventDefault();
        $event->stopPropagation();
    }

    public function addProsemirrorPolyfillAsset(Doku_Event $event, $param): void
    {
        global $ACT;

        if (!in_array((string)$ACT, ['edit', 'preview'], true)) return;
        if (defined('DOKUWIKI_PM_FILE_STATE_POLYFILL_INCLUDED')) return;
        if (empty($event->data) || !is_array($event->data)) return;

        define('DOKUWIKI_PM_FILE_STATE_POLYFILL_INCLUDED', 1);
        $path = DOKU_INC . 'lib/plugins/extranet/script/prosemirror_file_state_polyfill.js';
        $version = @filemtime($path) ?: time();

        $event->data['script'][] = [
            'type' => 'text/javascript',
            'src' => DOKU_BASE . 'lib/plugins/extranet/script/prosemirror_file_state_polyfill.js?v=' . rawurlencode((string)$version),
            '_data' => '',
            'defer' => 'defer',
        ];
    }

    public function handleProsemirrorSwitchToText(Doku_Event $event, $param): void
    {
        global $INPUT, $ID;

        if ($event->data !== 'plugin_prosemirror_switch_editors') return;
        if ($INPUT->bool('getJSON')) return;

        $json = (string)$INPUT->str('data');
        if ($json === '') return;

        $ID = $INPUT->str('id');

        /** @var helper_plugin_prosemirror $helper */
        $helper = plugin_load('helper', 'prosemirror');
        if (!$helper) return;

        try {
            $syntax = $helper->getSyntaxFromProsemirrorData($json);
        } catch (Throwable $e) {
            return;
        }

        $macroState = $this->extractMacroStateFromJson($json);
        if ($macroState === null) {
            $macroState = $this->extractMacroStateFromSyntax($syntax);
        }
        $syntax = $this->applyMacroStateToSyntax($syntax, $macroState);

        $event->preventDefault();
        $event->stopPropagation();
        header('Content-Type: application/json');
        echo json_encode(['text' => $syntax]);
    }

    public function syncTextFromProsemirrorState(Doku_Event $event, $param): void
    {
        global $INPUT, $TEXT;

        if ($INPUT->server->str('REQUEST_METHOD') !== 'POST') return;
        if (!in_array((string)$event->data, ['save', 'preview'], true)) return;
        if (!$INPUT->post->has('prosemirror_json')) return;
        if (!get_doku_pref('plugin_prosemirror_useWYSIWYG', false)) return;

        $macroState = $this->extractMacroStateFromJson((string)$INPUT->post->str('prosemirror_json'));
        if ($macroState === null) {
            $macroState = $this->extractMacroStateFromSyntax((string)$TEXT);
        }

        $TEXT = $this->applyMacroStateToSyntax((string)$TEXT, $macroState);
    }

    public function syncMacroFromProsemirrorStateBeforeWrite(Doku_Event $event, $param): void
    {
        global $INPUT;

        if (!$INPUT->post->has('prosemirror_json')) return;
        $macroState = $this->extractMacroStateFromJson((string)$INPUT->post->str('prosemirror_json'));

        $helper = $this->getHelper();
        $defaultPolicy = $helper ? $helper->getDefaultPolicy() : 'allow';
        if ($macroState === null && !in_array($defaultPolicy, ['force_allow', 'force_block'], true)) return;

        if (!isset($event->data[0]) || !is_array($event->data[0]) || !isset($event->data[0][1])) return;
        $event->data[0][1] = $this->applyMacroStateToSyntax((string)$event->data[0][1], $macroState);
    }

    protected function extractMacroStateFromJson(string $json): ?array
    {
        if ($json === '') return null;
        $data = json_decode($json, true);
        if (!is_array($data)) return null;
        $attrs = $data['attrs'] ?? null;
        if (!is_array($attrs)) return null;

        $hasNoExtranetAttr = array_key_exists('noextranet', $attrs);
        $hasExtranetAttr = array_key_exists('extranet', $attrs);
        if (!$hasNoExtranetAttr && !$hasExtranetAttr) return null;

        return [
            'noextranet' => $hasNoExtranetAttr ? (bool)$attrs['noextranet'] : false,
            'extranet' => $hasExtranetAttr ? (bool)$attrs['extranet'] : false,
        ];
    }

    protected function applyMacroStateToSyntax(string $content, ?array $macroState): string
    {
        $content = preg_replace('/^\h*~~\h*(NOEXTRANET|EXTRANET)\h*~~\h*$(\R)?/imu', '', $content);
        $content = rtrim((string)$content);

        $helper = $this->getHelper();
        $defaultPolicy = $helper ? $helper->getDefaultPolicy() : 'allow';

        if ($macroState === null || in_array($defaultPolicy, ['force_allow', 'force_block'], true)) {
            return $content !== '' ? $content . "\n" : '';
        }

        if (!empty($macroState['noextranet'])) {
            $content .= "\n\n~~NOEXTRANET~~\n";
        } elseif (!empty($macroState['extranet'])) {
            $content .= "\n\n~~EXTRANET~~\n";
        } elseif ($content !== '') {
            $content .= "\n";
        }

        return $content;
    }

    protected function extractMacroStateFromSyntax(string $content): ?array
    {
        $hasNoExtranet = (bool)preg_match('/~~\s*NOEXTRANET\s*~~/i', $content);
        $hasExtranet = (bool)preg_match('/~~\s*EXTRANET\s*~~/i', $content);

        if (!$hasNoExtranet && !$hasExtranet) {
            return null;
        }

        return [
            'noextranet' => $hasNoExtranet,
            'extranet' => $hasExtranet,
        ];
    }

    public function injectJsInfo(Doku_Event $event, $param): void
    {
        global $JSINFO;

        $helper = $this->getHelper();
        $mode = $helper ? $helper->getDefaultPolicy() : 'allow';

        $JSINFO['plugin_extranet_default_mode'] = $mode;
        $JSINFO['plugin_extranet_label_noextranet'] = (string)$this->getLang('label_noextranet');
        $JSINFO['plugin_extranet_label_extranet'] = (string)$this->getLang('label_extranet');
    }

    protected function isConfiguredActionDisabled(string $actionName): bool
    {
        $actionName = strtolower(trim($actionName));
        if ($actionName === '') return false;

        $helper = $this->getHelper();
        $actions = $helper ? $helper->parseRuleList($this->getConf('disable_actions')) : [];
        $actions = array_map(static function ($action) {
            return strtolower(trim((string)$action));
        }, $actions);

        return in_array($actionName, $actions, true);
    }

    protected function isRestrictedPageActionDisabled(string $actionName): bool
    {
        $actionName = strtolower(trim($actionName));
        if ($actionName === '') return false;

        $helper = $this->getHelper();
        $actions = $helper ? $helper->parseRuleList($this->getConf('restricted_disable_actions')) : [];
        $actions = array_map(static function ($action) {
            return strtolower(trim((string)$action));
        }, $actions);

        if (!in_array($actionName, $actions, true)) return false;

        global $ID;
        return $helper && !$helper->isPageAllowed((string)$ID);
    }

    public function blockConfiguredActions(Doku_Event $event, $param): void
    {
        $actionName = (string)$event->data;
        if ($this->isConfiguredActionDisabled($actionName) || $this->isRestrictedPageActionDisabled($actionName)) {
            throw new ActionDisabledException();
        }
    }

    protected function isContentRestrictedFromExtranet(string $content): bool
    {
        global $ID;
        $helper = $this->getHelper();
        if (!$helper) return false;
        return !$helper->isPageVisibleFromExtranet((string)$ID, $content);
    }

    protected function isMediaRestrictedFromExtranet(string $media): bool
    {
        $helper = $this->getHelper();
        if (!$helper) return false;
        return !$helper->isMediaAllowed($media);
    }

    public function disableActions(Doku_Event $event, $param): void
    {
        if (!empty($this->getConf('disable_actions'))) {
            global $conf;
            $conf['disableactions'] = (!empty($conf['disableactions']) ? $conf['disableactions'] . ',' : '') . $this->getConf('disable_actions');
        }
    }

    public function createExtranetCache(Doku_Event $event, $param): void
    {
        $cache = $event->data;
        $cache->key .= '#extranet';
        $cache->cache = getCacheName($cache->key, $cache->ext);
    }

    public function displayHideMessageIfRestricted(Doku_Event $event, $param): void
    {
        if (!$this->isContentRestrictedFromExtranet((string)$event->result)) return;

        $result = '';

        if ($this->getConf('preserve_first_title')) {
            $titlePattern = '/(?:^|\v)(={2,6}.+={2,})(?:\v|$)/';
            preg_match($titlePattern, $event->result, $matches);

            if (!empty($matches[0])) {
                $result .= $matches[0] . "\r\n";
            }
        }
        $result .= $this->getConf('message_prefix') . $this->getLang('hidden_message') . $this->getConf('message_suffix');

        $event->result = $result;
    }

    public function hideMediaIfRestricted(Doku_Event $event, $param): void
    {
        $hideFilesMode = $this->getHideFilesMode();
        if ($hideFilesMode === 'none') return;

        $mediaID = (string)($event->data['media'] ?? '');
        if ($mediaID === '') return;
        if ($hideFilesMode === 'except_pageicons' && $this->isPagesIconMedia($mediaID)) return;
        if (!$this->isMediaRestrictedFromExtranet($mediaID)) return;

        $event->data['file'] = dirname(__FILE__) . '/images/restricted.png';
        $event->data['orig'] = $event->data['file'];
        $event->data['status'] = 200;
        $event->data['statusmessage'] = 'OK';
        $event->data['mime'] = 'image/png';
        $event->data['download'] = false;
        $event->data['cache'] = false;
        $event->data['ispublic'] = false;
    }

    protected function getHideFilesMode(): string
    {
        $value = $this->getConf('hide_files');

        if ($value === true || $value === 1 || $value === '1') return 'all';
        if ($value === false || $value === 0 || $value === '0' || $value === '') return 'none';

        $value = strtolower(trim((string)$value));
        if (!in_array($value, ['all', 'except_pageicons', 'none'], true)) {
            return 'none';
        }

        return $value;
    }

    protected function isPagesIconMedia(string $mediaID): bool
    {
        $mediaID = cleanID($mediaID);
        if ($mediaID === '') return false;

        /** @var helper_plugin_pagesicon|null $helper */
        $helper = plugin_load('helper', 'pagesicon');
        if (!$helper || !method_exists($helper, 'isPageIconMedia')) return false;

        return (bool)$helper->isPageIconMedia($mediaID);
    }
}
