<?php

/**
 * Copyright © Resurs Bank AB. All rights reserved.
 * See LICENSE for license details.
 */

declare(strict_types=1);

namespace Resursbank\MerchantApi\Service;

use Carrier;
use Configuration;
use Db;
use DbQuery;
use JsonException;
use Order as PrestaOrder;
use OrderHistory;
use OrderPayment;
use OrderState;
use PrestaShopDatabaseException;
use PrestaShopException;
use ReflectionException;
use Resursbank\Ecom\Exception\ApiException;
use Resursbank\Ecom\Exception\AttributeCombinationException;
use Resursbank\Ecom\Exception\AuthException;
use Resursbank\Ecom\Exception\ConfigException;
use Resursbank\Ecom\Exception\CurlException;
use Resursbank\Ecom\Exception\PaymentActionException;
use Resursbank\Ecom\Exception\Validation\EmptyValueException;
use Resursbank\Ecom\Exception\Validation\IllegalTypeException;
use Resursbank\Ecom\Exception\Validation\IllegalValueException;
use Resursbank\Ecom\Exception\Validation\NotJsonEncodedException;
use Resursbank\Ecom\Exception\ValidationException;
use Resursbank\Ecom\Lib\Model\Payment;
use Resursbank\Ecom\Lib\Model\Payment\Order\ActionLog\OrderLine;
use Resursbank\Ecom\Lib\Model\Payment\Order\ActionLog\OrderLineCollection;
use Resursbank\Ecom\Lib\Order\OrderLineType;
use Resursbank\Ecom\Module\Payment\Repository;
use Resursbank\MerchantApi\Exception\InvalidArgumentException;
use Resursbank\MerchantApi\Exception\InvalidDataException;
use Resursbank\MerchantApi\Repository\ResursbankOrderRepositoryFrontend;
use Resursbank\MerchantApi\Util\Log;
use StockAvailable;
use Throwable;

class Order
{
    /**
     * Custom order status which defines "redirected customer to gateway".
     */
    public const ORDER_STATE_REDIRECTED = 'PS_OS_RESURSBANK_REDIRECTED';

    /**
     * Custom order status which defines "waiting for authorization callback".
     */
    public const ORDER_STATE_PENDING = 'PS_OS_RESURSBANK_PENDING';

    /**
     * Custom order status which defines "payment is under review".
     */
    public const ORDER_STATE_PAYMENT_REVIEW = 'PS_OS_RESURSBANK_PAYMENT_REVIEW';

    /**
     * Custom order status which defines "order has been paid".
     */
    public const ORDER_STATE_PAID = 'PS_OS_RESURSBANK_PAID';

    /**
     * Native status reflecting payment cancellation.
     */
    public const ORDER_STATE_CANCELED = 'PS_OS_CANCELED';

    /**
     * Native order status reflecting payment refund.
     */
    public const ORDER_STATE_REFUNDED = 'PS_OS_REFUND';

    /**
     * @param PrestaOrder $order
     * @param array|null $productList
     * @return OrderLineCollection
     * @throws AttributeCombinationException
     * @throws IllegalTypeException
     * @throws InvalidDataException
     * @throws JsonException
     * @throws ReflectionException
     */
    public static function convert(
        PrestaOrder $order,
        ?array $productList = null
    ): OrderLineCollection {
        $data = [];

        $products = $productList !== null ?
            $productList : $order->getProducts();

        foreach ($products as $product) {
            if (!self::validateProduct(product: $product)) {
                Log::debug(message: 'Invalid product data.');
                Log::debug(message: json_encode(value: $product));
                throw new InvalidDataException(message: 'Invalid product data.');
            }

            $data[] = new OrderLine(
                quantity: (float)$product['product_quantity'] ?? 0.0,
                quantityUnit: 'st',
                vatRate: round(
                    num: (float)$product['tax_rate'] ?? null,
                    precision: 2
                ),
                totalAmountIncludingVat: round(
                    num: (float)$product['total_price_tax_incl'] ?? 0.0,
                    precision: 2
                ),
                description: (string)$product['product_name'] ?? null,
                reference: (string)$product['reference'] ?? null,
                type: OrderLineType::NORMAL,
                unitAmountIncludingVat: round(
                    num: (float)$product['unit_price_tax_incl'] ?? 0.0,
                    precision: 2
                ),
                totalVatAmount: round(
                    num: (float)$product['total_price_tax_incl'] - (float)$product['total_price_tax_excl'],
                    precision: 2
                )
            );
        }

        self::addShippingLine(data: $data, order: $order);
        self::addDiscountLine(data: $data, order: $order);

        return new OrderLineCollection(data: $data);
    }

