<?php

declare(strict_types=1);

namespace BeycanPress\Tokenico;

use BeycanPress\Tokenico\Models\Sale;
use BeycanPress\Tokenico\Models\Presale;
use BeycanPress\Tokenico\PluginHero\Hook;
use BeycanPress\Tokenico\PluginHero\Helpers;
use BeycanPress\Tokenico\PluginHero\BaseAPI;
use BeycanPress\Tokenico\PluginHero\Http\Request;
use BeycanPress\Tokenico\PluginHero\Http\Response;
// MultipleChain interfaces and enums
use MultipleChain\Enums\TransactionStatus;
use MultipleChain\Interfaces\Models\ContractTransactionInterface;
// MultipleChain providers and models
use MultipleChain\TON\Provider as TonProvider;
use MultipleChain\Tron\Provider as TronProvider;
use MultipleChain\Solana\Provider as SolanaProvider;
use MultipleChain\EvmChains\Provider as EvmProvider;
use MultipleChain\TON\Models\ContractTransaction as TonContractTransaction;
use MultipleChain\Tron\Models\ContractTransaction as TronContractTransaction;
use MultipleChain\Solana\Models\ContractTransaction as SolanaContractTransaction;
use MultipleChain\EvmChains\Models\ContractTransaction as EvmContractTransaction;

class RestAPI extends BaseAPI
{
    /**
     * @var Request
     */
    private Request $request;

    /**
     * @var int
     */
    private int $currentUserId;

    /**
     * @var Presale
     */
    private Presale $presale;

    /**
     * @var Sale
     */
    private Sale $sale;

    /**
     * @return void
     */
    public function __construct()
    {
        add_action('init', [$this, 'init']);

        $this->addMiddleware('tokenico', [$this, 'middleware']);

        $this->addRoutes([
            'tokenico' => [
                'request-nonce' => [
                    'callback' => 'requestNonce',
                    'methods' => ['GET'],
                ],
                'save-deployed-contract' => [
                    'callback' => 'saveDeployedContract',
                    'methods' => ['POST'],
                ],
                'create-presale-record' => [
                    'callback' => 'createPresaleRecord',
                    'methods' => ['POST'],
                ],
                'get-presale-records' => [
                    'callback' => 'getPresaleRecords',
                    'methods' => ['GET'],
                ],
                'get-presale-record' => [
                    'callback' => 'getPresaleRecord',
                    'methods' => ['GET'],
                ],
                'delete-presale-record' => [
                    'callback' => 'deletePresaleRecord',
                    'methods' => ['POST'],
                ],
                'create-sale-record' => [
                    'callback' => 'createSaleRecord',
                    'methods' => ['POST'],
                ],
                'get-sale-records' => [
                    'callback' => 'getSaleRecords',
                    'methods' => ['GET'],
                ],
                'update-presale-metadata' => [
                    'callback' => 'updatePresaleMetadata',
                    'methods' => ['POST'],
                ],
                'find-solana-presale-by-account' => [
                    'callback' => 'findSolanaPresaleByAccount',
                    'methods' => ['GET'],
                ],
            ]
        ]);
    }

    /**
     * @return void
     */
    public function init(): void
    {
        $this->currentUserId = get_current_user_id();
    }

    /**
     * @param mixed $result
     * @param \WP_REST_Server $server
     * @param \WP_REST_Request $request
     * @return void
     */
    public function middleware(mixed $result, \WP_REST_Server $server, \WP_REST_Request $request): void
    {
        $this->sale = new Sale();
        $this->presale = new Presale();
        $this->request = new Request();
        if (
            "/tokenico/request-nonce" !== $request->get_route() &&
            !Helpers::verifyNonce($this->request->getParam('nonce'))
        ) {
            Response::error(esc_html__('Invalid nonce', 'tokenico'));
        }
    }

    /**
     * @return void
     */
    public function requestNonce(): void
    {
        Response::success(esc_html__('Nonce created successfully', 'tokenico'), Helpers::createNewNonce());
    }

