<?php

/*
 * Copyright © 2020 Miguel Nogueira
 *
 *   This file is part of Raspberry Staff Manager.
 *
 *     Raspberry Staff Manager is free software: you can redistribute it and/or modify
 *     it under the terms of the GNU General Public License as published by
 *     the Free Software Foundation, either version 3 of the License, or
 *     (at your option) any later version.
 *
 *     Raspberry Staff Manager is distributed in the hope that it will be useful,
 *     but WITHOUT ANY WARRANTY; without even the implied warranty of
 *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *     GNU General Public License for more details.
 *
 *     You should have received a copy of the GNU General Public License
 *     along with Raspberry Staff Manager.  If not, see <https://www.gnu.org/licenses/>.
 */

namespace App\Helpers;

use App\Exceptions\AccountNotLinkedException;
use App\User;
use Carbon\Carbon;
use Illuminate\Http\Client\RequestException;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;

// Small wrapper for the necessary sections of the Discord API; A library is overkill here
class Discord
{
    /**
     * The current working guild. Default is Home guild from app config
     *
     * @var string
     */
    protected string $workingGuild;

    /**
     * Current user.
     *
     * @var User The user all methods will affect.
     */
    protected User $user;

    public function __construct()
    {
        if (isset($this->workingGuild)) {
            $this->setWorkingGuild(config('services.discord.home_guild'));
        }
    }

    /**
     * Sets the working guild
     *
     * @param  string  $workingGuild
     * @return Discord
     */
    public function setWorkingGuild(string $workingGuild): Discord
    {
        $this->workingGuild = $workingGuild;

        return $this;
    }

    /**
     * Sets the current user, upon validation
     *
     * @param  User  $user
     * @return Discord
     *
     * @throws AccountNotLinkedException
     */
    public function setUser(User $user): Discord
    {
        if ($user->hasDiscordConnection()) {
            $this->user = $user;

            return $this;
        }

        throw new AccountNotLinkedException('Specified website user has not linked their Discord account yet.');
    }

    /**
     * Obtains the current user's authentication info. Caches the data for the access_token's TTL, thus
     * preventing unnecessary API requests.
     *
     * @return object Current user's authentication info (has no sensitive fields)
     *
     * @throws RequestException
     */
    public function getAuthorizationInfo(): object
    {
        if (Cache::has($this->user->discord_user_id)) {
            return unserialize(Cache::get($this->user->discord_user_id));
        } else {
            $authInfo = (object) Http::withToken($this->user->discord_token)
                ->get(config('services.discord.base_url').'/oauth2/@me')
                ->throw()
                ->json();

            Cache::put($this->user->discord_user_id, serialize($authInfo), Carbon::parse($authInfo->expires));

            return $authInfo;
        }
    }

    /**
     * Checks if the user's token is close to expiring.
     * Tokens should be refreshed the day before they expire.
     *
     * @return bool Whether the user's token needs to be refreshed
     *
     * @throws RequestException
     */
    public function needsRefresh(): bool
    {
        return Carbon::parse($this->getAuthorizationInfo()->expires)->diffInDays() == 1;
    }

    public function exchangeRefreshToken()
    {
    }

    /**
     * Adds current working user to current working guild. Bot must be member of target guild, and account must be linked
     *
     * @return object|bool A GuildMember object; false if member is already in guild
     *
     * @throws RequestException Any client and server errors
     *
     * @see https://discord.com/developers/docs/resources/guild#guild-member-object
     */
    public function addGuildMember(): object|bool
    {
        $params = [
            'access_token' => $this->user->discord_token,
        ];

        $member = Http::withBody(json_encode($params), 'application/json')
            ->withHeaders([
                'Authorization' => 'Bot '.config('services.discord.token'),
            ])->put(config('services.discord.base_url')."/guilds/{$this->workingGuild}/members/{$this->user->discord_user_id}")
            ->throw();

        if ($member->successful() && $member->status() == 204) {
            return false;
        } else {
            return (object) $member->json();
        }
    }

    /**
     * Bans a specified user from the guild.
     * May be called from the suspension service optionally by the banning user
     *
     * @param  string  $reason The reason to supply Discord with
     * @return void Nothing on success
     *
     * @throws RequestException
     * @throws AccountNotLinkedException
     */
    public function addGuildBan(string $reason): void
    {
        Http::withHeaders([
            'Authorization' => 'Bot '.config('services.discord.token'),
            'X-Audit-Log-Reason' => $reason,
        ])->put(config('services.discord.base_url')."/guilds/{$this->workingGuild}/bans/{$this->user->discord_user_id}")
            ->throw();

        throw new AccountNotLinkedException('Specified website user has not linked their Discord account yet.');
    }

    /**
     * @param  string  $reason The removal reason to provide Discord with (e.g. ban expired)
     * @return null|bool Null on unnan, false if user is not banned
     *
     * @throws RequestException
     */
    public function removeGuildBan(string $reason): null|bool
    {
        if ($this->getGuildBan($this->user)) {
            Http::withHeaders([
                'Authorization' => 'Bot '.config('services.discord.token'),
                'X-Audit-Log-Reason' => $reason,
            ])->delete(config('services.discord.base_url')."/guilds/{$this->workingGuild}/bans/{$this->user->discord_user_id}")
                ->throw();

            return null;
        }

        return false;
    }

    /**
     * Gets (possible) ban for current user.
     *
     * @return object|bool Ban object if user is banned. Null
     *
     * @throws RequestException
     *
     * @see https://discord.com/developers/docs/resources/guild#ban-object
     */
    public function getGuildBan(): object|bool
    {
        $ban = Http::withHeaders([
            'Authorization' => 'Bot '.config('services.discord.token'),
        ])->get(config('services.discord.base_url')."/guilds/{$this->workingGuild}/bans/{$this->user->discord_user_id}");

        if ($ban->status() == 404) {
            return false;
        }

        return ($ban->successful()) ? (object) $ban->json() : $ban->throwIf($ban->status() !== 404);
    }

    /**
     * Retrieves list of Role objects
     *
     * @see https://discord.com/developers/docs/topics/permissions#role-object
     *
     * @return array List of role objects
     *
     * @throws RequestException
     */
    public function getGuildRoles(): array
    {
        return Http::withHeaders([
            'Authorization' => 'Bot '.config('services.discord.token'),
        ])->get(config('services.discord.base_url')."/guilds/{$this->workingGuild}/roles")
            ->throw()
            ->json();
    }
}
