<?php
/**
 * Class Refund
 *
 * @package Mtc\Plugins\Refunds\Classes
 * @author Martins Fridenbergs <martins.fridenbergs@mtcmedia.co.uk>
 */
namespace Mtc\Plugins\Refunds\Classes;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Capsule\Manager as DB;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Event;
use Mtc\Plugins\EvoPay\Classes\EvoPay;
use Mtc\Shop\Coupon;
use MTC\Shop\Order;
use Mtc\Shop\Order\Note;

/**
 * Class Refund
 *
 * Refund object model.
 * Links with order and identifies that this order has been (fully or partially) refunded
 *
 *
 * @package Mtc\Plugins\Refunds\Classes
 * @author Martins Fridenbergs <martins.fridenbergs@mtcmedia.co.uk>
 */
class Refund extends Model
{
    /**
     * The table associated with the model.
     *
     * @var string
     */
    protected $table = 'order_refunds';

    /**
     * Mass assignable attributes
     *
     * @var string[]
     */
    protected $fillable = [
        'order_id',
        'delivery_refund_amount',
        'reference',
        'note',
        'created_at',
        'updated_at',
    ];

    /**
     * Relationship to items of this refund
     *
     * @return \Illuminate\Database\Eloquent\Relations\HasMany
     */
    public function items()
    {
        return $this->hasMany(RefundItem::class, 'refund_id');
    }

    /**
     * Render payment form.
     * The form is used in basket overview step to offer user payment through this gateway.
     *
     * @param \Basket $basket current users basket
     * @return string the payment form template
     */
    public static function adminButton(\Order $shop_order)
    {
        $order = self::prepareRefundDataObject($shop_order->getId());

        // We were unable to find the order, something went wrong, return empty string as there isn't anything to render
        if (!$order) {
            return '';
        }

        $order->errors = [];
        $order->refund_method = $order->refund_note = $order->refund_reference = $order->refund_template = '';
        // Pull subtotal and total for the order for display purposes
        $order->cost_subtotal = $shop_order->getSubTotalCost();

        // We only render the refund button when we should allow returns
        // i.e. when refunded amount is less than order total and the order has not been marked as fully refunded
        if ($order->already_refunded_amount <= $order->cost_total && $order->status !== 6) {
            $twig = App::make('twig');
            return $twig->render('Refunds\admin\form.twig', [
                'order' => $order,
                'refund_methods' => collect(Event::dispatch('refunds.get_refund_methods', $shop_order))->filter(),
                'templates' => \Mtc\Shop\EmailTemplate::all(),
                'request' => $_REQUEST
            ]);
        }

        // Nothing to render, return empty string
        return '';
    }

    /**
     * Find the delivery amount already refunded for an order
     *
     * @param int $order_id order id
     * @return float delivery amount alreadey refunded
     */
    public static function deliveryRefundedForOrder($order_id)
    {
        return self::where('order_id', $order_id)->sum('delivery_refund_amount');
    }


    /**
     * Simple method for confirming that all required components are on site.
     * This way we ensure that there are no missing dependencies.
     *
     * @param string $class_to_confirm Class name from the required component
     * @param string $module_name verbose component name that explains what is missing
     * @throws \Exception
     */
    public static function confirmComponentExists($class_to_confirm, $module_name)
    {
        if (!class_exists($class_to_confirm)) {
            throw new \Exception("{$module_name} is required by Refund Plugin");
        }
    }

    /**
     * Prepare order object from ID
     *
     * @param int $order_id order id
     * @return false|Order
     */
    public static function prepareRefundDataObject($order_id)
    {
        // Find eloquent object for this order as eloquent is easier to work with
        $order = Order::with([
            'items',
            'discounts'
        ])->find($order_id);

        if (!$order) {
            return false;
        }

        // Map order items to add additional info we need to know
        $order->items->map(function ($item, $key) {
            // Find the amount of this line item that has already been refunded
            // As this might not be the first attempt to refund the order
            $item->already_refunded_item_qty = RefundItem::getRefundedLineItemQty($item->id);

            // Check and set a flag is this order item line fully refunded
            $item->is_fully_refunded = $item->already_refunded_item_qty == $item->quantity;

            // Calculate the amount that has already been refunded for this line item
            $item->refunded_amount_per_line = RefundItem::getRefundedLineItemAmount($item->id);

            // Set the maximal quantity we can allow to refund for this item
            // This cannot exceed the overall order item quantity and must exclude items already refunded
            // We're also setting quantity_to_refund as maximal allowed amount upon initialization
            $item->max_quantity_to_refund = $item->quantity - $item->already_refunded_item_qty;
            $item->quantity_to_refund = $item->max_quantity_to_refund;

            // Set the amount that would be refunded based on the price paid per item nad item quantity
            $item->to_refund_amount = $item->quantity_to_refund * $item->price_paid;

            $item->setHidden([
                'timestamp',
                'assessment_id',
                'item_price_exvat',
                'colour',
                'tosend',
                'sent',
                'vat_deducatable'
            ]);
            return $item;
        });

        // Find and set the already refunded delivery costs
        $order->delivery_amount_refunded = self::deliveryRefundedForOrder($order->id);
        $order->delivery_refund_amount = $order->delivery_refund_amount_max = $order->delivery_cost - $order->delivery_amount_refunded;
        $order->already_refunded_amount = $order->delivery_amount_refunded + $order->items->sum('refunded_amount_per_line');

        return $order;
    }