    /**
     * @return void
     */
    public function saveDeployedContract(): void
    {
        if (!$this->currentUserId) {
            Response::error(esc_html__('You must be logged in to save a deployed contract', 'tokenico'));
        }

        if (!user_can($this->currentUserId, 'manage_options')) {
            Response::error(__('Unauthorized', 'tokenico'));
        }

        $version = $this->request->getParam('version');
        $address = $this->request->getParam('address');
        $networkId = $this->request->getParam('networkId');

        $deployedContracts = Service::getDeployedContracts();

        if (!isset($deployedContracts->$networkId)) {
            $deployedContracts->$networkId = new \stdClass();
        }

        if (is_array($deployedContracts->$networkId)) {
            $deployedContracts->$networkId[$version] = $address;
        } else {
            $deployedContracts->$networkId->$version = $address;
        }

        update_option(
            Hook::callFilter('deployed_contracts_key', 'tokenico_deployed_contracts'),
            json_encode($deployedContracts, JSON_PRETTY_PRINT)
        );

        Response::success();
    }

    /**
     * @return void
     */
    public function createPresaleRecord(): void
    {
        if (!$this->currentUserId) {
            Response::error(esc_html__('You must be logged in to create a presale record', 'tokenico'));
        }

        $key = $this->request->getParam('key');
        $data = $this->request->getParam('data');
        $args = $this->request->getParam('args');
        $token = $this->request->getParam('token');
        $createdAt = $this->request->getParam('createdAt');
        $networkId = $this->request->getParam('networkId');
        $networkCode = $this->request->getParam('networkCode');
        $adminAddress = $this->request->getParam('adminAddress');
        $nativeCurrency = $this->request->getParam('nativeCurrency');
        $contractAddress = $this->request->getParam('contractAddress');
        $contractVersion = $this->request->getParam('contractVersion');

        /** @disregard */
        $result = $this->presale->insert([
            'key' => $key,
            'totalSales' => 0,
            'networkId' => $networkId,
            'networkCode' => $networkCode,
            'data' => json_encode($data),
            'args' => json_encode($args),
            'token' => json_encode($token),
            'userId' => $this->currentUserId,
            'adminAddress' => $adminAddress,
            'contractAddress' => $contractAddress,
            'contractVersion' => $contractVersion,
            'nativeCurrency' => $nativeCurrency,
            'remainingLimit' => $data->totalSaleLimit,
            'createdAt' => (int) floor($createdAt / 1000),
            'startTime' => (int) floor($data->startTime / 1000),
            'endTime' => (int) floor($data->endTime / 1000),
            'premium' => defined('BP_TOKENICO_PRO_LOADED'),
        ]);

        if ($result) {
            Response::success(esc_html__('Presale record created successfully', 'tokenico'));
        } else {
            Response::error(esc_html__('Failed to create presale record', 'tokenico'));
        }
    }

    /**
     * @return void
     */
    public function getPresaleRecords(): void
    {
        $filter = $this->request->getParam('filter');
        $page = absint($this->request->getParam('page')) ?: 1;
        $creator = 'true' === $this->request->getParam('creator');

        $args = [
            'page' => $page - 1,
            'premium' => defined('BP_TOKENICO_PRO_LOADED') ? 1 : 0,
        ];

        $args['status'] = '' !== $filter?->status ? $filter?->status : null;
        $args['networkId'] = '' !== $filter?->networkId ? $filter?->networkId : null;
        $args['search'] = '' !== $filter?->search ? sanitize_text_field($filter?->search) : null;

        if ($this->currentUserId && !Service::isAdmin($this->currentUserId) && $creator) {
            $args['userId'] = $this->currentUserId;
        }

        [$records, $totalPage] = $this->presale->filter($args);

        Response::success(esc_html__('Success', 'tokenico'), [
            'records' => $records,
            'totalPage' => $totalPage,
        ]);
    }

