<?php
/**
 * Caterpoint Order Import Service
 *
 * @category    PortalPeople
 * @package     Caterpoint
 * @author      Portal People Development Team
 * @copyright   Copyright (c) 2026 Portal People (https://portalpeople.co.uk)
 */

namespace PortalPeople\caterpoint\services;

use Yii;
use yii\base\BaseObject;
use yii\helpers\Json;
use PortalPeople\caterpoint\Module;
use PortalPeople\caterpoint\components\CaterpointClient;

/**
 * Order Import Service
 * 
 * Fetches orders from Caterpoint API and imports them into the local system
 */
class OrderImportService extends BaseObject
{
    /**
     * @var CaterpointClient API client for Caterpoint
     */
    public CaterpointClient $client;

    /**
     * @var CaterpointCursorStore Cursor storage for tracking import progress
     */
    public CaterpointCursorStore $cursorStore;

    /**
     * @var Module Module reference for configuration and helpers
     */
    public Module $module;

    /**
     * @var int Default number of orders to fetch per request
     */
    public int $defaultLimit = 100;

    /**
     * @var bool Mark orders as exported in Caterpoint after successful import
     */
    public bool $markExportedAfterImport = false;

    /**
     * @var string Model class for orders (configure based on your system)
     */
    public string $orderModelClass = '';

    /**
     * @var string Model class for order lines (configure based on your system)
     */
    public string $orderLineModelClass = '';

    /**
     * Run the order import process
     * 
     * @param string|null $cursor Optional cursor to start from (overrides stored cursor)
     * @param int $limit Optional limit (overrides default)
     * @param bool $dryRun If true, preview changes without persisting them
     * @return array Summary of import results
     */
    public function run(?string $cursor = null, int $limit = 0, bool $dryRun = false): array
    {
        $effectiveLimit = $limit > 0 ? $limit : $this->defaultLimit;
        $effectiveCursor = $cursor !== null && $cursor !== '' ? $cursor : $this->cursorStore->get();

        try {
            $fetched = $this->client->getOrdersUpdatedSince($effectiveCursor, $effectiveLimit);
            $orders = $this->normalizeOrders($fetched['orders'] ?? []);
        } catch (\Throwable $throwable) {
            return [
                'cursor' => $effectiveCursor,
                'nextCursor' => null,
                'fetched' => 0,
                'processed' => 0,
                'created' => 0,
                'updated' => 0,
                'failed' => 1,
                'dryRun' => $dryRun,
                'errors' => [
                    [
                        'message' => 'Failed to fetch orders: ' . $throwable->getMessage(),
                        'code' => $throwable->getCode(),
                    ]
                ],
            ];
        }

        $summary = [
            'cursor' => $effectiveCursor,
            'nextCursor' => $fetched['nextCursor'] ?? null,
            'fetched' => count($orders),
            'processed' => 0,
            'created' => 0,
            'updated' => 0,
            'markedExported' => 0,
            'exportFailed' => 0,
            'failed' => 0,
            'dryRun' => $dryRun,
            'errors' => [],
        ];

        foreach ($orders as $order) {
            try {
                if ($dryRun) {
                    $preview = $this->previewOrder($order);
                    if ($preview['created'] === true) {
                        $summary['created']++;
                    } else {
                        $summary['updated']++;
                    }
                    $summary['processed']++;
                    continue;
                }

                $result = $this->importOrder($order);

                if ($result['created'] === true) {
                    $summary['created']++;
                } else {
                    $summary['updated']++;
                }

                $summary['processed']++;

                if ($this->markExportedAfterImport && !$dryRun) {
                    $orderId = $this->resolveOrderId($order);
                    if ($orderId !== '') {
                        try {
                            $this->client->markOrderExported($orderId);
                            $summary['markedExported']++;
                        } catch (\Throwable $throwable) {
                            $summary['exportFailed']++;
                            $summary['errors'][] = [
                                'message' => 'Failed to mark order exported: ' . $throwable->getMessage(),
                                'code' => $throwable->getCode(),
                                'order' => $order,
                            ];
                        }
                    }
                }
            } catch (\Throwable $throwable) {
                $summary['failed']++;
                $summary['errors'][] = [
                    'message' => $throwable->getMessage(),
                    'code' => $throwable->getCode(),
                    'order' => $order,
                ];

                Yii::error(
                    'Caterpoint order import failed: ' . $throwable->getMessage() . ' | Order: ' . Json::encode($order),
                    'caterpoint.order-import'
                );
            }
        }

        // Update cursor if not in dry run and we have a next cursor
        if (!$dryRun && is_string($summary['nextCursor']) && $summary['nextCursor'] !== '') {
            $this->cursorStore->set($summary['nextCursor']);
        }

        return $summary;
    }

