<?php

/**
 * GForce Delivery Module for NGauge/Yii2
 *
 * @category    PortalPeople
 * @package     GForce
 * @author      Portal People Development Team
 * @copyright   Copyright (c) 2026 Portal People (https://portalpeople.co.uk)
 */

namespace PortalPeople\gforce;

use Yii;
use yii\base\Module as BaseModule;
use backend\components\Security;
use backend\models\{Cartsbe, Cartlines};
use common\events\UserEvent;
use PortalPeople\gforce\models\products\Gforce as GForceProductData;
use yii\console\Application as ConsoleApplication;
use yii\base\Event;
use yii\db\{ActiveRecord, AfterSaveEvent};
use yii\web\View;

/**
 * GForce Delivery Module
 *
 * Provides integration with the GForce REST API for creating,
 * managing and tracking pallet consignments.
 *
 * Configuration (backend/config/params-local.php):
 * ```php
 * 'plugins' => [
 *     'gforce' => [
 *         'enabled'     => true,
 *         'apiUrl'      => 'https://api-uat.vigocloud.com/api/v1',
 *         'apiKey'      => '<encrypted>',
 *         'autoConfirm' => false,
 *         'debug'       => false,
 *     ],
 * ],
 * ```
 */
class Module extends BaseModule implements \yii\base\BootstrapInterface
{
    private const EVENT_STOCK_ADJUSTMENT = 'gforce-stock-adjustment';

    private const EVENT_ORDER_BECAME_PAID = 'gforce-order-became-paid';

    private const EVENT_CARTLINE_HAULIER_UPDATED = 'gforce-cartline-haulier-updated';

    /** @var string Yii2 controller namespace. */
    public $controllerNamespace = 'PortalPeople\gforce\controllers';

    /** @var string GForce REST API base URL. */
    public $apiUrl = 'https://api-uat.vigocloud.com/api/v1';

    /** @var string Customer API key issued by GForce IT. */
    public $apiKey;

    /** @var bool Automatically confirm consignments into the depot system (Confirm=yes). */
    public $autoConfirm = false;

    /** @var int HTTP request timeout in seconds. */
    public $timeout = 30;

    /** @var string Default collection country code used for route/service lookups. */
    public $defaultCollectionCountry = 'GBR';

    /** @var string Default collection postcode used for route/service lookups. */
    public $defaultCollectionPostcode = '';

    /** @var bool Enable debug logging. */
    public $debug = false;

    /** @var string URL rule class. */
    public $urlRuleClass = 'yii\web\UrlRule';

    /**
     * Bootstrap: register URL rules.
     * @param \yii\base\Application $app
     */
    public function bootstrap($app)
    {
        $this->id = 'gforce';
        self::init();

        if (!isset($app->getModules()[$this->id])) {
            $app->setModule($this->id, $this);
        }

        if (Yii::$app->name !== 'NGaugeApi') {
            $app->urlManager->addRules([
                "{$this->id}/<action:[\w\-]+>" => "{$this->id}/config/<action>",
                "{$this->id}/<controller:[\w\-]+>/<action:[\w\-]+>" => "{$this->id}/<controller>/<action>",
            ], false);
        }
        Event::on(\backend\models\CartsData::class, 'carts-set-data-types', function ($event) {
            $event->customData['gforce'] = 'gforce';
        });

        $app->on(
            'carts-after-DataLoad-gforce',
            function ($event) {
                $event->result = \PortalPeople\gforce\models\carts\GForce::class;
            }
        );

        Event::on(
            View::class,
            'cart-shipping-tab',
            function ($event) use ($app) {
                echo $app->view->render(
                    '@PortalPeople/gforce/views/cart/viewtabs/_gforce_integration_tab',
                    ['model' => $event->model]
                );
            }
        );


        $app->on(self::EVENT_STOCK_ADJUSTMENT, function (UserEvent $event) {
            $this->handleStockAdjustmentEvent($event);
        });

        Event::on(Cartsbe::class, ActiveRecord::EVENT_AFTER_INSERT, function (AfterSaveEvent $event) use ($app) {
            $cart = $event->sender;
            if (!$cart instanceof Cartsbe) {
                return;
            }

            if (!$this->didCartBecomePaid($cart, $event->changedAttributes)) {
                return;
            }

            $app->trigger(
                self::EVENT_STOCK_ADJUSTMENT,
                new UserEvent([
                    'model' => $cart,
                    'customData' => [
                        'event' => self::EVENT_ORDER_BECAME_PAID,
                        'changedAttributes' => $event->changedAttributes,
                    ],
                ])
            );
        });

        Event::on(Cartsbe::class, ActiveRecord::EVENT_AFTER_UPDATE, function (AfterSaveEvent $event) use ($app) {
            $cart = $event->sender;
            if (!$cart instanceof Cartsbe) {
                return;
            }

            if (!$this->didCartBecomePaid($cart, $event->changedAttributes)) {
                return;
            }

            $app->trigger(
                self::EVENT_STOCK_ADJUSTMENT,
                new UserEvent([
                    'model' => $cart,
                    'customData' => [
                        'event' => self::EVENT_ORDER_BECAME_PAID,
                        'changedAttributes' => $event->changedAttributes,
                    ],
                ])
            );
        });

        Event::on(Cartlines::class, ActiveRecord::EVENT_AFTER_UPDATE, function (AfterSaveEvent $event) use ($app) {
            $cartline = $event->sender;
            if (!$cartline instanceof Cartlines) {
                return;
            }

            $oldHaulier = $this->extractHaulierBeforeUpdate($event->changedAttributes);
            $newHaulier = $this->extractHaulierFromCartline($cartline);
            if (!$this->didHaulierChange($oldHaulier, $newHaulier)) {
                return;
            }

            $app->trigger(
                self::EVENT_STOCK_ADJUSTMENT,
                new UserEvent([
                    'model' => $cartline,
                    'customData' => [
                        'event' => self::EVENT_CARTLINE_HAULIER_UPDATED,
                        'changedAttributes' => $event->changedAttributes,
                        'oldHaulier' => $oldHaulier,
                        'newHaulier' => $newHaulier,
                    ],
                ])
            );
        });
        Event::on(\modules\custom\models\ProductsData::class, 'products-set-data-types', function($event) {
            $event->buttons['gforce'] = 'gforce';
        });
        Yii::$app->on('products-after-DataLoad-gforce', function($event) {
            $event->result = \PortalPeople\gforce\models\products\Gforce::class;
        });
        Yii::$app->on('netsuite.createlabels-GFO001', function($event) {
            $model = $event->model;
            // Instantiate the service and execute immediately, returning its result.
            $service = new \PortalPeople\gforce\services\NewConsignment([], $model);
            $event->result = $service->create();
            return $event;
        });
        
    }