    /**
     * Convert refund data.
     *
     * This method uses the convert method but first remaps some data from the
     * product list included in the actionOrderSlipAdd parameters object.
     *
     * @param PrestaOrder $order
     * @param array $productList
     * @return OrderLineCollection
     * @throws AttributeCombinationException
     * @throws IllegalTypeException
     * @throws InvalidDataException
     * @throws JsonException
     * @throws ReflectionException
     */
    public static function convertRefund(
        PrestaOrder $order,
        array $productList
    ): OrderLineCollection {
        $updatedProducts = [];
        foreach ($order->getProducts() as $product) {
            if (!array_key_exists(key: $product['id_order_detail'], array: $productList)) {
                continue;
            }

            $product['product_quantity'] = $productList[$product['id_order_detail']]['quantity'];
            $product['total_price_tax_incl'] = $productList[$product['id_order_detail']]['total_refunded_tax_incl'];
            $product['total_price_tax_excl'] = $productList[$product['id_order_detail']]['total_refunded_tax_excl'];
            $updatedProducts[$product['id_order_detail']] = $product;
        }

        return self::convert(
            order: $order,
            productList: $updatedProducts,
        );
    }

    /**
     * Cancel an order programmatically and restock products.
     */
    public static function cancel(PrestaOrder $order): void
    {
        // Update order status to "canceled"
        $orderHistory = new OrderHistory();
        $orderHistory->id_order = $order->id;
        $orderHistory->changeIdOrderState(
            new_order_state: Configuration::get(key: self::ORDER_STATE_CANCELED),
            id_order: $order->id
        );
        $orderHistory->add();

        // Restock products
        foreach ($order->getProducts() as $orderProduct) {
            StockAvailable::updateQuantity(
                id_product: $orderProduct['product_id'],
                id_product_attribute: $orderProduct['product_attribute_id'],
                delta_quantity: $orderProduct['product_quantity'],
                id_shop: $order->id_shop
            );
        }
    }

    /**
     * Add shipping line to converted data.
     *
     * @param array $data
     * @param PrestaOrder $order
     * @return void
     * @throws AttributeCombinationException
     * @throws JsonException
     * @throws ReflectionException
     */
    public static function addShippingLine(
        array &$data,
        PrestaOrder $order
    ): void {
        $carrier = new Carrier(id: $order->id_carrier);
        $total = round(num: (float)$order->total_shipping_tax_incl, precision: 2);
        $vatRate = round(num: (float)$order->carrier_tax_rate, precision: 2);

        // Add shipping cost line, if any
        if ($total > 0) {
            $data[] = new OrderLine(
                quantity: 1.0,
                quantityUnit: 'st',
                vatRate: $vatRate,
                totalAmountIncludingVat: $total,
                description: (string)$carrier->name,
                reference: (string)$carrier->id,
                type: OrderLineType::SHIPPING,
                unitAmountIncludingVat: $total,
                totalVatAmount: round(num: $total * ($vatRate / (100 + $vatRate)), precision: 2)
            );
        }
    }

    /**
     * Add discount line to converted data.
     *
     * @param array $data
     * @param PrestaOrder $order
     * @return void
     * @throws AttributeCombinationException
     * @throws JsonException
     * @throws ReflectionException
     */
    public static function addDiscountLine(
        array &$data,
        PrestaOrder $order
    ): void {
        // Resolve discount totals.
        $totalInclTax = (float)$order->total_discounts_tax_incl;
        $totalExclTax = (float)$order->total_discounts_tax_excl;

        // Add discount line, if any.
        if ($totalInclTax > 0) {
            $data[] = new OrderLine(
                quantity: 1.0,
                quantityUnit: 'st',
                vatRate: round(
                    num: (($totalInclTax - $totalExclTax) / $totalExclTax) * 100,
                    precision: 2
                ),
                totalAmountIncludingVat: round(num: -$totalInclTax, precision: 2),
                description: 'Discount',
                reference: 'DISCOUNT',
                type: OrderLineType::DISCOUNT,
                unitAmountIncludingVat: round(num: -$totalInclTax, precision: 2),
                totalVatAmount: -round(num: $totalInclTax - $totalExclTax, precision: 2)
            );
        }
    }