    /**
     * Preview how an order would be imported without persisting changes
     * 
     * @param array $order Order data from Caterpoint
     * @return array Preview result with 'created' boolean flag
     */
    public function previewOrder(array $order): array
    {
        $orderId = $this->resolveOrderId($order);
        
        // Check if order already exists in local system
        // This is a placeholder - implement based on your system
        $existing = \backend\models\Cartsbe::find()->where(['porderref' => $orderId])->one();

        return [
            'created' => $existing === null,
            'orderId' => $orderId,
        ];
    }

    /**
     * Import a single order into the local system via Cart::fromApi().
     *
     * @param array $order Order data from Caterpoint
     * @return array Import result with 'created' flag
     * @throws \RuntimeException on import failure
     */
    public function importOrder(array $order): array
    {
        $this->assertRuntimeClasses();
        $orderId = $this->resolveOrderId($order);

        return Yii::$app->db->transaction(function () use ($order, $orderId) {
            // Deduplication: remove any existing basket for this Caterpoint order ref
            $existing = \backend\models\Cartsbe::find()->where(['porderref' => $orderId])->one();
            if ($existing !== null) {
                \backend\models\Cartlines::deleteAll(['basketid' => $existing->id]);
                $existing->delete();
            }

            $payload = $this->buildFromApiPayload($order, $orderId);

            $result = \PortalPeople\cart\models\Cart::fromApi($payload);

            if (isset($result['errors'])) {
                throw new \RuntimeException(
                    'Cart::fromApi() failed: ' . Json::encode($result['errors'])
                );
            }

            $localOrder = $result['order'] ?? null;
            if ($localOrder === null) {
                throw new \RuntimeException("Cart::fromApi() returned no order for: {$orderId}");
            }

            // Re-tag as caterpoint so these orders are distinguishable in the backend
            $localOrder->type = 'ediOrders';
            $localOrder->sourceurl = 'CaterPoint';
            $localOrder->save(false);

            $this->createShipment($localOrder, $order);

            return [
                'created'      => true,
                'orderId'      => $orderId,
                'localOrderId' => $localOrder->id ?? null,
            ];
        });
    }

