<?php

namespace LaraBB\Tracker\Actions;

use Illuminate\Database\Eloquent\Collection;
use LaraBB\Tracker\Traits\SetCurrentRequestTrait;
use Exception;
use JetBrains\PhpStorm\ArrayShape;
use LaraBB\Peer\Models\Peer;
use LaraBB\Peer\Tasks\DestroyTask as DestroyPeerTask;
use LaraBB\Peer\Tasks\FindTask as FindPeerTask;
use LaraBB\Peer\Tasks\StoreTask as StorePeerTask;
use LaraBB\Peer\Tasks\UpdateTask as UpdatePeerTask;
use LaraBB\Profile\Tasks\UpdateTask as UpdateProfileTask;
use LaraBB\Torrent\Models\Torrent;
use LaraBB\Torrent\Tasks\FindTask as FindTorrentTask;
use LaraBB\Torrent\Tasks\UpdateTask as UpdateTorrentTask;
use LaraBB\Tracker\UI\Web\Requests\Announce;
use LaraBB\User\Models\User;
use LaraBB\User\Tasks\FindTask as FindUserTask;
use Log;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Rhilip\Bencode\Bencode;
use stdClass;

/**
 *
 */
class AnnounceAction
{
    use SetCurrentRequestTrait;

    /**
     * @var User|null
     */
    private ?User $user;
    /**
     * @var Peer|null
     */
    private ?Peer $peer;
    /**
     * @var Torrent|null
     */
    private ?Torrent $torrent;

    /**
     * @var stdClass
     */
    private stdClass $clientRequest;

    /**
     * @var Announce
     */
    private Announce $request;
    /**
     * @var string
     */
    private string $browsers = 'Mozilla|Chrome|Opera|Lynx|Trident|Edge';

    /**
     * @param FindPeerTask $findPeerTask
     * @param StorePeerTask $storePeerTask
     * @param UpdatePeerTask $updatePeerTask
     * @param DestroyPeerTask $destroyPeerTask
     * @param FindUserTask $findUserTask
     * @param UpdateProfileTask $updateProfileTask
     * @param FindTorrentTask $findTorrentTask
     * @param UpdateTorrentTask $updateTorrentTask
     */
    public function __construct(
        private readonly FindPeerTask $findPeerTask,
        private readonly StorePeerTask $storePeerTask,
        private readonly UpdatePeerTask $updatePeerTask,
        private readonly DestroyPeerTask $destroyPeerTask,
        private readonly FindUserTask $findUserTask,
        private readonly UpdateProfileTask $updateProfileTask,
        private readonly FindTorrentTask $findTorrentTask,
        private readonly UpdateTorrentTask $updateTorrentTask
    ) {

    }

    /**
     * @param Announce $request
     * @return false|string
     * @throws ContainerExceptionInterface
     * @throws NotFoundExceptionInterface
     * @throws Exception
     */
    public function run(Announce $request): bool|string
    {
        $this->clientRequest = $this->setCurrentRequest($request);
        $this->request       = $request;

        if (!$this->isTorrentClient()) {
            return false;
        }

        $this->isFirewalled();

        $this->user    = $this->findUserTask->byPid($this->clientRequest->pid, ['profile']);
        $this->torrent = $this->findTorrentTask->byInfoHash($this->clientRequest->infoHash);
        $this->peer    = $this->findPeerTask->byPeerIdAndTorrentUuid($this->clientRequest->peerId, $this->torrent->uuid);
        if (is_null($this->peer)) {
            $this->peer = $this->storePeerTask->run($this->preparePeerData());
        }


        switch ($this->clientRequest->event) {
            case 'started':
                $this->started();
            break;
            case 'stopped':
                $this->stopped();
            break;
            case 'finished':
                $this->finished();
            break;
            case empty($this->currentRequest->event):
                $this->noEvent();
            break;
        }

        return Bencode::encode([
            'interval'     => config('tracker.max_announce'),
            'min interval' => config('tracker.max_announce'),
            'complete'     => $this->torrent->seeder,
            'incomplete'   => $this->torrent->leecher,
            'peers'        => (function($peerReturn) {
                if($this->clientRequest->compact) {
                    return $peerReturn;
                }

                $peers = [];
                $peerReturn->map(function(Peer $peer) use(&$peers) {
                    $peers[] = [
                        'peer id' => $peer->peer_id,
                        'ip'      => $peer->ip,
                        'port'    => $peer->port
                    ];
                });

                return $peers;
            })($this->sendRandomPeers())
        ]);
    }

    /**
     * @return bool
     */
    private function isTorrentClient(): bool
    {
        return !preg_match('/' . $this->browsers . '/', $this->clientRequest->agent);
    }

    /**
     *
     */
    public function isFirewalled(): void
    {
        $theError = '';
        $fd = @fsockopen($this->clientRequest->ip, $this->clientRequest->port, $errno, $theError, 10);

        $this->clientRequest->nat = !$fd;
        if($fd) fclose($fd);
    }

    /**
     * @return void
     */
    private function started(): void
    {
        $this->updatePeerTask->run($this->peer, $this->preparePeerData('started'));
        $this->updateTorrentTask->run($this->torrent, $this->prepareTorrentData('started'));
        $this->updateProfileTask->run($this->user->profile, $this->prepareProfileData());
    }