    public static function validateProduct(mixed $product): bool
    {
        return is_array(value: $product) &&
            isset(
                $product['product_quantity'],
                $product['product_name'],
                $product['total_price_tax_excl'],
                $product['total_price_tax_incl'],
                $product['unit_price_tax_incl'],
                $product['tax_rate'],
                $product['reference']
            );
    }

    /**
     * Link a captured payment from Resurs Bank to its corresponding PrestaShop invoice.
     *
     * This method assumes that the invoice has already been created by PrestaShop,
     * typically when the order reaches a paid or invoiced state.
     * If the invoice does not exist yet, we throw an exception rather than
     * creating one manually, to avoid breaking numbering and tax consistency.
     *
     * @param Payment $payment The Resurs Bank payment model.
     * @param PrestaOrder $order The PrestaShop order object.
     * @param float $capturedAmount The total captured amount for this transaction.
     *
     * @throws PrestaShopException     If the payment or invoice cannot be created or resolved.
     */
    public static function createInvoice(
        Payment $payment,
        PrestaOrder $order,
        float $capturedAmount
    ): void {
        $orderState = new OrderState(id: (int)$order->getCurrentState());
        $stateName = $orderState->name[(int)Configuration::get(key: 'PS_LANG_DEFAULT')] ?? reset(array: $orderState->name);
        // Check if we are allowed to create invoices before doing that part.
        if ((int)$orderState->invoice !== 1) {
            Log::debug(
                message: sprintf(
                    'Order state "%s" (ID %d) does not trigger invoice creation. Skipping createInvoice() for order #%d.',
                    $stateName,
                    $orderState->id,
                    $order->id
                )
            );
            return;
        }


        // Create and persist a new OrderPayment object for this transaction.
        $orderPayment = new OrderPayment();
        $orderPayment->order_reference = $order->reference;
        $orderPayment->id_currency = $order->id_currency;
        $orderPayment->amount = $capturedAmount;
        $orderPayment->payment_method = $payment->paymentMethod?->name ?? 'Resurs Bank';
        $orderPayment->transaction_id = $payment->id;
        $orderPayment->date_add = date(format: 'Y-m-d H:i:s');

        if (!$orderPayment->add()) {
            throw new PrestaShopException(message: 'Failed to create payment record in PrestaShop.');
        }

        // Try to fetch the first invoice related to this order.
        // If no invoice exists yet, the order has likely not reached
        // a status that triggers invoice generation.
        $invoice = $order->getInvoicesCollection()->getFirst();

        // If there's still no invoice, we stop here — we don't create one manually.
        if ($invoice === null) {
            throw new PrestaShopException(
                message: sprintf(
                    'No invoice found for order #%d (reference %s). The invoice must exist before linking a payment.',
                    $order->id,
                    $order->reference
                )
            );
        }

        // Some OrderInvoice objects can exist in memory but not be saved yet.
        // Ensure the object has an ID before linking.
        if (isset($invoice->id)) {
            // Link the persisted invoice to the created payment if it exists, otherwise skip it.
            // PrestaShop provides no helper for this association, so we write to the pivot table directly.
            Db::getInstance()->insert(
                table: 'order_invoice_payment',
                data: [
                    'id_order_invoice' => (int)$invoice->id,
                    'id_order_payment' => (int)$orderPayment->id,
                    'id_order' => (int)$order->id,
                ]
            );
        }
    }

    /**
     * Check if an order is a Resurs Bank order.
     *
     * @param string $orderReference
     * @return bool
     */
    public static function isResursbank(string $orderReference): bool
    {
        try {
            $query =
                'select ro.* from ' . _DB_PREFIX_ . 'resursbank_order ro ' .
                'inner join ' . _DB_PREFIX_ . 'orders o on o.id_order = ro.order_id ' .
                'where o.reference = "' . $orderReference . '"';
            $result = Db::getInstance()->query(sql: $query);
            $fetchedResult = Database::getAll(result: $result);

            return count(value: $fetchedResult) > 0;
        } catch (Throwable $error) {
            Log::error(message: $error);
        }

        return false;
    }