    /**
     * Build the payload array expected by Cart::fromApi() from a normalised GS1 order.
     *
     * Resolves the local account via the Caterpoint GLN/additionalPartyIdentification
     * stored as JSON_VALUE(extended, '$.caterpoint_code') on Accounts, applies the
     * three-level contact fallback (account → address → Useradv), and maps GS1 line
     * items to the stockCode/qty/price/product structure expected by fromApi().
     *
     * @param array  $order   Normalised order data
     * @param string $orderId Caterpoint order ID (used as orderNumber)
     * @return array          Payload for Cart::fromApi()
     */
    private function buildFromApiPayload(array $order, string $orderId): array
    {
        $delivery = $order['delivery'] ?? [];

        // Resolve account via Caterpoint-specific JSON field
        $shipToCode  = $delivery['shipTo']['additionalPartyIdentification'] ?? null;
        $jsonExpr    = new \yii\db\Expression('JSON_VALUE(extended, \'$.caterpoint_code\')');
        $userAdv_account    = new \yii\db\Expression('JSON_VALUE(adv, \'$.account_code\')');
        $account     = \backend\models\Accounts::find()->where(['=', $jsonExpr, $shipToCode])->one();
        $accountCode = $account ? $account->account_code : (string) $shipToCode;

        // Contact fallback: account → address → Useradv
        $contact  = null;
        $email    = null;
        if ($account !== null) {
            $addressl = $account->addressl;
            $contact  = !empty($account->contact)    ? $account->contact
                      : (!empty($addressl?->contact) ? $addressl->contact : null);
            if (empty($contact)) {
                try {
                    $user = \common\models\User::find()->where(['=', $userAdv_account, $accountCode])->one();
                    if ($user !== null) {
                        $contact = trim($user->firstname . ' ' . $user->lastname);
                    }
                } catch (\Throwable $e) {
                    // sys_useradv table may not exist; silently skip
                }
            }
            $email = $account->email ?? null;
        }

        // Delivery date from GS1 logistical date structure
        $deliveryDate = $delivery['orderLogisticalDateInformation']['requestedDeliveryDateTime']
            ?? $delivery['requestedDeliveryDateTime']
            ?? null;
        if (is_array($deliveryDate)) {
            $deliveryDate = $deliveryDate['date'] ?? $deliveryDate['dateTime'] ?? null;
        }
        if ($deliveryDate === null) {
            $deliveryDate = date('Y-m-d');
        }

        // Map GS1 line items to fromApi() line format
        $lines = [];
        foreach ($order['items'] ?? [] as $item) {
            $tti   = $item['transactionalTradeItem'] ?? [];
            $code  = $item['product_id'] ?? $item['stock_code'] ?? $tti['additionalTradeItemIdentification'] ?? null;
            $product = \frontend\models\Products::find()
                ->where(['=', new \yii\db\Expression("JSON_VALUE(data, '$.caterpoint.stock_code')"), $code])
                ->one();
            $desc = $product ? '' : 'Missing Code (' . $code . ') -- ' ;
            $desc .= isset($tti['tradeItemDescription'])
                ? trim((string) $tti['tradeItemDescription'])
                : ($item['description'] ?? '');
            $qty   = (int)   ($item['quantity'] ?? $item['qty'] ?? $item['requestedQuantity'] ?? 0);
            $price = (float) ($product->dmprice ?? $item['price'] ?? $item['unit_price'] ?? 0);

            $lines[] = [
                'stockCode' => (string) $product?->stock_code ?? $code,
                'qty'       => $qty,
                'price'     => $price,
                'product'   => $desc,
                'data'      => $item, // include original item data for debugging
            ];
        }

        return [
            'deliveryCustomerCode' => $accountCode,
            'orderNumber'          => $orderId,
            'deliveryDate'         => $deliveryDate,
            'contactName'          => $contact ?? '',
            'contactEmail'         => $email   ?? '',
            'lines'                => $lines,
        ];
    }

    /**
     * Normalize and validate order data from Caterpoint
     * 
     * @param array $orders Raw orders from API
     * @return array Normalized orders
     */
    private function normalizeOrders(array $orders): array
    {
        return array_map(function ($order) {
            return $this->normalizeOrder($order);
        }, $orders);
    }

