<?php

namespace App\Services\Transaction\User;

use App\Models\Account;
use App\Helpers\MsgState;
use App\Services\Service;
use App\Jobs\ProcessEmail;
use Brick\Math\BigDecimal;
use App\Models\Transaction;
use App\Models\UserAccount;
use Illuminate\Support\Arr;
use App\Models\UserTempMeta;
use Illuminate\Http\Request;
use App\Models\PaymentMethod;
use App\Enums\TransactionType;
use App\Models\WithdrawMethod;
use App\Enums\TransactionStatus;
use App\Traits\WrapInTransaction;
use App\Enums\PaymentMethodStatus;
use App\Filters\TransactionFilter;
use App\Enums\WithdrawMethodStatus;
use App\Exceptions\User\Transaction\DepositTryMethodException;
use Illuminate\Validation\ValidationException;
use App\Services\Transaction\TransactionProcessor;
use App\Exceptions\User\Transaction\DepositWrongException;
use App\Exceptions\User\Transaction\DepositNoRateException;
use App\Exceptions\User\Transaction\WithdrawWrongException;
use App\Exceptions\User\Transaction\WithdrawNoRateException;
use App\Exceptions\User\Transaction\DepositInvalidMethodException;
use App\Exceptions\User\Transaction\TnxCancelTimeoutException;
use App\Exceptions\User\Transaction\TnxErrorException;
use App\Exceptions\User\Transaction\TnxInvalidActionException;
use App\Exceptions\User\Transaction\TnxInvalidStatusException;
use App\Exceptions\User\Transaction\WithdrawInvalidMethodException;
use App\Exceptions\User\Transaction\WithdrawInvalidAccountException;
use App\Exceptions\User\Transaction\WithdrawAmountValidationException;
use App\Exceptions\User\Transaction\WithdrawInvalidActionException;
use App\Services\Transaction\TransactionService as TransactionTransactionService;

class TransactionService extends Service
{
    use WrapInTransaction;

    public $rounded;
    public $basecur;
    public $altcur;

    public function __construct()
    {
        $this->basecur = base_currency();
        $this->altcur = secondary_currency();
        $this->rounded = (object)[
            'fiat' => sys_settings('decimal_fiat_calc', 3),
            'crypto' => sys_settings('decimal_crypto_calc', 6),
            'fiatmax' => env('FIXED_FIAT_MAX', 2),
            'cryptomax' => env('FIXED_CRYPTO_MAX', 5),
        ];
    }
    /**
     * @param $name
     * @return boolean
     * @version 1.0.0
     * @since 1.0
     */
    public function hasAccountBalance($name = null)
    {
        $name = (empty($name)) ? AccType('main') : $name;
        $userID = auth()->user()->id;
        return Account::hasBalance($name, $userID);
    }
    /**
     * @param $method
     * @return boolean
     * @version 1.0.0
     * @since 1.0
     */

    public function hasUserAccounts($method = null)
    {
        $user_id = auth()->user()->id;
        return UserAccount::hasAccounts($method, $user_id);
    }

    /**
     * @param $wdm
     * @return mixed|object|models
     * @version 1.0.0
     * @since 1.0
     */
    public function wdmDetails($wdm)
    {
        return WithdrawMethod::where('slug', $wdm)
            ->where('status', WithdrawMethodStatus::ACTIVE)->first();
    }

    /**
     * @param $account |object
     * @param $method |object
     * @return mixed|array
     * @version 1.0.0
     * @since 1.0
     */
    private function payAccount($account, $method, $currency = null, $only = true)
    {
        return UserAccount::paymentInfo($account, $method, $currency, $only);
    }

    /**
     * @param $amount1
     * @param $amount2
     * @return number|string
     * @version 1.0.0
     * @since 1.1.4
     */
    private function toSub($amount1, $amount2)
    {
        $total = BigDecimal::of($amount1)->minus(BigDecimal::of($amount2));
        return is_object($total) ? (string)$total : $total;
    }

    /**
     * @param $method
     * @return object|bool
     * @version 1.0.0
     * @since 1.0
     */
    public function getUserAccounts($method = null)
    {
        if (!blank($method)) {
            $user_id = auth()->user()->id;
            return UserAccount::getAccounts($method, $user_id);
        }
        return false;
    }

    /**
     * @param $name
     * @return mixed|object|models
     * @version 1.0.0
     * @since 1.0
     */
    public function getAccountBalance($name = null, $echo = false)
    {
        $name = (empty($name)) ? AccType('main') : $name;
        $userID = auth()->user()->id;
        return Account::getBalance($name, $userID, $echo);
    }