    /**
     * Save an ActiveRecord with retries on lock wait timeout (SQLSTATE 1205).
     */
    private function saveWithRetry($model, array $attributes = [], int $attempts = 3): void
    {
        $try = 0;
        while (true) {
            try {
                $try++;
                $model->save(false, $attributes);
                return;
            } catch (\yii\db\Exception $e) {
                $code = $e->errorInfo[1] ?? null;
                // MySQL 1205 = Lock wait timeout exceeded
                if ($code === 1205 && $try < $attempts) {
                    usleep(200000 * $try); // backoff: 200ms, 400ms, ...
                    continue;
                }
                throw $e;
            }
        }
    }

    private function handleStockAdjustmentEvent(UserEvent $event): void
    {
        $eventType = $event->customData['event'] ?? null;

        if ($eventType === self::EVENT_ORDER_BECAME_PAID && $event->model instanceof Cartsbe) {
            $this->decreaseStockForPaidCart($event->model);
            return;
        }

        if ($eventType === self::EVENT_CARTLINE_HAULIER_UPDATED && $event->model instanceof Cartlines) {
            $this->rebalanceStockForHaulierChange($event->model, $event->customData);
        }
    }

    private function didCartBecomePaid(Cartsbe $cart, array $changedAttributes): bool
    {
        if (
            $this->isPaidState(
                $changedAttributes['status'] ?? null,
                $changedAttributes['paid'] ?? null,
                $changedAttributes['billingstatus'] ?? null
            )
        ) {
            return false;
        }

        return $this->isPaidState($cart->status ?? null, $cart->paid ?? null, $cart->billingstatus ?? null);
    }

    private function isPaidState($status, $paid, $billingStatus): bool
    {
        if (is_string($status) && strcasecmp($status, 'Paid') === 0) {
            return true;
        }

        if (is_string($billingStatus) && strcasecmp($billingStatus, 'Paid') === 0) {
            return true;
        }

        return filter_var($paid, FILTER_VALIDATE_BOOLEAN);
    }

    private function didHaulierChange($oldHaulier, $newHaulier): bool
    {
        $old = $oldHaulier === null ? null : (string) $oldHaulier;
        $new = $newHaulier === null ? null : (string) $newHaulier;

        return $old !== $new;
    }

    private function extractHaulierBeforeUpdate(array $changedAttributes)
    {
        if (array_key_exists('haulierint', $changedAttributes)) {
            return $changedAttributes['haulierint'];
        }

        if (array_key_exists('data', $changedAttributes)) {
            $data = $this->normaliseData($changedAttributes['data']);
            return $this->extractHaulierFromData($data);
        }

        return null;
    }

    private function extractHaulierFromCartline(Cartlines $cartline)
    {
        if (isset($cartline->haulierint) && $cartline->haulierint !== '') {
            return $cartline->haulierint;
        }

        $data = $this->normaliseData($cartline->data ?? null);

        return $this->extractHaulierFromData($data);
    }