    /**
     * Check if modification of payment is possible.
     *
     * @param Payment $payment
     * @param PrestaOrder $order
     * @return bool
     */
    public static function modificationPossible(
        Payment $payment,
        PrestaOrder $order
    ): bool {
        $approvedCreditLimit = $payment->application->approvedCreditLimit;

        try {
            $requestedAmount = $order->getTotalPaid();

            if ($requestedAmount > $approvedCreditLimit) {
                throw new PaymentActionException(
                    message: 'Requested amount ' . $requestedAmount .
                    ' exceeds ' . $approvedCreditLimit . ' on ' . $payment->id
                );
            }

            return true;
        } catch (Throwable $error) {
            Log::error(message: $error);
            return false;
        }
    }

    /**
     * Check if an order already has a registered payment.
     *
     * @param PrestaOrder $order
     * @return bool
     */
    public static function hasPayment(PrestaOrder $order): bool
    {
        try {
            $query = new DbQuery();
            $query->select(fields: 'COUNT(*)');
            $query->from(table: 'order_payment');
            $query->where(restriction: 'order_reference = "' . pSQL(string: $order->reference) . '"');

            $count = (int)Db::getInstance()->getValue(sql: $query);
            return $count > 0;
        } catch (Throwable $error) {
            Log::error(message: $error);
            return false;
        }
    }

    /**
     * Sync state of order in PrestaShop with payment status.
     *
     * @param string $paymentId
     * @param PrestaOrder|null $order
     * @return PrestaOrder
     * @throws ApiException
     * @throws AttributeCombinationException
     * @throws AuthException
     * @throws ConfigException
     * @throws CurlException
     * @throws EmptyValueException
     * @throws IllegalTypeException
     * @throws IllegalValueException
     * @throws InvalidArgumentException
     * @throws JsonException
     * @throws NotJsonEncodedException
     * @throws PrestaShopDatabaseException
     * @throws PrestaShopException
     * @throws ReflectionException
     * @throws ValidationException
     * @noinspection
     */
    public static function syncOrderState(
        string $paymentId,
        ?PrestaOrder $order = null
    ): PrestaOrder {
        // Get Payment.
        $payment = Repository::get(paymentId: $paymentId);

        // Get PrestaShop Order.
        if ($order === null) {
            $order = ResursbankOrderRepositoryFrontend::getOrderByMapiId(mapiId: $paymentId);
        }

        if ($order === null) {
            throw new InvalidArgumentException(message: "Order not found for payment ID: $paymentId");
        }

        // Update order status based on payment status.
        if ($payment->isCancelled()) {
            self::cancel(order: $order);
        } elseif ($payment->isFrozen()) {
            $order->setCurrentState(id_order_state: Configuration::get(key: self::ORDER_STATE_PAYMENT_REVIEW));
        } elseif ($payment->isCaptured() && $order->getCurrentState() != Configuration::get(key: self::ORDER_STATE_PAID)) {

            // Only update once in a lifetime.
            if (!self::hasEverHadStatus(
                order: $order,
                statusId: (int)Configuration::get(key: self::ORDER_STATE_PAID))
            ) {
                // Make sure we don't double captured payments.
                $order->setCurrentState(id_order_state: Configuration::get(key: self::ORDER_STATE_PAID));

                // Save order state entirely before creating invoice.
                $order->save();
            }

            if (!Order::hasPayment(order: $order)) {
                // Create invoice in PrestaShop to reflect captured amount.
                Order::createInvoice(
                    payment: $payment,
                    order: $order,
                    capturedAmount: $payment->order->capturedAmount
                );
            }
        } elseif ($payment->canCapture()) {
            $order->setCurrentState(id_order_state: Configuration::get(key: self::ORDER_STATE_PENDING));
        }

        $order->save();

        return $order;
    }

    /**
     * Check if an order has ever been in a specific state.
     */
    public static function hasEverHadStatus(PrestaOrder $order, int $statusId): bool
    {
        $query = new DbQuery();
        $query->select(fields: 'COUNT(*)');
        $query->from(table: 'order_history');
        $query->where(restriction: 'id_order = ' . (int)$order->id);
        $query->where(restriction: 'id_order_state = ' . (int)$statusId);

        return (bool)Db::getInstance()->getValue(sql: $query);
    }
}
