<?php
if (!defined('DOKU_INC')) die();

class helper_plugin_extranet extends DokuWiki_Plugin
{
    private function getConfiguredString(array $keys, string $fallback = ''): string
    {
        foreach ($keys as $key) {
            $value = trim((string)$this->getConf($key));
            if ($value !== '') return $value;
        }

        return $fallback;
    }

    public function getDefaultPolicy(): string
    {
        $mode = strtolower(trim((string)$this->getConf('default_policy')));
        if (in_array($mode, ['allow', 'block', 'force_allow', 'force_block'], true)) {
            return $mode;
        }
        return 'allow';
    }

    private function hasMacro(string $content, string $macro): bool
    {
        return (bool)preg_match('/~~\s*' . preg_quote($macro, '/') . '\s*~~/i', $content);
    }

    public function parseRuleList($raw): array
    {
        $parts = preg_split('/[\r\n,;]+/', (string)$raw);
        $rules = [];
        foreach ($parts as $part) {
            $rule = trim((string)$part);
            if ($rule !== '') $rules[] = $rule;
        }
        return $rules;
    }

    public function idMatchesRule(string $id, string $rule): bool
    {
        $rule = trim($rule);
        if ($rule === '') return false;

        if (strlen($rule) > 1 && $rule[0] === '/' && substr($rule, -1) === '/') {
            return (bool)@preg_match($rule, $id);
        }

        if (substr($rule, -1) === ':') {
            return strpos($id, $rule) === 0;
        }

        if (strpos($rule, '*') !== false) {
            $regex = '/^' . str_replace('\\*', '.*', preg_quote($rule, '/')) . '$/';
            return (bool)preg_match($regex, $id);
        }

        return $id === $rule;
    }

    public function matchesConfiguredList(string $id, string $confKey): bool
    {
        $rules = $this->parseRuleList($this->getConf($confKey));
        foreach ($rules as $rule) {
            if ($this->idMatchesRule($id, $rule)) return true;
        }
        return false;
    }

    public function hasConfiguredList(string $confKey): bool
    {
        return !empty($this->parseRuleList($this->getConf($confKey)));
    }

    private function hasFilter(): bool
    {
        if ($this->hasConfiguredList('filter_list')) return true;
        return trim((string)$this->getConf('filter_regex')) !== '';
    }

    private function isInFilter(string $id): bool
    {
        // filter_list: exact pages, namespace prefixes and wildcards only — no regex entries
        foreach ($this->parseRuleList($this->getConf('filter_list')) as $rule) {
            $isRegex = strlen($rule) > 1 && $rule[0] === '/' && substr($rule, -1) === '/';
            if (!$isRegex && $this->idMatchesRule($id, $rule)) return true;
        }

        // filter_regex: dedicated regex field
        $regex = trim((string)$this->getConf('filter_regex'));
        if ($regex !== '' && @preg_match($regex, $id)) return true;

        return false;
    }

    public function getRequestMatchKey(): string
    {
        return $this->getConfiguredString(['request_match_key', 'server_ip_key'], 'REMOTE_ADDR');
    }

    public function getExtranetMatchList(): string
    {
        return $this->getConfiguredString(['extranet_match_list', 'extranet_ip_list']);
    }

    public function getExtranetMatchRegex(): string
    {
        return $this->getConfiguredString(['extranet_match_regex', 'extranet_ip_regex']);
    }

    /**
     * Build candidate values from the configured request marker.
     *
     * A proxy header may contain a single marker ("extranet"), a hostname,
     * or a comma-separated list such as X-Forwarded-For. We accept both the
     * full raw value and each comma-separated token.
     *
     * @return string[]
     */
    private function getRequestMatchCandidates(): array
    {
        $requestKey = $this->getRequestMatchKey();
        $rawValue = trim((string)($_SERVER[$requestKey] ?? ''));
        if ($rawValue === '') return [];

        $candidates = [$rawValue];
        if (strpos($rawValue, ',') !== false) {
            foreach (explode(',', $rawValue) as $part) {
                $part = trim((string)$part);
                if ($part !== '') $candidates[] = $part;
            }
        }

        return array_values(array_unique($candidates));
    }

    public function isExtranetRequest(): bool
    {
        $candidates = $this->getRequestMatchCandidates();
        if ($candidates === []) return false;

        $matchList = $this->getExtranetMatchList();
        if ($matchList !== '') {
            foreach (explode(',', $matchList) as $configuredValue) {
                $configuredValue = trim((string)$configuredValue);
                if ($configuredValue !== '' && in_array($configuredValue, $candidates, true)) {
                    return true;
                }
            }
        }

        $matchRegex = $this->getExtranetMatchRegex();
        if ($matchRegex !== '') {
            foreach ($candidates as $candidate) {
                if ((bool)@preg_match($matchRegex, $candidate)) return true;
            }
        }

        return false;
    }

    public function isClientFromExtranet(): bool
    {
        return $this->isExtranetRequest();
    }

    public function isMediaVisibleFromExtranet(string $mediaID): bool
    {
        if (!$this->hasFilter()) return true;

        $inFilter = $this->isInFilter($mediaID);
        $defaultPolicy = $this->getDefaultPolicy();

        if ($defaultPolicy === 'force_allow' || $defaultPolicy === 'allow') {
            return !$inFilter;
        }

        return $inFilter;
    }

    public function isPageVisibleFromExtranet(string $id, ?string $content = null): bool
    {
        if ($content === null) {
            $content = rawWiki($id);
        }
        $content = (string)$content;

        $hasFilter = $this->hasFilter();
        $inFilter  = $hasFilter && $this->isInFilter($id);

        $defaultPolicy = $this->getDefaultPolicy();

        if ($defaultPolicy === 'force_allow') {
            return !$inFilter;
        }

        if ($defaultPolicy === 'force_block') {
            return $inFilter;
        }

        $hasExtranet   = $this->hasMacro($content, 'EXTRANET');
        $hasNoExtranet = $this->hasMacro($content, 'NOEXTRANET');

        if ($defaultPolicy === 'allow') {
            return !$inFilter && !$hasNoExtranet;
        }

        // block
        return $inFilter || $hasExtranet;
    }

    public function isPageAllowed(string $id, ?string $content = null): bool
    {
        if (!$this->isExtranetRequest()) return true;
        return $this->isPageVisibleFromExtranet($id, $content);
    }

    public function isMediaAllowed(string $mediaID): bool
    {
        if (!$this->isExtranetRequest()) return true;
        return $this->isMediaVisibleFromExtranet($mediaID);
    }
}