    public function getScheduledCount()
    {
        $scheduledCount = Transaction::loggedUser()
        ->whereIn('status', [TransactionStatus::PENDING, TransactionStatus::CONFIRMED, TransactionStatus::ONHOLD])
        ->whereNotIn('type', [TransactionType::REFERRAL])
        ->count();

        return $scheduledCount;
    }

    /**
     * @param Request $request
     * @param TransactionFilter $filter
     * @return array
     * @throws \ReflectionException
     * @version 1.0.0
     * @since 1.0
     */
    public function getTnxList(Request $request, TransactionFilter $filter)
    {
        $tnxTypes = get_enums(TransactionType::class, false);
        $tnxStates = Arr::except(get_enums(TransactionStatus::class, false), ['NONE', 'ONHOLD', 'CONFIRMED', 'PENDING']);

        $scheduledCount = $this->getScheduledCount();

        $scheduled = ($request->has('view') && $request->get('view') == 'scheduled') ? true : false;

        $orderBy = $scheduled ? 'id' : 'completed_at';
        $sortBy = $scheduled ? 'asc' : 'desc';

        $query = Transaction::loggedUser()->orderBy($orderBy, $sortBy)->whereNotIn('status', [TransactionStatus::NONE]);

        if (!$scheduled && blank($request->get('query')) && blank($request->get('filter'))) {
            $query->where('status', TransactionStatus::COMPLETED);
        }

        if ($scheduled || ($request->get('filter') == true && $request->get('type') != TransactionType::REFERRAL)) {
            $query->whereNotIn('type', [TransactionType::REFERRAL]);
        }

        $tnxCount = $query->filter($filter)->count();

        $transactions = $query->paginate(user_meta('tnx_perpage', 10))
            ->onEachSide(0);

        return compact('transactions', 'tnxTypes', 'tnxStates', 'tnxCount', 'scheduledCount');
    }

    public function getPaymentMethodList()
    {
        $paymentMethods = available_payment_methods();
        $activeMethods = PaymentMethod::whereIn('slug', array_column($paymentMethods, 'slug'))
            ->where('status', PaymentMethodStatus::ACTIVE)
            ->get()->filter(function ($item) {
                return $item->is_active;
            });

        return $activeMethods;
    }

    public function getWithdrawMethodList()
    {
        $withdrawMethods = available_withdraw_methods();
        $activeMethods = WithdrawMethod::whereIn('slug', array_column($withdrawMethods, 'slug'))
            ->where('status', WithdrawMethodStatus::ACTIVE)
            ->get()->filter(function ($item) {
                return $item->is_active;
            });

        return $activeMethods;
    }

    public function pmDetails($pm)
    {
        return PaymentMethod::where('slug', $pm)
            ->where('status', PaymentMethodStatus::ACTIVE)
            ->first();
    }

    /**
     * @param $method
     * @param bool $format
     * @param string $what
     * @return array
     * @version 1.0.0
     * @since 1.0
     */
    public function getCurrenciesData($method, $format = false, $what = 'deposit', $only = null)
    {
        if (!blank($method)) {
            $method_currencies = collect($method->currencies);
            $active_currencies = $method_currencies->intersect(active_currencies('key'));

            $data = $active_currencies->map(function ($currency) use ($method, $format, $what) {
                $amounts = $this->getCurrencyAmounts($currency, $method, $format, $what);
                $rate = ($format === false) ? $amounts->fx : amount($amounts->fx, $currency, ['dp' => 'calc']);
                $minimum = ($format === false) ? round($amounts->minimum, $amounts->dp) : amount($amounts->minimum, $currency, ['dp' => 'calc']);
                $maximum = ($format === false) ? round($amounts->maximum, $amounts->dp) : amount($amounts->maximum, $currency, ['dp' => 'calc']);

                return [
                    'rate' => (float) $rate, 'min' => (float) $minimum, 'max' => (float) $maximum, 'code' => $currency, 'dp' => $amounts->dp, 'dx' => $amounts->dx,
                ];
            })->keyBy('code');

            $output = ($only == 'rate') ? array_column($data->toArray(), 'rate', 'code') : $data->toArray();

            if (!isset($output[$this->basecur])) {
                if ($only == 'rate') {
                    $output[$this->basecur] = 1;
                } else {
                    $base = $this->getCurrencyAmounts($this->basecur, $method, $format, $what, true);
                    $output[$this->basecur] = ['rate' => $base->fx, 'min' => $base->minimum, 'max' => $base->maximum, 'code' => $this->basecur, 'dp' => $base->dp, 'dx' => $base->dx];
                }
            }
            return $output;
        }

        return [];
    }

