<?php

namespace Pterodactyl\Services\Servers;

use Illuminate\Support\Arr;
use Pterodactyl\Models\Server;
use Pterodactyl\Models\Allocation;
use Illuminate\Database\ConnectionInterface;
use Pterodactyl\Exceptions\DisplayException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Pterodactyl\Repositories\Wings\DaemonServerRepository;

class BuildModificationService
{
    /**
     * @var \Illuminate\Database\ConnectionInterface
     */
    private $connection;

    /**
     * @var \Pterodactyl\Repositories\Wings\DaemonServerRepository
     */
    private $daemonServerRepository;

    /**
     * @var \Pterodactyl\Services\Servers\ServerConfigurationStructureService
     */
    private $structureService;

    /**
     * BuildModificationService constructor.
     *
     * @param \Pterodactyl\Services\Servers\ServerConfigurationStructureService $structureService
     * @param \Illuminate\Database\ConnectionInterface $connection
     * @param \Pterodactyl\Repositories\Wings\DaemonServerRepository $daemonServerRepository
     */
    public function __construct(
        ServerConfigurationStructureService $structureService,
        ConnectionInterface $connection,
        DaemonServerRepository $daemonServerRepository
    ) {
        $this->daemonServerRepository = $daemonServerRepository;
        $this->connection = $connection;
        $this->structureService = $structureService;
    }

    /**
     * Change the build details for a specified server.
     *
     * @param \Pterodactyl\Models\Server $server
     * @param array $data
     * @return \Pterodactyl\Models\Server
     *
     * @throws \Throwable
     * @throws \Pterodactyl\Exceptions\DisplayException
     */
    public function handle(Server $server, array $data)
    {
        $this->connection->beginTransaction();

        $this->processAllocations($server, $data);

        if (isset($data['allocation_id']) && $data['allocation_id'] != $server->allocation_id) {
            try {
                Allocation::query()->where('id', $data['allocation_id'])->where('server_id', $server->id)->firstOrFail();
            } catch (ModelNotFoundException $ex) {
                throw new DisplayException('The requested default allocation is not currently assigned to this server.');
            }
        }

        // If any of these values are passed through in the data array go ahead and set
        // them correctly on the server model.
        $merge = Arr::only($data, ['oom_disabled', 'memory', 'swap', 'io', 'cpu', 'threads', 'disk', 'allocation_id']);

        $server->forceFill(array_merge($merge, [
            'database_limit' => Arr::get($data, 'database_limit', 0) ?? null,
            'allocation_limit' => Arr::get($data, 'allocation_limit', 0) ?? null,
            'backup_limit' => Arr::get($data, 'backup_limit', 0) ?? 0,
        ]))->saveOrFail();

        $server = $server->fresh();

        $updateData = $this->structureService->handle($server);

        if (!empty($updateData['build'])) {
            $this->daemonServerRepository->setServer($server)->update([
                'build' => $updateData['build'],
            ]);
        }

        $this->connection->commit();

        return $server;
    }

    /**
     * Process the allocations being assigned in the data and ensure they are available for a server.
     *
     * @param \Pterodactyl\Models\Server $server
     * @param array $data
     *
     * @throws \Pterodactyl\Exceptions\DisplayException
     */
    private function processAllocations(Server $server, array &$data)
    {
        if (empty($data['add_allocations']) && empty($data['remove_allocations'])) {
            return;
        }

        // Handle the addition of allocations to this server. Only assign allocations that are not currently
        // assigned to a different server, and only allocations on the same node as the server.
        if (! empty($data['add_allocations'])) {
            $query = Allocation::query()
                ->where('node_id', $server->node_id)
                ->whereIn('id', $data['add_allocations'])
                ->whereNull('server_id');

            // Keep track of all the allocations we're just now adding so that we can use the first
            // one to reset the default allocation to.
            $freshlyAllocated = $query->pluck('id')->first();

            $query->update(['server_id' => $server->id, 'notes' => null]);
        }

        if (! empty($data['remove_allocations'])) {
            foreach ($data['remove_allocations'] as $allocation) {
                // If we are attempting to remove the default allocation for the server, see if we can reassign
                // to the first provided value in add_allocations. If there is no new first allocation then we
                // will throw an exception back.
                if ($allocation === ($data['allocation_id'] ?? $server->allocation_id)) {
                    if (empty($freshlyAllocated)) {
                        throw new DisplayException(
                            'You are attempting to delete the default allocation for this server but there is no fallback allocation to use.'
                        );
                    }

                    // Update the default allocation to be the first allocation that we are creating.
                    $data['allocation_id'] = $freshlyAllocated;
                }
            }

            // Remove any of the allocations we got that are currently assigned to this server on
            // this node. Also set the notes to null, otherwise when re-allocated to a new server those
            // notes will be carried over.
            Allocation::query()->where('node_id', $server->node_id)
                ->where('server_id', $server->id)
                // Only remove the allocations that we didn't also attempt to add to the server...
                ->whereIn('id', array_diff($data['remove_allocations'], $data['add_allocations'] ?? []))
                ->update([
                    'notes' => null,
                    'server_id' => null,
                ]);
        }
    }
}
