<?php
namespace App\Repositories;

use App\Exceptions\ClientNotAllowedException;
use App\Models\AgentAllow;
use App\Models\AgentDeny;
use Illuminate\Support\Collection;
use Nexus\Database\NexusDB;

class AgentAllowRepository extends BaseRepository
{
    public function getList(array $params)
    {
        $query = AgentAllow::query();
        if (!empty($params['family'])) {
            $query->where('family', 'like', "%{$params['family']}%");
        }
        list($sortField, $sortType) = $this->getSortFieldAndType($params);
        $query->orderBy($sortField, $sortType);
        return $query->paginate();
    }

    public function store(array $params)
    {
        $this->getPatternMatches($params['peer_id_pattern'], $params['peer_id_start'], $params['peer_id_match_num']);
        $this->getPatternMatches($params['agent_pattern'], $params['agent_start'], $params['agent_match_num']);
        $model = AgentAllow::query()->create($params);
        return $model;
    }

    public function update(array $params, $id)
    {
        $this->getPatternMatches($params['peer_id_pattern'], $params['peer_id_start'], $params['peer_id_match_num']);
        $this->getPatternMatches($params['agent_pattern'], $params['agent_start'], $params['agent_match_num']);
        $model = AgentAllow::query()->findOrFail($id);
        $model->update($params);
        return $model;
    }

    public function getDetail($id)
    {
        $model = AgentAllow::query()->findOrFail($id);
        return $model;
    }

    public function delete($id)
    {
        $model = AgentAllow::query()->findOrFail($id);
        $model->denies()->delete();
        $result = $model->delete();
        return $result;
    }

    public function getPatternMatches($pattern, $start, $matchNum)
    {
        if (!preg_match($pattern, $start, $matches)) {
            throw new ClientNotAllowedException(sprintf('pattern: %s can not match start: %s', $pattern, $start));
        }
        $matchCount = count($matches) - 1;
        //due to old data may be matchNum > matchCount
//        if ($matchNum > $matchCount && !IN_NEXUS) {
//            throw new ClientNotAllowedException("pattern: $pattern match start: $start got matches count: $matchCount, but require $matchNum.");
//        }
        return array_slice($matches, 1, $matchNum);
    }