    /**
     * Normalize a single order record
     *
     * Handles both generic REST format and GS1 EDI XML-converted-to-JSON format
     * returned by the Caterpoint API.
     *
     * @param array $order Raw order from API
     * @return array Normalized order
     */
    private function normalizeOrder(array $order): array
    {
        // GS1 EDI fallback values
        $gsOrderId = $order['orderIdentification']['entityIdentification'] ?? null;

        // orderLineItem may be a single item (assoc array) or a list
        $gsItems = [];
        if (isset($order['orderLineItem'])) {
            $raw = $order['orderLineItem'];
            // If it's an indexed list already use it; a single item is associative
            $gsItems = isset($raw[0]) ? $raw : [$raw];
        }

        return [
            'id'                => $order['id'] ?? $order['order_id'] ?? $gsOrderId,
            'reference'         => $order['reference'] ?? $order['orderRef'] ?? $order['order_reference'] ?? $gsOrderId,
            'customer_reference'=> $order['customerReference'] ?? $order['customer_reference'] ?? null,
            'status'            => $order['status'] ?? $order['documentStatusCode'] ?? null,
            'date'              => $order['date'] ?? $order['orderDate'] ?? $order['created_at'] ?? $order['creationDateTime'] ?? null,
            'updated_at'        => $order['updated_at'] ?? $order['updatedAt'] ?? null,
            'items'             => $order['items'] ?? $order['lines'] ?? $gsItems,
            'customer'          => $order['customer'] ?? $order['buyer'] ?? [],
            'delivery'          => $order['delivery'] ?? $order['orderLogisticalInformation'] ?? [],
            'totals'            => $order['totals'] ?? [],
            'metadata'          => $order,
        ];
    }

    /**
     * Resolve the primary order identifier from Caterpoint order data
     * 
     * @param array $order Order data
     * @return string Order identifier
     */
    private function resolveOrderId(array $order): string
    {
        return (string) ($order['id'] ?? $order['order_id'] ?? $order['reference'] ?? '');
    }

    /**
     * Assert that the service runtime environment is sane.
     *
     * Currently a no-op placeholder kept for subclass extensibility.
     */
    private function assertRuntimeClasses(): void
    {
    }

    /**
     * Create a delivery shipment record for the imported basket.
     *
     * Extracts the requested delivery date from GS1 EDI logistical information
     * and persists a Shipment record linked to the basket (shipment_type = 0).
     *
     * @param \backend\models\Cartsbe $localOrder Saved basket record
     * @param array                   $order       Normalised order data
     */
    private function createShipment($localOrder, array $order): void
    {
        // GS1 EDI delivery date paths (orderLogisticalInformation is mapped to 'delivery')
        $delivery     = $order['delivery'] ?? [];
        $rawMeta      = $order['metadata'] ?? [];
        $deliveryDate = $delivery['orderLogisticalDateInformation']['requestedDeliveryDateTime']
            ?? $delivery['requestedDeliveryDateTime']
            ?? $rawMeta['requestedDeliveryDateTime']
            ?? $rawMeta['requestedShipByDateTime']
            ?? null;

        // GS1 date may be a nested object: {"date": "2026-02-24"} or {"dateTime": "..."}
        if (is_array($deliveryDate)) {
            $deliveryDate = $deliveryDate['date'] ?? $deliveryDate['dateTime'] ?? $deliveryDate['dateTimePeriod'] ?? null;
        }

        // Normalise to Y-m-d
        if ($deliveryDate !== null) {
            $ts = is_numeric($deliveryDate) ? (int) $deliveryDate : strtotime((string) $deliveryDate);
            $deliveryDate = ($ts !== false && $ts > 0) ? date('Y-m-d', $ts) : null;
        }

        $shipment               = new \backend\models\Shipment();
        $shipment->id           = \common\models\Base::TempId();
        $shipment->cart_id      = $localOrder->id;
        $shipment->shipment_type = 0; // 0 = delivery
        $shipment->language     = $localOrder->language ?? 'en';
        $shipment->created_at   = time();

        if ($deliveryDate !== null) {
            $shipment->shipment_date = $deliveryDate;
        }

        if (!$shipment->save()) {
            Yii::warning(
                'Caterpoint: failed to save shipment for basket ' . $localOrder->id . ': ' . Json::encode($shipment->errors),
                'caterpoint.order-import'
            );
        }
    }
}