    /**
     * @param string $currency
     * @param object $method
     * @param boolean $fm
     * @param string $what
     * @param boolean $ignore
     * @return mixed
     * @version 1.0.0
     * @since 1.0
     */
    private function getCurrencyAmounts($currency, $method, $fm = false, $what = 'deposit', $ignore = false)
    {
        $fx = BigDecimal::of(get_ex_rate($currency));
        $type = (is_crypto($currency)) ? 'crypto' : 'fiat';
        $where = ($what === 'withdraw') ? 'withdraw_' : 'deposit_';
        $isMin = $this->specifyMin($method, $currency);
        $isMax = $this->specifyMax($method, $currency);

        $maxF = sys_settings($where . 'fiat_maximum', 0);
        $minF = sys_settings($where . 'fiat_minimum', 0.1);

        $round = sys_settings($type . '_rounded');
        $maxD = sys_settings($where . $type . '_maximum', 0);
        $minD = sys_settings($where . $type . '_minimum', 0.1);

        $maxD = (empty($maxD)) ? $maxF : $maxD;
        $minD = (empty($minD)) ? $minF : $minD;

        $maxM = ($method->max_amount) ? $method->max_amount : 0;
        $maxS = ($isMax && $ignore !== true) ? $isMax : 0;

        $minM = ($method->min_amount) ? $method->min_amount : 0;
        $minS = ($isMin && $ignore !== true) ? $isMin : 0;

        $minDefault = BigDecimal::of($minD)->multipliedBy($fx);
        $minMethod = BigDecimal::of($minM)->multipliedBy($fx);
        $minSpecify = BigDecimal::of($minS);

        $maxDefault = BigDecimal::of($maxD)->multipliedBy($fx);
        $maxMethod = BigDecimal::of($maxM)->multipliedBy($fx);
        $maxSpecify = BigDecimal::of($maxS);

        $minAmount = (BigDecimal::of($minMethod)->compareTo($minDefault) > 0) ? $minMethod : $minDefault;
        $minAmount = (BigDecimal::of($minSpecify)->compareTo('0') > 0) ? $minSpecify : $minAmount;

        $maxAmount = (BigDecimal::of($maxMethod)->compareTo($maxDefault) > 0) ? $maxMethod : $maxDefault;
        $maxAmount = (BigDecimal::of($maxSpecify)->compareTo('0') > 0) ? $maxSpecify : $maxAmount;
        $maxAmount = (BigDecimal::of($maxAmount)->compareTo($minAmount) > 0) ? $maxAmount : 0;

        $fx = is_object($fx) ? (string)$fx : $fx;
        $minAmount = is_object($minAmount) ? (string)$minAmount : $minAmount;
        $maxAmount = is_object($maxAmount) ? (string)$maxAmount : $maxAmount;

        if (in_array($round, ['up', 'down'])) {
            $minAmount = ($round === 'up') ? ceil($minAmount) : floor($minAmount);
            $maxAmount = ($round === 'up') ? ceil($maxAmount) : floor($maxAmount);
        }

        // Override if 0
        $minAmount = (BigDecimal::of($minAmount)->compareTo(0) == 1) ? $minAmount : 0.1;

        $rounded = $this->specifyRounded($method, $currency);
        if ($this->isMethod($method, 'crypto')) {
            $rounded = (is_crypto($currency)) ? $this->specifyRounded($method, $currency) : $this->rounded->$type;
        }

        $return = [
            'dx' => $rounded,
            'dp' => $this->rounded->$type,
            'fx' => ($fm === true) ? amount($fx, $currency) : $fx,
            'minimum' => ($fm === true) ? amount($minAmount, $currency) : round($minAmount, $this->rounded->$type),
            'maximum' => ($fm === true) ? amount($maxAmount, $currency) : round($maxAmount, $this->rounded->$type),
        ];

        return (object)$return;
    }

    /**
     * @param $gateway |object
     * @param $currency
     * @return mixed
     * @version 1.1.4
     * @since 1.0.0
     */
    private function specifyMin($gateway, $currency)
    {
        $metaMinOld = data_get($gateway->config, 'meta.min', 0);

        if ($this->isMethod($gateway, 'crypto')) {
            if (data_get($gateway->config, 'wallet.' . $currency . '.min')) {
                return data_get($gateway->config, 'wallet.' . $currency . '.min', 0);
            }
            return $metaMinOld;
        } else {
            if (data_get($gateway->config, 'currencies.' . $currency . '.min')) {
                return data_get($gateway->config, 'currencies.' . $currency . '.min');
            }
            return $metaMinOld;
        }
    }