    /**
     * @param $peerId
     * @param $agent
     * @param false $debug
     * @return \App\Models\NexusModel|mixed
     * @throws ClientNotAllowedException
     */
    public function checkClient($peerId, $agent, $debug = false)
    {
        //check from high version to low version, if high version allow, stop!
        $cacheKey = nexus_env("CACHE_KEY_AGENT_ALLOW", "all_agent_allows") . ":php";
        $allows = NexusDB::remember($cacheKey, 3600, function () {
            return AgentAllow::query()
                ->orderBy('peer_id_start', 'desc')
                ->orderBy('agent_start', 'desc')
                ->get();
        });
        $agentAllowPassed = null;
        $versionTooLowStr = '';
        foreach ($allows as $agentAllow) {
            $agentAllowId = $agentAllow->id;
            $logPrefix = "[ID: $agentAllowId]";
            $isPeerIdAllowed = $isAgentAllowed = $isPeerIdTooLow = $isAgentTooLow = false;
            //check peer_id, when handle scrape request, no peer_id, so let it pass
            if ($agentAllow->peer_id_pattern == '' || $peerId === null) {
                $isPeerIdAllowed = true;
            } else {
                $pattern = $agentAllow->peer_id_pattern;
                $start = $agentAllow->peer_id_start;
                $matchType = $agentAllow->peer_id_matchtype;
                $matchNum = $agentAllow->peer_id_match_num;
                try {
                    $peerIdResult = $this->isAllowed($pattern, $start, $matchNum, $matchType, $peerId, $debug, $logPrefix);
                    if ($debug) {
                        do_log(
                            "$logPrefix, peerIdResult: $peerIdResult, with parameters: "
                            . nexus_json_encode(compact('pattern', 'start', 'matchNum', 'matchType', 'peerId'))
                        );
                    }
                } catch (\Exception $exception) {
                    do_log("$logPrefix, check peer_id error: " . $exception->getMessage(), 'error');
                    throw new ClientNotAllowedException("regular expression err for peer_id: " . $start . ", please ask sysop to fix this");
                }
                if ($peerIdResult == 1) {
                    $isPeerIdAllowed = true;
                }
                if ($peerIdResult == 2) {
                    $isPeerIdTooLow = true;
                }
            }

            //check agent
            if ($agentAllow->agent_pattern == '') {
                $isAgentAllowed = true;
            } else {
                $pattern = $agentAllow->agent_pattern;
                $start = $agentAllow->agent_start;
                $matchType = $agentAllow->agent_matchtype;
                $matchNum = $agentAllow->agent_match_num;
                try {
                    $agentResult = $this->isAllowed($pattern, $start, $matchNum, $matchType, $agent, $debug, $logPrefix);
                    if ($debug) {
                        do_log(
                            "$logPrefix, agentResult: $agentResult, with parameters: "
                            . nexus_json_encode(compact('pattern', 'start', 'matchNum', 'matchType', 'agent'))
                        );
                    }
                } catch (\Exception $exception) {
                    do_log("$logPrefix, check agent error: " . $exception->getMessage(), 'error');
                    throw new ClientNotAllowedException("regular expression err for agent: " . $start . ", please ask sysop to fix this");
                }
                if ($agentResult == 1) {
                    $isAgentAllowed = true;
                }
                if ($agentResult == 2) {
                    $isAgentTooLow = true;
                }
            }

            //both OK, passed, client is allowed
            if ($isPeerIdAllowed && $isAgentAllowed) {
                $agentAllowPassed = $agentAllow;
                break;
            }
            if ($isPeerIdTooLow && $isAgentTooLow) {
                $versionTooLowStr = "Your " . $agentAllow->family . " 's version is too low, please update it after " . $agentAllow->start_name;
            }
        }

        if ($versionTooLowStr) {
            throw new ClientNotAllowedException($versionTooLowStr);
        }

        if (!$agentAllowPassed) {
            throw new ClientNotAllowedException("Banned Client, Please goto " . getSchemeAndHttpHost() . "/faq.php#id29 for a list of acceptable clients");
        }

        if ($debug) {
            do_log("agentAllowPassed: " . $agentAllowPassed->toJson());
        }

        // check if exclude
        if ($agentAllowPassed->exception == 'yes') {
            $agentDeny = $this->checkIsDenied($peerId, $agent, $agentAllowPassed->id);
            if ($agentDeny) {
                if ($debug) {
                    do_log("agentDeny: " . $agentDeny->toJson());
                }
                throw new ClientNotAllowedException(sprintf(
                    "[%s-%s]Client: %s is banned due to: %s",
                    $agentAllowPassed->id, $agentDeny->id, $agentDeny->name, $agentDeny->comment
                ));
            }
        }
        if (isHttps() && $agentAllowPassed->allowhttps != 'yes') {
            throw new ClientNotAllowedException(sprintf(
                "[%s]This client does not support https well, Please goto %s/faq.php#id29 for a list of proper clients",
                $agentAllowPassed->id, getSchemeAndHttpHost()
            ));
        }

        return $agentAllowPassed;

    }

    private function checkIsDenied($peerId, $agent, $familyId)
    {
        $cacheKey = nexus_env("CACHE_KEY_AGENT_DENY", "all_agent_denies") . ":php";
        /** @var Collection $allDenies */
        $allDenies = NexusDB::remember($cacheKey, 3600, function () {
            return AgentDeny::query()->get()->groupBy('family_id');
        });
        $agentDenies = $allDenies->get($familyId, []);
        foreach ($agentDenies as $agentDeny) {
            if ($agentDeny->agent == $agent && preg_match("/^" . $agentDeny->peer_id . "/", $peerId)) {
                return $agentDeny;
            }
        }
    }