    /**
     * @return void
     */
    public function getPresaleRecord(): void
    {
        $key = $this->request->getParam('key');
        $creator = 'true' === $this->request->getParam('creator');

        $args = [
            'key' => $key,
        ];

        if ($this->currentUserId && !Service::isAdmin($this->currentUserId) && $creator) {
            $args['userId'] = $this->currentUserId;
        }

        $record = $this->presale->findOneBy($args);

        if ($record) {
            Response::success(esc_html__('Success', 'tokenico'), [
                'record' => Service::presaleFormat($record),
            ]);
        } else {
            Response::error(esc_html__('Record not found', 'tokenico'));
        }
    }

    /**
     * @return void
     */
    public function deletePresaleRecord(): void
    {
        if (!$this->currentUserId) {
            Response::error(esc_html__('You must be logged in to delete a presale record', 'tokenico'));
        }

        $key = $this->request->getParam('key');

        $args = [
            'key' => $key,
        ];

        if (!Service::isAdmin($this->currentUserId)) {
            $args['userId'] = $this->currentUserId;
        }

        $result = $this->presale->delete($args);

        if ($result) {
            Response::success(esc_html__('Presale record deleted successfully', 'tokenico'));
        } else {
            Response::error(esc_html__('Failed to delete presale record', 'tokenico'));
        }
    }

    /**
     * @param string|int $id
     * @return array<mixed>
     */
    private function getMcProvider(string|int $id): array
    {
        $networks = Settings::getNetworks();
        $networkIndex = array_search($id, array_column($networks, 'id'));
        if (false === $networkIndex) {
            throw new \InvalidArgumentException('Network not found: ' . $id);
        }
        $network = $networks[$networkIndex];
        switch ($network['code']) {
            case 'evm':
                return [
                    'provider' => new EvmProvider($network),
                    'transaction' => EvmContractTransaction::class,
                ];
            case 'solana':
                return [
                    'provider' => new SolanaProvider($network),
                    'transaction' => SolanaContractTransaction::class,
                ];
            case 'tron':
                return [
                    'provider' => new TronProvider($network),
                    'transaction' => TronContractTransaction::class,
                ];
            case 'ton':
                return [
                    'provider' => new TonProvider($network),
                    'transaction' => TonContractTransaction::class,
                ];
            default:
                throw new \InvalidArgumentException('Unsupported network code: ' . $network['code']);
        }
    }

    /**
     * @return void
     */
    public function createSaleRecord(): void
    {
        $token = $this->request->getParam('token');
        $networkId = $this->request->getParam('networkId');
        $presaleKey = $this->request->getParam('presaleKey');
        $createdAt = $this->request->getParam('createdAt');
        $tokenAmount = $this->request->getParam('tokenAmount');
        $nativeCurrency = $this->request->getParam('nativeCurrency');
        $purchaseAmount = $this->request->getParam('purchaseAmount');
        $transactionHash = $this->request->getParam('transactionHash');
        $transactionUrl = $this->request->getParam('transactionUrl');
        $receiverAddress = $this->request->getParam('receiverAddress');
        $receiverAddressUrl = $this->request->getParam('receiverAddressUrl');

        /** @var object|null $presale */
        $presale = $this->presale->findOneBy([
            'key' => $presaleKey,
        ]);

        if (!$presale) {
            Response::error(esc_html__('Presale record not found', 'tokenico'));
        }

        $provider = $this->getMcProvider($presale->networkId);
        /** @var ContractTransactionInterface $tx */
        $tx = new $provider['transaction']($transactionHash);

        $tx->wait();

        if (TransactionStatus::CONFIRMED !== $tx->getStatus()) {
            Response::error(esc_html__('Transaction is not confirmed', 'tokenico'));
        }

        if (strtolower($tx->getAddress()) !== strtolower($presale->contractAddress)) {
            Response::error(
                esc_html__('Transaction receiver address does not match presale contract address', 'tokenico')
            );
        }

        if (strtolower($tx->getSigner()) !== strtolower($receiverAddress)) {
            Response::error(esc_html__('Transaction signer does not match receiver address', 'tokenico'));
        }

        $this->presale->update([
            'totalSales' => $presale->totalSales + $purchaseAmount,
            'remainingLimit' => $presale->remainingLimit - $purchaseAmount,
        ], [
            'key' => $presaleKey,
        ]);

        /** @disregard */
        $result = $this->sale->insert([
            'networkId' => $networkId,
            'userId' => $presale->userId,
            'token' => json_encode($token),
            'presaleKey' => $presaleKey,
            'tokenAmount' => $tokenAmount,
            'nativeCurrency' => $nativeCurrency,
            'purchaseAmount' => $purchaseAmount,
            'transactionHash' => $transactionHash,
            'transactionUrl' => $transactionUrl,
            'receiverAddress' => $receiverAddress,
            'receiverAddressUrl' => $receiverAddressUrl,
            'createdAt' => (int) floor($createdAt / 1000),
            'premium' => defined('BP_TOKENICO_PRO_LOADED'),
        ]);

        if ($result) {
            Response::success(esc_html__('Sale record created successfully', 'tokenico'));
        } else {
            Response::error(esc_html__('Failed to create sale record', 'tokenico'));
        }
    }