    private function extractHaulierFromData(array $data)
    {
        if (isset($data['haulierid']) && $data['haulierid'] !== '') {
            return $data['haulierid'];
        }

        if (isset($data['haulier_id']) && $data['haulier_id'] !== '') {
            return $data['haulier_id'];
        }

        if (isset($data['haulierint']) && $data['haulierint'] !== '') {
            return $data['haulierint'];
        }

        return null;
    }

    private function decreaseStockForPaidCart(Cartsbe $cart): void
    {
        foreach ($cart->allcartlines as $cartline) {
            if ($cartline instanceof Cartlines) {
                $this->applyStockChangeForCartline($cartline, 'decrease');
            }
        }
    }

    private function rebalanceStockForHaulierChange(Cartlines $cartline, array $payload): void
    {
        $oldHaulier = $payload['oldHaulier'] ?? null;
        $newHaulier = $payload['newHaulier'] ?? null;

        if ($oldHaulier !== null && $oldHaulier !== '') {
            $this->applyStockChangeForCartline($cartline, 'increase', $oldHaulier);
        }

        if ($newHaulier !== null && $newHaulier !== '') {
            $this->applyStockChangeForCartline($cartline, 'decrease', $newHaulier);
        }
    }

    private function applyStockChangeForCartline(Cartlines $cartline, string $direction, $haulierOverride = null): void
    {
        if ((int) ($cartline->qty ?? 0) <= 0 || !$cartline->product) {
            return;
        }

        $product = $cartline->product;
        $productData = $this->normaliseData($product->data ?? null);
        $gforceData = $this->normaliseData($productData['gforce'] ?? null);
        if ($gforceData === []) {
            return;
        }

        $cartlineData = $this->normaliseData($cartline->data ?? null);
        $haulier = $haulierOverride ?? $this->extractHaulierFromCartline($cartline);

        $gforce = new GForceProductData();
        $gforce->erpid = (string) ($gforceData['erpid'] ?? ($product->id ?? ''));
        $gforce->stock_level = (string) ($gforceData['stock_level'] ?? 0);
        if ($haulier !== null && $haulier !== '') {
            $gforceData['haulierid'] = (string) $haulier;
        }

        if ($direction === 'increase') {
            $newStock = $gforce->increaseStockValueFromCartLine(
                (int) $cartline->qty,
                $product,
                $haulier,
                $cartlineData
            );
        } else {
            $newStock = $gforce->adjustStockValueFromCartLine((int) $cartline->qty, $product, $haulier, $cartlineData);
        }

        $gforceData['stock_level'] = (string) $newStock;
        $productData['gforce'] = $gforceData;
        $product->data = json_encode($productData);
        $this->saveWithRetry($product, ['data']);

        $cartline->data = json_encode($cartlineData);
        $this->saveWithRetry($cartline, ['data']);
    }

    private function normaliseData($value): array
    {
        if ($value instanceof \stdClass) {
            return (array) $value;
        }

        if (is_array($value)) {
            return $value;
        }

        if (is_string($value) && $value !== '') {
            $decoded = json_decode($value, true);
            return is_array($decoded) ? $decoded : [];
        }

        return [];
    }

    /**
     * @inheritdoc
     */
    public function init()
    {
        parent::init();

        if (isset(Yii::$app->params['plugins']['gforce'])) {
            $config = Yii::$app->params['plugins']['gforce'];

            $this->apiUrl      = $config['apiUrl']      ?? $this->apiUrl;
            $this->apiKey      = $config['apiKey']      ?? $this->apiKey;
            $this->autoConfirm = (bool) ($config['autoConfirm'] ?? false);
            $this->timeout     = (int) ($config['timeout'] ?? $this->timeout);
            $this->defaultCollectionCountry = (string) (
                $config['defaultCollectionCountry'] ?? $this->defaultCollectionCountry
            );
            $this->defaultCollectionPostcode = (string) (
                $config['defaultCollectionPostcode'] ?? $this->defaultCollectionPostcode
            );
            $this->debug       = (bool) ($config['debug']       ?? false);
        }
        $this->registerComponents();
        
        if (Yii::$app instanceof ConsoleApplication) {
            $this->controllerNamespace = 'PortalPeople\\gforce\\commands';
        }
    }

    /**
     * Register module-level components.
     */
    protected function registerComponents()
    {
        Yii::$app->setComponents([
            'gforceClient' => [
                'class'       => 'PortalPeople\gforce\components\GForceClient',
                'apiUrl'      => $this->apiUrl,
                'apiKey'      => $this->apiKey,
                'autoConfirm' => $this->autoConfirm,
                'timeout'     => $this->timeout,
                'debug'       => $this->debug,
            ],
        ]);
    }

    /**
     * Whether the module has the minimum required configuration.
     * @return bool
     */
    public function isConfigured(): bool
    {
        return !empty($this->apiUrl) && !empty($this->apiKey);
    }
}