    /**
     * @param $gateway |object
     * @param $currency
     * @return mixed
     * @version 1.0.0
     * @since 1.1.4
     */
    private function specifyMax($gateway, $currency)
    {
        if ($this->isMethod($gateway, 'crypto')) {
            return data_get($gateway->config, 'wallet.' . $currency . '.max', 0);
        } else {
            if (data_get($gateway->config, 'currencies.' . $currency . '.max')) {
                return data_get($gateway->config, 'currencies.' . $currency . '.max', 0);
            }
            if (data_get($gateway->config, 'meta.max')) {
                return data_get($gateway->config, 'meta.max', 0);
            }
        }
    }

    /**
     * @param $name
     * @param $gateway |object
     * @return bool
     * @version 1.0.0
     * @since 1.0
     */
    private function isMethod($gateway, $name = null)
    {
        $method = (isset($gateway->method)) ? $gateway->method : false;

        if (empty($name) || empty($method)) {
            return false;
        }

        return ($method == $name) ? true : false;
    }

    /**
     * @param $gateway |object
     * @return mixed
     * @version 1.0.0
     * @since 1.1.4
     */
    public function specifyRounded($gateway, $currency = null)
    {
        $method = data_get($gateway, 'method');
        $rounded = data_get($gateway, 'module_config.rounded');

        $decimal = $this->rounded->fiat;
        if (!empty($currency) && is_crypto($currency)) {
            $decimal = $this->rounded->crypto;
        }

        $default = ($method == 'crypto') ? $this->rounded->crypto : $decimal;

        return ($rounded > 0 && $default > $rounded) ? $rounded : $default;
    }

    public function depositAmount($pm)
    {
        $method = $this->pmDetails($pm);

        if (empty($method)) {
            throw new DepositInvalidMethodException();
        }

        $currenciesData = $this->getCurrenciesData($method, false, 'deposit');
        $currenciesCode = array_keys($currenciesData);
        $currenciesOnly = array_intersect($currenciesCode, $method->currencies);
        $currencies = Arr::only($currenciesData, $currenciesOnly);

        if (blank($method) || blank($currencies)) {
            throw new DepositInvalidMethodException();
        }


        $rates = $this->getCurrenciesData($method, false, 'deposit', 'rate');
        $default = (in_array($this->basecur, array_keys($currencies))) ? Arr::get($currencies, $this->basecur) : Arr::first($currencies);

        $tempMetas = [
            [
                'user_id' => auth()->id(),
                'meta_key' => 'deposit_currencies_' . $method->slug,
                'meta_value' => array_keys($currencies),
            ],
        ];

        foreach ($tempMetas as $tempMeta) {
            UserTempMeta::updateOrCreate([
                'user_id' => $tempMeta['user_id'],
                'meta_key' => $tempMeta['meta_key'],
            ], ['meta_value' => $tempMeta['meta_value']]);
        }

        return compact('method', 'default', 'currenciesData', 'currencies', 'rates');
    }