    /**
     * check peer_id or agent is allowed
     *
     * 0: not allowed
     * 1: allowed
     * 2: version too low
     *
     * @param $pattern
     * @param $start
     * @param $matchNum
     * @param $matchType
     * @param $value
     * @param bool $debug
     * @param string $logPrefix
     * @return int
     * @throws ClientNotAllowedException
     */
    private function isAllowed($pattern, $start, $matchNum, $matchType, $value, $debug = false, $logPrefix = ''): int
    {
        $matchBench = $this->getPatternMatches($pattern, $start, $matchNum);
        if ($debug) {
            do_log("$logPrefix, matchBench: " . nexus_json_encode($matchBench));
        }
        if (!preg_match($pattern, $value, $matchTarget)) {
            if ($debug) {
                do_log(sprintf("$logPrefix, pattern: (%s) not match: (%s)", $pattern, $value));
            }
            return 0;
        }
        if ($matchNum <= 0) {
            return 1;
        }
        $matchTarget = array_slice($matchTarget, 1);
        if ($debug) {
            do_log("$logPrefix, matchTarget: " . nexus_json_encode($matchTarget));
        }
        for ($i = 0; $i < $matchNum; $i++) {
            if (!isset($matchBench[$i]) || !isset($matchTarget[$i])) {
                break;
            }
            if ($matchType == 'dec') {
                $matchBench[$i] = intval($matchBench[$i]);
                $matchTarget[$i] = intval($matchTarget[$i]);
            } elseif ($matchType == 'hex') {
                $matchBench[$i] = hexdec($matchBench[$i]);
                $matchTarget[$i] = hexdec($matchTarget[$i]);
            } else {
                throw new ClientNotAllowedException(sprintf("Invalid match type: %s", $matchType));
            }
            if ($matchTarget[$i] > $matchBench[$i]) {
                //higher, pass directly
                return 1;
            } elseif ($matchTarget[$i] < $matchBench[$i]) {
                return 2;
            }
        }

        //NOTE: at last, after all position checked, not [NOT_MATCH] or lower, it is passed!
        return 1;

    }

    public function checkClientSimple($peerId, $agent, $debug = false)
    {
        //check from high version to low version, if high version allow, stop!
        $cacheKey = nexus_env("CACHE_KEY_AGENT_ALLOW", "all_agent_allows") . ":php";
        $allows = NexusDB::remember($cacheKey, 3600, function () {
            return AgentAllow::query()
                ->orderBy('peer_id_start', 'desc')
                ->orderBy('agent_start', 'desc')
                ->get();
        });
        $agentAllowPassed = null;
        foreach ($allows as $agentAllow) {
            $agentAllowId = $agentAllow->id;
            $agentAllowLogPrefix = "[ID: $agentAllowId], peerId: $peerId";
            $pattern = $agentAllow->peer_id_pattern;
            //check peer_id, when handle scrape request, no peer_id, so let it pass
            $isPeerIdAllowed = empty($pattern) || preg_match($pattern, $peerId);
            $agentAllowLogPrefix .= ", peer_id pattern: $pattern, isPeerIdAllowed: $isPeerIdAllowed";

            //check agent, agent must have both announce + scrape
            $pattern = $agentAllow->agent_pattern;
            $isAgentAllowed = !empty($pattern) && preg_match($pattern, $agent);
            $agentAllowLogPrefix .= ", agent pattern: $pattern, isAgentAllowed: $isAgentAllowed";

            //both OK, passed, client is allowed
            if ($isPeerIdAllowed && $isAgentAllowed) {
                $agentAllowPassed = $agentAllow;
                do_log("$agentAllowLogPrefix, PASSED", 'debug');
                break;
            }
            if ($debug) {
                do_log("$agentAllowLogPrefix, NOT PASSED", 'debug');
            }
        }

        if (!$agentAllowPassed) {
            throw new ClientNotAllowedException("Banned Client, Please goto " . getSchemeAndHttpHost() . "/faq.php#id29 for a list of acceptable clients");
        }

        if ($debug) {
            do_log("agentAllowPassed: " . $agentAllowPassed->toJson(), 'debug');
        }

        // check if exclude
        if ($agentAllowPassed->exception == 'yes') {
            $agentDeny = $this->checkIsDenied($peerId, $agent, $agentAllowPassed->id);
            if ($agentDeny) {
                if ($debug) {
                    do_log("agentDeny: " . $agentDeny->toJson());
                }
                throw new ClientNotAllowedException(sprintf(
                    "[%s-%s]Client: %s is banned due to: %s",
                    $agentAllowPassed->id, $agentDeny->id, $agentDeny->name, $agentDeny->comment
                ));
            }
        }
        if (isHttps() && $agentAllowPassed->allowhttps != 'yes') {
            throw new ClientNotAllowedException(sprintf(
                "[%s]This client does not support https well, Please goto %s/faq.php#id29 for a list of proper clients",
                $agentAllowPassed->id, getSchemeAndHttpHost()
            ));
        }

        return $agentAllowPassed;

    }

}