    /**
     * @return void
     * @throws Exception
     */
    private function stopped(): void
    {
        $this->destroyPeerTask->run($this->peer);
        $this->updateTorrentTask->run($this->torrent, $this->prepareTorrentData('stopped'));
        $this->updateProfileTask->run($this->user->profile, $this->prepareProfileData());
    }


    /**
     * @return void
     */
    private function finished(): void
    {
        $this->updatePeerTask->run($this->peer, $this->preparePeerData('finished'));
        $this->updateTorrentTask->run($this->torrent, $this->prepareTorrentData('finished'));
        $this->updateProfileTask->run($this->user->profile, $this->prepareProfileData());
    }


    /**
     * @return void
     */
    private function noEvent(): void
    {
        if ($this->peer->bytes != 0 && $this->clientRequest->left == 0) {
            $this->updatePeerTask->run($this->peer, $this->preparePeerData('finished'));
            $this->updateTorrentTask->run($this->torrent, $this->prepareTorrentData('finished'));
        }

        $this->collectBytes();
    }

    /**
     * @return void
     */
    private function collectBytes(): void
    {
        $diff = bcsub($this->peer->bytes, $this->clientRequest->left);

        $this->updatePeerTask->run($this->peer, $this->preparePeerData('bytes', $diff));
    }


    /**
     * @return string|null|Collection
     */
    private function sendRandomPeers(): Collection|string|null
    {
        $peerCollection = $this->findPeerTask->byTorrentUuid($this->torrent->uuid);
        if (!is_null($peerCollection)) {
            if($this->clientRequest->compact) {
                $compactStr = '';
                $peerCollection->map(function (Peer $peer) use (&$compactStr) {
                    if($peer->created_uuid == $this->user->uuid) return;

                    $compactStr .= str_pad(pack('Nn', ip2long($peer->ip), $peer->port), 6);
                });

                return $compactStr;
            }

            return $peerCollection;
        }

        return null;
    }

    /**
     * @param string $status
     * @param float $diff
     * @return array
     */
    private function preparePeerData(string $status = '', float $diff = 0): array
    {
        $data = [
            'created_uuid' => $this->user->uuid,
            'updated_uuid' => $this->user->uuid,
            'torrent_uuid' => $this->torrent->uuid,
            'peer_id'      => $this->clientRequest->peerId,
            'ip'           => $this->clientRequest->ip,
            'port'         => $this->clientRequest->port,
            'status'       => $this->clientRequest->left == 0 ? 1 : 2,
            'natuser'      => $this->clientRequest->nat,
            'client'       => $this->clientRequest->agent,
            'dns'          => $this->clientRequest->dns,
            'downloaded'   => $this->clientRequest->downloaded,
            'uploaded'     => $this->clientRequest->uploaded,
        ];

        switch ($status) {
            case 'started':
                unset($data['peer_id'], $data['torrent_uuid'], $data['ip'], $data['port'], $data['natuser'], $data['client'], $data['dns']);
            break;
            case 'finished':
                unset($data['peer_id'], $data['torrent_uuid'], $data['ip'], $data['port'], $data['natuser'], $data['client'], $data['dns'], $data['compact']);

                $data['bytes']  = 0;
                $data['status'] = 'seeder';
            break;
            case 'bytes':
                unset($data['peer_id'], $data['torrent_uuid'], $data['ip'], $data['port'], $data['status'], $data['natuser'], $data['client'], $data['dns'], $data['compact']);

                $data['bytes'] = empty($diff) ? $this->peer->bytes : $this->clientRequest->left;
            break;
        }

        return $data;
    }

    /**
     * @param string $status
     * @return array
     */
    private function prepareTorrentData(string $status = ''): array
    {
        switch ($status) {
            case 'started':
                $this->clientRequest->left > 0 ? $this->torrent->leecher++ : $this->torrent->seeder++;

                return [
                    'leecher' => $this->torrent->leecher,
                    'seeder' => $this->torrent->seeder,
                    'finished' => $this->torrent->finished,
                ];
            case 'finished':
                return [
                    'leecher' => $this->torrent->leecher--,
                    'seeder' => $this->torrent->seeder++,
                    'finished' => $this->torrent->finished++,
                ];
            case 'stopped':
                $this->clientRequest->left > 0 ? $this->torrent->leecher-- : $this->torrent->seeder--;

                return [
                    'leecher' => $this->torrent->leecher,
                    'seeder' => $this->torrent->seeder,
                    'finished' => $this->torrent->finished,
                ];
        }

        return [];
    }

    /**
     * @return array
     */
    #[ArrayShape([
        'uploaded' => "mixed",
        'downloaded' => "mixed",
        'ratio' => "float|int"
    ])]
    private function prepareProfileData(): array
    {
        $uploaded   = $this->user->profile->uploaded + $this->clientRequest->uploaded;
        $downloaded = $this->user->profile->downloaded + $this->clientRequest->downloaded;

        return [
            'uploaded'   => $uploaded,
            'downloaded' => $downloaded,
            'ratio'      => $uploaded == 0 ? 0 : ($downloaded / $uploaded)
        ];
    }
}