    public function depositPreview($request)
    {
        $base_cur = $this->basecur;
        $currency = strtoupper($request->get('deposit_currency'));
        $amount = (float)$request->get('deposit_amount');
        $user = auth()->user();
        $pm = $this->pmDetails($request->deposit_payment_method);
        $currencies = user_temp_meta('deposit_currencies_' . $request->deposit_payment_method, [], $user);
        $currencies = (!blank($pm)) ? array_intersect($currencies, $pm->currencies) : $currencies;
        $rates = (!blank($pm)) ? $this->getCurrenciesData($pm) : array();

        // check currency empty
        if (empty($currencies)) {
            throw new DepositWrongException();
        }

        // recheck error
        if (
            !in_array($currency, $currencies)
            || !(request()->ajax() || request()->acceptsJson() || request()->wantsJson())
            || blank($pm)
            || !isset($rates[$currency])
        ) {
            if (blank($pm)) {
                throw new DepositInvalidMethodException();
            }
            throw new DepositWrongException();
        }

        $amount = round($amount, $this->specifyRounded($pm, $currency));
        $has_fiat = (isset($pm->config['meta']['fiat'])) ? $pm->config['meta']['fiat'] : false;
        $alt_cur = ($has_fiat && $has_fiat !== 'alter') ? $has_fiat : secondary_currency();

        $roundedC = is_crypto($currency) ? $this->specifyRounded($pm, $currency) : $this->rounded->fiatmax;
        $roundedB = is_crypto($base_cur) ? $this->specifyRounded($pm, $base_cur) : $this->rounded->fiatmax;
        $fx = $rates[$currency];

        if (BigDecimal::of($amount)->compareTo($fx['min']) == -1) {
            throw ValidationException::withMessages([
                'deposit_amount' => __("Sorry, the minimum amount of :amount is required to deposit funds.", ['amount' => money($fx['min'], $currency)])
            ]);
        }

        if (!empty($fx['max']) && BigDecimal::of($amount)->compareTo($fx['max']) == 1) {
            throw ValidationException::withMessages([
                'deposit_amount' => __("You can deposit funds maximum :amount in a single order.", ['amount' => money($fx['max'], $currency)])
            ]);
        }

        $exchange = BigDecimal::of($fx['rate']);
        if (BigDecimal::of($exchange)->compareTo(0) != 1) {
            throw new DepositNoRateException();
        }

        $equal_amount = BigDecimal::of(get_fx_rate($currency, $alt_cur, $amount));
        $fees = fees_calc($pm, $amount, $currency, false);
        $amount_fee = Arr::get($fees, 'total', 0);
        $amount_fee = round($amount_fee, $roundedC);
        $fx_cur = ($currency == $base_cur) ? $alt_cur : $currency;
        $fx_rate = ($currency == $base_cur) ? get_fx_rate($base_cur, $alt_cur) : $fx['rate'];

        $exchange = is_object($exchange) ? (string)$exchange : $exchange;
        $equal_amount = is_object($equal_amount) ? (string)$equal_amount : $equal_amount;

        $base_amount = round(($amount / $exchange), $roundedB);
        $base_fee = round(($amount_fee / $exchange), $roundedB);

        $payment = [
            'method' => $pm->slug,
            'method_name' => $pm->title,
            'currency' => $currency,
            'currency_name' => get_currency($currency, 'name'),
            'amount' => $amount,
            'amount_fees' => $amount_fee,
            'fees' => $fees,
            'total' => $this->toSum($amount, $amount_fee),
            'base_amount' => $base_amount,
            'base_fees' => $base_fee,
            'base_total' => $this->toSum($base_amount, $base_fee),
            'base_currency' => $base_cur,
            'equal_amount' => $equal_amount,
            'equal_currency' => $alt_cur,
            'exchange_rate' => $exchange,
            'fx_rate' => $fx_rate,
            'fx_currency' => $fx_cur,
            'pay_to' => $this->payInfo($pm, $currency, true),
            'pay_meta' => $this->payInfo($pm, $currency, false)
        ];

        $feeinfo = $amount_fee ? get_fs_tip($payment) : '';

        UserTempMeta::updateOrCreate([
            'user_id' => $user->id,
            'meta_key' => 'deposit_details'
        ], ['meta_value' => $payment]);

        return compact('amount', 'currency', 'payment', 'pm', 'feeinfo');
    }

    /**
     * @param $amount1
     * @param $amount2
     * @return number|string
     * @version 1.0.0
     * @since 1.0
     */
    private function toSum($amount1, $amount2)
    {
        $total = BigDecimal::of($amount1)->plus(BigDecimal::of($amount2));
        return is_object($total) ? (string)$total : $total;
    }

    /**
     * @param $gateway
     * @return mixed|array
     * @version 1.0.0
     * @since 1.0
     */
    public function payInfo($gateway, $currency, $only = true)
    {
        if (is_object($gateway)) {
            $pay_info = PaymentMethod::paymentInfo($gateway, $currency, $only);
        } else {
            $get_gateway = PaymentMethod::where('slug', $gateway)->first();
            if (!blank($get_gateway)) {
                $pay_info = PaymentMethod::paymentInfo($get_gateway, $currency, $only);
            } else {
                $pay_info = false;
            }
        }

        return $pay_info;
    }