    /**
     * @return void
     */
    public function getSaleRecords(): void
    {
        if (!$this->currentUserId) {
            Response::error(esc_html__('You must be logged in to get sale records', 'tokenico'));
        }

        $search = $this->request->getParam('search');
        $page = absint($this->request->getParam('page')) ?: 1;
        $search = '' !== $search ? sanitize_text_field($search) : null;

        $args = [
            'premium' => defined('BP_TOKENICO_PRO_LOADED') ? 1 : 0
        ];

        if ($search) {
            $args[] = [
                'token',
                'LIKE',
                $search,
            ];
        }

        if (!Service::isAdmin($this->currentUserId)) {
            $args['userId'] = $this->currentUserId;
        }

        $records = $this->sale->findBy($args, ['id', 'DESC'], 10, $page - 1);
        $totalPage = (int) ceil($this->sale->getCount($args) / 10);

        Response::success(esc_html__('Success', 'tokenico'), [
            'records' => Service::saleBulkFormat($records),
            'totalPage' => $totalPage
        ]);
    }

    /**
     * @return void
     */
    public function updatePresaleMetadata(): void
    {
        if (!$this->currentUserId) {
            Response::error(esc_html__('You must be logged in to update metadata', 'tokenico'));
        }

        $key = $this->request->getParam('key');
        $metadata = $this->request->getParam('metadata');

        $result = $this->presale->update([
            'metadata' => json_encode($metadata),
        ], [
            'key' => $key,
            'userId' => $this->currentUserId,
        ]);

        if ($result) {
            Response::success(esc_html__('Metadata updated successfully', 'tokenico'));
        } else {
            Response::error(esc_html__('Failed to update metadata', 'tokenico'));
        }
    }

    /**
     * @return void
     */
    public function findSolanaPresaleByAccount(): void
    {
        if (!$this->currentUserId) {
            Response::error(esc_html__('You must be logged in to find a presale record', 'tokenico'));
        }

        $presaleAccount = $this->request->getParam('presaleAccount');

        $record = $this->presale->findOneBy([
            'args' => json_encode(['presaleAccount' => $presaleAccount]),
            'premium' => defined('BP_TOKENICO_PRO_LOADED') ? 1 : 0,
        ], ['id', 'DESC']);

        if ($record) {
            Response::success(esc_html__('Success', 'tokenico'), [
                'record' => Service::presaleFormat($record),
            ]);
        } else {
            Response::success(esc_html__('Record not found', 'tokenico'));
        }
    }
}