    /**
     * Generate coupon as part of the refund.
     *
     * @param int $order_id order which is being refunded
     * @param float $amount_to_refund amount to refund as coupon code
     */
    public static function createRefundCoupon(int $order_id, float $amount_to_refund)
    {
        $coupon_config = [
            'prefix' => 'RF-',
            'value' => $amount_to_refund,
            'name' => "Refund on Order {$order_id}"
        ];
        $coupon = Coupon::generateCoupon($coupon_config);

        Note::create([
            'order_id' => $order_id,
            'note' => "Coupon {$coupon->code} generated for refund on this order"
        ]);

        // TODO: Add automated email notification to recipient?
    }

    /**
     * Sends a refund request to SagePay.
     *
     * @param array $options The options to use to perform the refund request
     *
     * @return bool True if refund successful or False otherwise
     */
    public static function doSagePayRefund(array $options)
    {
        require SITE_PATH . '/shop/checkout/server/function.sagepay.php';

        $post_values = [];
        $post_values['VPSProtocol'] = SAGEPAY_PROTOCOL;
        $post_values['TxType'] = 'REFUND';
        $post_values['Vendor'] = SAGEPAY_VENDOR;
        $post_values['VendorTxCode'] = $options['reference'];
        $post_values['Amount'] = $options['amount'];
        $post_values['Currency'] = SAGEPAY_CURRENCY;
        $post_values['Description'] = $options['description'];
        $post_values['RelatedVPSTxId'] = $options['related_VPSTxId'];
        $post_values['RelatedVendorTxCode'] = $options['related_VendorTxCode'];
        $post_values['RelatedSecurityKey'] = $options['related_SecurityKey'];
        $post_values['RelatedTxAuthNo'] = $options['related_TxAuthNo'];

        $strPost = implode('&', array_map(function ($value, $key) {
            return "$key=$value";
        }, $post_values, array_keys($post_values)));

        $response = requestPost($strRefundURL, $strPost);

        DB::table('sagepay_responses')->insert([
            'date' => date('Y-m-d H:i:s'),
            'VendorTxCode' => $options['reference'],
            'request' => $strPost,
            'StatusDetail' => $response['StatusDetail'],
            'response' => serialize($response),
        ]);

        if (!empty($options['order_id'])) {
            DB::table('order_note')->insert([
                'order_id' => $options['order_id'],
                'note' => 'A SagePay refund request for £' . $options['amount'] . ' has been sent and the following response has been received: ' . $response['StatusDetail'],
            ]);
        }

        return $response['Status'] === 'OK';
    }

    /**
     * Do a SagePay refund. This is used as a callback when doing a refund
     * through the admin.
     *
     * @param int   $order_id
     * @param float $amount_to_refund
     *
     * @return bool True if successful or False otherwise
     */
    public static function refundSagePay(int $order_id, float $amount_to_refund)
    {
        $order = new \Order($order_id);

        $reference_base = $order->payment['VendorTxCode'] . '_REFUND';
        $reference = $reference_base;
        $i = 1;
        while (DB::table('sagepay_responses')->where('VendorTxCode', $reference)->count()) {
            $reference = $reference_base . '_' . ++$i;
        }

        $options = [
            'order_id' => $order_id,
            'reference' => $reference,
            'amount' => $amount_to_refund,
            'description' => 'Refund initiated by admin',
            'related_VPSTxId' => $order->payment['VPSTxId'],
            'related_VendorTxCode' => $order->payment['VendorTxCode'],
            'related_SecurityKey' => DB::table('basket_protx')->select('secKey')->where('basket_id', $order->getBasketId())->first()->secKey,
            'related_TxAuthNo' => $order->payment['TxAuthNo'],
        ];

        return self::doSagePayRefund($options);
    }
}