    public function depositConfirm($request)
    {
        $depositDetails = user_temp_meta('deposit_details');

        if (
            $request->get('confirm')
            && ($request->ajax() || $request->wantsJson())
            && $depositDetails
        ) {
            return $this->wrapInTransaction(function () use ($depositDetails) {
                $hasPayInfo = $this->payInfo($depositDetails['method'], $depositDetails['currency'], true);
                if (!empty($hasPayInfo)) {
                    $transactionService = new TransactionTransactionService();
                    $transaction = $transactionService->createDepositTransaction($depositDetails);
                    $transactionProcessor = new TransactionProcessor();
                    $response = $transactionProcessor->process($transaction, Arr::get($depositDetails, 'method'));
                    $feeInfo = $transaction->fees ? "* Additional fees, network fees or intermediary fees may be deducted from the Amount Transferred by your payment provider." : null;

                    try {
                        ProcessEmail::dispatch('deposit-placed-customer', data_get($transaction, 'customer'), null, $transaction, $feeInfo);
                        ProcessEmail::dispatch('deposit-placed-admin', data_get($transaction, 'customer'), null, $transaction);
                    } catch (\Exception $e) {
                        save_mailer_log($e, 'deposit-placed');
                    }

                    $transaction->status = TransactionStatus::PENDING;
                    $transaction->save();

                    // In normal situations, $response will be returned, view() won't
                    return $response ? $response : view('user.transaction.deposit-confirm');
                } else {
                    throw new DepositTryMethodException();
                }
            });
        } else {
            throw ValidationException::withMessages(['confirm' => __('Opps! We unable to process your request. Please reload the page and try again.')]);
        }
    }

    /**
     * @param $method
     * @param $currencies
     * @return array
     * @version 1.0.0
     * @since 1.0
     */
    private function validateWithdrawAccount($method, $currencies)
    {
        if (blank($method)) {
            return MsgState::of('no-method', 'withdraw');
        }

        if (blank($currencies)) {
            return MsgState::of('invalid-method', 'withdraw');
        }

        if (!request()->is('api/*')) {
            if (!$this->hasAccountBalance()) {
                return MsgState::of('no-fund', 'account');
            }
        }
    }

    public function withdrawAmount($wdm)
    {
        $source = AccType('main');
        $method = $this->wdmDetails($wdm);

        if (empty($method)) {
            throw new WithdrawInvalidMethodException();
        }

        $accounts = $this->getUserAccounts($wdm);
        $currencies = $this->getCurrenciesData($method, false, 'withdraw');

        $errors = $this->validateWithdrawAccount($method, $currencies);
        if (!empty($errors)) {
            throw new WithdrawInvalidAccountException('', $errors);
        }

        $rates = $this->getCurrenciesData($method, false, 'withdraw', 'rate');
        $balance = $this->getAccountBalance($source, true);

        $tempMetas = [
            'wd_source' => $source,
            'withdraw_method' => $method,
            'wd_currencies_' . $method->slug  => array_keys($currencies),
        ];

        foreach ($tempMetas as $key => $meta) {
            UserTempMeta::updateOrCreate([
                'user_id' => auth()->id(),
                'meta_key' => $key,
            ], ['meta_value' => $meta]);
        }

        return compact('method', 'accounts', 'currencies', 'rates', 'balance');
    }

    public function withdrawPreview($request)
    {
        $accountID = get_hash($request->get('wd_account'));
        $wdmAccount = UserAccount::find($accountID);
        $currency = strtoupper($request->get('wd_currency', $this->basecur));
        $amount_bs = (float)$request->get('wd_amount');
        $wddesc = strip_tags($request->get('wd_desc') ?? '');
        $amount_wd = (float)$request->get('wd_amount_to');
        $currency_wd = strtoupper($request->get('wd_currency_to'));
        $input_by = $request->get('wd_amount_by', 1);
        $slug = data_get($wdmAccount, 'slug');
        $wdm = (!blank($wdmAccount)) ? $this->wdmDetails($slug) : null;
        $wdmcur = (!blank($wdmAccount)) ? $wdmAccount->account_currency : false;
        $user = auth()->user();
        $currencies = user_temp_meta('wd_currencies_' . $slug, [], $user);
        $currencies = (!blank($wdm)) ? array_intersect($currencies, $wdm->currencies) : $currencies;
        $rates = (!blank($wdm)) ? $this->getCurrenciesData($wdm, false, 'withdraw') : array();
        // recheck error
        if (blank($wdmAccount)) {
            throw ValidationException::withMessages([
                'wd_account' => ['title' => __('Account may not valid or found!'), 'message' => __('Selected account is no longer available. Please choose another account and try again.')]
            ]);
        } elseif (!($currency_wd === $wdmcur)) {
            throw ValidationException::withMessages([
                'wd_account' => ['title' => __('Account currency does not support!'), 'message' => __('Please update currency on your withdraw account from profile and then try again.')]
            ]);
        }
        if ((!$currency === $this->basecur) || !in_array($currency_wd, $currencies) || !(request()->ajax() || request()->acceptsJson() || request()->wantsJson()) || !isset($rates[$currency]) || !isset($rates[$currency_wd])) {
            $msgof = (blank($wdm) || !($currency === $this->basecur)) ? 'invalid-method' : 'wrong';
            $errors = MsgState::of($msgof, 'withdraw');
            throw new WithdrawWrongException('', $errors);
        }

        $type = (is_crypto($currency)) ? 'crypto' : 'fiat';
        $type_to = (is_crypto($currency_wd)) ? 'crypto' : 'fiat';
        $rounded = is_crypto($currency) ? $this->rounded->$type : $this->rounded->fiatmax;

        $source = $request->get('wd_source', AccType('main'));
        $account = $this->getAccountBalance($source);
        $balance = $this->getAccountBalance($source, true);

        if ($input_by == 2) {
            $amount_to = round($amount_wd, $this->specifyRounded($wdm, $currency_wd));
            $amount = round(get_fx_rate($currency_wd, $currency, $amount_to), $rounded);
        } else {
            $amount = round($amount_bs, $this->specifyRounded($wdm, $currency));
            $amount_to = round(get_fx_rate($currency, $currency_wd, $amount), $this->rounded->$type_to);
            $amount = round($amount_bs, $rounded);
        }

        $wfx = $rates[$currency_wd];
        $currency_to = $currency_wd;
        $amount_min = round(get_fx_rate($currency_to, $currency, $wfx['min']), $rounded);
        $amount_max = round(get_fx_rate($currency_to, $currency, $wfx['max']), $rounded);

        // amount validation
        if (BigDecimal::of($amount)->compareTo($amount_min) == -1) {
            throw ValidationException::withMessages([
                'wd_amount' => __("The minimum amount of :amount (:from) is required to withdraw.", ['amount' => money($wfx['min'], $currency_to), 'from' => money($amount_min, $currency)])
            ]);
        }

        if (!empty($amount_max) && BigDecimal::of($amount)->compareTo($amount_max) == 1) {
            throw ValidationException::withMessages([
                'wd_amount' => __("You can withdraw maximum :amount (:from) in a single request.", ['amount' => money($wfx['max'], $currency_to), 'from' => money($amount_max, $currency)])
            ]);
        }
        if (BigDecimal::of($amount)->compareTo($balance) > 0) {
            throw ValidationException::withMessages(['wd_amount' => ['title' => __('Insufficient balance!'), 'message' => __('The amount exceeds your current balance.')]]);
        }

        $exchange = BigDecimal::of($wfx['rate']);
        if (BigDecimal::of($exchange)->compareTo(0) != 1) {
            throw new WithdrawNoRateException();
        }
        $exchange = is_object($exchange) ? (string)$exchange : $exchange;
        $fees = fees_calc($wdm, $amount_to, $currency_to, true);

        $amount_total = $amount_to;
        $amount_fees = Arr::get($fees, 'total', 0);
        $amount_only = $this->toSub($amount_total, $amount_fees);

        $base_total  = $amount;
        $base_fees   = get_fx_rate($currency_to, $currency, $amount_fees);
        $base_amount = $this->toSub($base_total, $base_fees);

        // amount negetive validation
        if (
            BigDecimal::of($base_total)->compareTo(0) <= 0 || BigDecimal::of($base_amount)->compareTo(0) <= 0 ||
            BigDecimal::of($amount_total)->compareTo(0) <= 0 || BigDecimal::of($amount_only)->compareTo(0) <= 0
        ) {
            $errors = MsgState::of('amount', 'withdraw');
            throw new WithdrawAmountValidationException('', $errors);
        }

        $withdraw = [
            'method' => $wdm->slug,
            'method_name' => ($wdm->slug === 'wd-crypto-wallet') ? get_currency($currency_to, 'name') . ' Wallet' : $wdm->title,
            'currency' => $currency_to,
            'currency_name' => get_currency($currency_to, 'name'),
            'amount' => $amount_only,
            'amount_fees' => $amount_fees,
            'total' => $amount_total,
            'base_amount' => $base_amount,
            'base_fees' => $base_fees,
            'base_total' => $base_total,
            'base_currency' => $currency,
            'exchange_rate' => $exchange,
            'pay_to' => $this->payAccount($wdmAccount, $wdm, $currency_to, true),
            'pay_meta' => $this->payAccount($wdmAccount, $wdm, $currency_to, false),
            'unote' => ($wddesc) ? $wddesc : '',
            'source' => $source,
            'account' => $wdmAccount->account_name,
            'fx_rate' => $exchange,
            'fx_currency' => $currency_to,
            'fees' => $fees,
        ];

        $feeinfo = $amount_fees ? get_fs_tip($withdraw) : '';

        UserTempMeta::updateOrCreate([
            'user_id' => $user->id,
            'meta_key' => 'wd_details'
        ], ['meta_value' => $withdraw]);

        return compact('withdraw', 'wdm', 'amount', 'currency', 'feeinfo');
    }

    public function withdrawConfirm($request)
    {
        $user = auth()->user();
        $withdrawDetails = user_temp_meta('wd_details', null, $user);

        if ($request->get('confirm') && ($request->ajax() || $request->wantsJson()) && $withdrawDetails) {
            return $this->wrapInTransaction(function ($withdrawDetails, $userId) {
                $transactionService = new TransactionTransactionService();
                $transaction = $transactionService->createWithdrawTransaction($withdrawDetails);

                $userAccount = get_user_account($userId);
                $amount = BigDecimal::of($userAccount->amount)->minus(BigDecimal::of($transaction->total));
                $userAccount->amount = $amount;

                if ($amount->toFloat() < 0) {
                    throw ValidationException::withMessages([
                        'wd_amount' => __('You do not have sufficient balance to make withdraw.')
                    ]);
                }

                $userAccount->save();

                $transaction->status = TransactionStatus::PENDING;
                $transaction->save();

                $feeInfo = $transaction->fees ? "* Additional fees, network fees or intermediary fees may be deducted from the Amount Transferred by your payment provider." : null;

                try {
                    ProcessEmail::dispatch('withdraw-request-customer', data_get($transaction, 'customer'), null, $transaction, $feeInfo);
                    ProcessEmail::dispatch('withdraw-request-admin', data_get($transaction, 'customer'), null, $transaction);
                } catch (\Exception $e) {
                    save_mailer_log($e, 'withdraw-request');
                }
                return $transaction;
            }, $withdrawDetails, $user->id);
        } else {
            throw ValidationException::withMessages(['confirm' => __('Opps! We unable to process your request. Please reload the page and try again.')]);
        }
    }

    public function finalTransactionUpdate($transaction, $status, $online = false)
    {
        if (blank($transaction)) {
            throw new TnxErrorException(__('The transaction may invalid or not found!'));
        }

        if ($transaction->status == TransactionStatus::FAILED && $transaction->type == TransactionType::DEPOSIT) {
            throw new DepositWrongException();
        }

        if (in_array($transaction->status, [
            TransactionStatus::COMPLETED,
            TransactionStatus::CONFIRMED,
            TransactionStatus::CANCELLED
        ]) && !$online) {
            throw new TnxInvalidActionException('invalid-action', MsgState::of('invalid-action', $transaction->type));
        }

        if (!in_array($status, ['success', 'cancel'])) {
            throw new TnxInvalidStatusException();
        }

        $etype = false;
        if ($transaction->type == TransactionType::DEPOSIT) {
            $etype = 'deposit';
        } elseif ($transaction->type == TransactionType::WITHDRAW) {
            $etype = 'withdraw';
        }

        if ($transaction->type == TransactionType::WITHDRAW && $status == 'success') {
            throw new WithdrawInvalidActionException('invalid-action', MsgState::of('invalid-action', 'withdraw'));
        }

        if ($status == 'cancel' && !$online) {
            if ($transaction->is_cancellable) {
                $transaction->status = TransactionStatus::CANCELLED;
                $transaction->save();

                if ($transaction->type == TransactionType::WITHDRAW) {
                    $userAccount = get_user_account($transaction->user_id);
                    $userAccount->amount = BigDecimal::of($userAccount->amount)->plus(BigDecimal::of($transaction->total));
                    $userAccount->save();
                }

                try {
                    if ($etype) {
                        ProcessEmail::dispatch($etype . '-cancel-user-customer', data_get($transaction, 'customer'), null, $transaction);
                        ProcessEmail::dispatch($etype . '-cancel-user-admin', data_get($transaction, 'customer'), null, $transaction);
                    }
                } catch (\Exception $e) {
                    save_mailer_log($e, $etype);
                }
            } else {
                throw new TnxCancelTimeoutException('cancel-timeout', MsgState::of('cancel-timeout', $transaction->type));
            }
        }

        return [
            'transaction' => $transaction,
            'contentBlade' => $transaction->type . '-' . $status,
        ];
    }
}
