<?php

namespace PortalPeople\virtualstock\services;

use Yii;
use yii\base\BaseObject;
use yii\helpers\Json;
use PortalPeople\virtualstock\Module;
use PortalPeople\virtualstock\components\VirtualStockClient;

class OrderImportService extends BaseObject
{
    public VirtualStockClient $client;
    public VirtualStockCursorStore $cursorStore;
    public Module $module;
    public int $defaultLimit = 100;

    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();

        $fetched = $this->client->getOrdersUpdatedSince($effectiveCursor, $effectiveLimit);
        $orders = $this->normalizeOrders($fetched['orders'] ?? []);

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

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

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

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

                $summary['linesCreated'] += $result['linesCreated'];
                $summary['linesUpdated'] += $result['linesUpdated'];
            } catch (\Throwable $throwable) {
                $summary['failed']++;
                $summary['errors'][] = [
                    'message' => $throwable->getMessage(),
                    'order' => $order,
                ];
            }
        }

        if ($dryRun === false && is_string($summary['nextCursor']) && $summary['nextCursor'] !== '') {
            $this->cursorStore->set($summary['nextCursor']);
        }

        return $summary;
    }

    private function previewOne(array $order): array
    {
        $orderNo = $this->resolveOrderNo($order);
        $existing = $this->findCartByVirtualStockOrderNo($orderNo);

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

    private function importOne(array $order): array
    {
        $this->assertRuntimeClasses();
        $orderNo = $this->resolveOrderNo($order);

        return Yii::$app->db->transaction(function () use ($order, $orderNo) {
            [$cart, $created] = $this->findOrCreateCart($orderNo);
            $this->mapOrderToCart($cart, $order, $orderNo);

            if (!$cart->save()) {
                throw new \RuntimeException('Failed to save cart: ' . Json::encode($cart->errors));
            }

            $lineResult = $this->upsertOrderLines($cart, $this->extractOrderLines($order));
            $this->refreshOrderTotals($cart);

            return [
                'created' => $created,
                'linesCreated' => $lineResult['created'],
                'linesUpdated' => $lineResult['updated'],
            ];
        });
    }

    private function mapOrderToCart($cart, array $order, string $orderNo): void
    {
        $cart->ordernumber = $cart->ordernumber ?: $this->module->makeOrderNumber($orderNo);

        if ($cart->hasAttribute('vs_order_no')) {
            $cart->setAttribute('vs_order_no', $orderNo);
        }

        $orderRef = $this->firstNonEmpty([
            $order['customerReference'] ?? null,
            $order['orderRef'] ?? null,
        ]);
        if ($orderRef !== '') {
            $cart->orderref = $orderRef;
        }

        $status = $this->firstNonEmpty([
            $order['status'] ?? null,
            $order['orderStatus'] ?? null,
        ]);
        $cart->status = $this->module->mapStatus($status);

        $dateValue = $this->firstNonEmpty([
            $order['orderDate'] ?? null,
            $order['createdAt'] ?? null,
            $order['updatedAt'] ?? null,
            $order['updated_at'] ?? null,
        ]);
        $dateTimestamp = $this->toTimestamp($dateValue);
        if ($dateTimestamp !== null) {
            $cart->date = $dateTimestamp;
        }

        $accountCode = $this->firstNonEmpty([
            $order['customerAccount'] ?? null,
            $order['accountCode'] ?? null,
        ]);
        if ($accountCode !== '') {
            $cart->account_code = $accountCode;
        }

        $email = $this->firstNonEmpty([
            $order['customerEmail'] ?? null,
            $order['email'] ?? null,
        ]);
        if ($email !== '') {
            $cart->email = $email;
        }

        $contact = $this->firstNonEmpty([
            $order['customerName'] ?? null,
            $order['contact'] ?? null,
        ]);
        if ($contact !== '') {
            $cart->contact = $contact;
        }

        $telephone = $this->firstNonEmpty([
            $order['customerPhone'] ?? null,
            $order['telephone'] ?? null,
        ]);
        if ($telephone !== '') {
            $cart->telephone = $telephone;
        }

        $postcode = $this->firstNonEmpty([
            $order['deliveryPostcode'] ?? null,
            $order['postcode'] ?? null,
        ]);
        if ($postcode !== '') {
            $cart->postcode = $postcode;
        }

        $fullAddress = $this->buildFullAddress($order);
        if ($fullAddress !== '') {
            $cart->fulladdress = $fullAddress;
        }

        $meta = [
            'order_no' => $orderNo,
            'status' => $status,
            'last_sync' => date('c'),
            'updated_at_source' => $this->firstNonEmpty([
                $order['updatedAt'] ?? null,
                $order['updated_at'] ?? null,
            ]),
        ];

        $shipments = $this->extractShipments($order);
        if ($shipments !== []) {
            $meta['shipments_json'] = Json::encode($shipments);
        }

        $this->setVirtualStockMetadata($cart, $meta);
    }

    private function upsertOrderLines($cart, array $lines): array
    {
        $cartLinesClass = '\\backend\\models\\Cartlines';

        $created = 0;
        $updated = 0;
        $seenSequences = [];

        foreach ($lines as $index => $line) {
            if (!is_array($line)) {
                continue;
            }

            $sequence = (int) ($line['lineNo'] ?? $line['sequence'] ?? ($index + 1));
            if ($sequence <= 0) {
                $sequence = $index + 1;
            }

            $seenSequences[] = $sequence;

            $lineModel = $cartLinesClass::find()
                ->where(['basketid' => $cart->id, 'sequence' => $sequence])
                ->one();

            if ($lineModel === null) {
                $lineModel = new $cartLinesClass([
                    'basketid' => $cart->id,
                    'sequence' => $sequence,
                ]);
                $created++;
            } else {
                $updated++;
            }

            $this->mapOrderLineToCartLine($lineModel, $line, $sequence);

            if (!$lineModel->save()) {
                throw new \RuntimeException('Failed to save cart line: ' . Json::encode($lineModel->errors));
            }
        }

        $this->markMissingLinesDeleted($cart->id, $seenSequences);

        return [
            'created' => $created,
            'updated' => $updated,
        ];
    }

    private function mapOrderLineToCartLine($lineModel, array $line, int $sequence): void
    {
        $lineModel->sequence = $sequence;
        $lineModel->qty = max(0, (int) ($line['quantity'] ?? $line['qty'] ?? 0));
        $lineModel->price = (float) ($line['unitPrice'] ?? $line['price'] ?? 0);
        $lineModel->status = 'Active';

        $productName = $this->firstNonEmpty([
            $line['productDescription'] ?? null,
            $line['name'] ?? null,
            $line['product_name'] ?? null,
        ]);

        if ($productName !== '') {
            $lineModel->product_name = $productName;
        }

        $productDescription = $this->firstNonEmpty([
            $line['description'] ?? null,
            $line['productDescription'] ?? null,
            $line['product_description'] ?? null,
        ]);

        if ($productDescription !== '' && $lineModel->hasAttribute('product_description')) {
            $lineModel->setAttribute('product_description', $productDescription);
        }

        $supplierSku = trim($this->firstNonEmpty([
            $line['supplierProductNo'] ?? null,
            $line['sku'] ?? null,
            $line['stock_code'] ?? null,
        ]));

        if ($supplierSku !== '' && $lineModel->hasAttribute('stock_code')) {
            $lineModel->setAttribute('stock_code', $supplierSku);
        }

        $productId = $this->resolveProductIdBySku($supplierSku);
        if ($productId !== null) {
            $lineModel->productid = $productId;
        }
    }

    private function refreshOrderTotals($cart): void
    {
        if (!$cart->save(false)) {
            throw new \RuntimeException('Failed to refresh cart totals: ' . Json::encode($cart->errors));
        }
    }

    private function markMissingLinesDeleted(string $cartId, array $seenSequences): void
    {
        $cartLinesClass = '\\backend\\models\\Cartlines';
        $cartsClass = '\\backend\\models\\Cartsbe';

        $query = $cartLinesClass::find()
            ->where(['basketid' => $cartId])
            ->andWhere(['<', 'sequence', $cartsClass::SEQUENCE_PRODUCT_MAX])
            ->andWhere(['!=', 'status', 'Deleted']);

        if ($seenSequences !== []) {
            $query->andWhere(['not in', 'sequence', $seenSequences]);
        }

        $staleLines = $query->all();

        foreach ($staleLines as $lineModel) {
            $lineModel->status = 'Deleted';
            $lineModel->qty = 0;
            if (!$lineModel->save(false, ['status', 'qty'])) {
                throw new \RuntimeException('Failed to mark stale cart line deleted: ' . Json::encode($lineModel->errors));
            }
        }
    }

    private function findOrCreateCart(string $orderNo): array
    {
        $cart = $this->findCartByVirtualStockOrderNo($orderNo);
        if ($cart !== null) {
            return [$cart, false];
        }

        $cartClass = '\\backend\\models\\Cartsbe';
        $cart = new $cartClass();

        return [$cart, true];
    }

    private function findCartByVirtualStockOrderNo(string $orderNo)
    {
        $cartClass = '\\backend\\models\\Cartsbe';
        $probe = new $cartClass();

        if ($probe->hasAttribute('vs_order_no')) {
            $found = $cartClass::find()->where(['vs_order_no' => $orderNo])->one();
            if ($found !== null) {
                return $found;
            }
        }

        $orderNumber = $this->module->makeOrderNumber($orderNo);

        return $cartClass::find()->where(['ordernumber' => $orderNumber])->one();
    }

    private function setVirtualStockMetadata($cart, array $metadata): void
    {
        $existing = $this->decodeExistingData($cart);

        $virtualStockData = [];
        if (isset($existing['virtualstock']) && is_array($existing['virtualstock'])) {
            $virtualStockData = $existing['virtualstock'];
        }

        $existing['virtualstock'] = array_merge($virtualStockData, $metadata);
        $cart->data = Json::encode($existing);
    }

    private function decodeExistingData($cart): array
    {
        $rawData = $cart->isNewRecord ? null : $cart->getOldAttribute('data');

        if (!is_string($rawData) || $rawData === '') {
            if (is_string($cart->data)) {
                $rawData = $cart->data;
            }
        }

        if (!is_string($rawData) || $rawData === '') {
            return [];
        }

        $decoded = Json::decode($rawData, true);

        return is_array($decoded) ? $decoded : [];
    }

    private function resolveOrderNo(array $order): string
    {
        $orderNo = trim($this->firstNonEmpty([
            $order['orderNo'] ?? null,
            $order['order_no'] ?? null,
            $order['id'] ?? null,
        ]));

        if ($orderNo === '') {
            throw new \InvalidArgumentException('Order payload missing order identifier.');
        }

        return $orderNo;
    }

    private function extractOrderLines(array $order): array
    {
        $lines = $order['orderLines'] ?? $order['lines'] ?? [];

        if (!is_array($lines)) {
            return [];
        }

        if (array_is_list($lines)) {
            return array_values(array_filter($lines, static fn($line) => is_array($line)));
        }

        if (isset($lines['lineNo']) || isset($lines['quantity']) || isset($lines['supplierProductNo'])) {
            return [$lines];
        }

        $out = [];
        foreach ($lines as $line) {
            if (is_array($line)) {
                $out[] = $line;
            }
        }

        return $out;
    }

    private function extractShipments(array $order): array
    {
        $shipments = $order['shipments'] ?? [];
        if (!is_array($shipments)) {
            return [];
        }

        if (array_is_list($shipments)) {
            return array_values(array_filter($shipments, static fn($shipment) => is_array($shipment)));
        }

        if (isset($shipments['trackingNo']) || isset($shipments['carrier'])) {
            return [$shipments];
        }

        $out = [];
        foreach ($shipments as $shipment) {
            if (is_array($shipment)) {
                $out[] = $shipment;
            }
        }

        return $out;
    }

    private function buildFullAddress(array $order): string
    {
        $parts = [];

        if (isset($order['deliveryAddress']) && is_array($order['deliveryAddress'])) {
            $deliveryAddress = $order['deliveryAddress'];
            $parts[] = $this->firstNonEmpty([
                $deliveryAddress['line1'] ?? null,
                $deliveryAddress['address1'] ?? null,
            ]);
            $parts[] = $this->firstNonEmpty([
                $deliveryAddress['line2'] ?? null,
                $deliveryAddress['address2'] ?? null,
            ]);
            $parts[] = $this->firstNonEmpty([
                $deliveryAddress['city'] ?? null,
                $deliveryAddress['town'] ?? null,
            ]);
            $parts[] = $deliveryAddress['postcode'] ?? null;
            $parts[] = $deliveryAddress['country'] ?? null;
        } else {
            $parts[] = $this->firstNonEmpty([
                $order['deliveryAddress1'] ?? null,
                $order['address1'] ?? null,
            ]);
            $parts[] = $this->firstNonEmpty([
                $order['deliveryAddress2'] ?? null,
                $order['address2'] ?? null,
            ]);
            $parts[] = $this->firstNonEmpty([
                $order['deliveryCity'] ?? null,
                $order['city'] ?? null,
            ]);
            $parts[] = $this->firstNonEmpty([
                $order['deliveryPostcode'] ?? null,
                $order['postcode'] ?? null,
            ]);
            $parts[] = $this->firstNonEmpty([
                $order['deliveryCountry'] ?? null,
                $order['country'] ?? null,
            ]);
        }

        $parts = array_filter(array_map(static fn($part) => trim((string) $part), $parts), static fn($part) => $part !== '');

        return implode(', ', $parts);
    }

    private function resolveProductIdBySku(string $supplierSku): ?string
    {
        if ($supplierSku === '') {
            return null;
        }

        $productsClass = '\\backend\\models\\Productsbe';
        $product = $productsClass::find()->select(['id'])->where(['stock_code' => $supplierSku])->one();

        return $product ? (string) $product->id : null;
    }

    private function toTimestamp(string $dateValue): ?int
    {
        if ($dateValue === '') {
            return null;
        }

        $timestamp = strtotime($dateValue);

        return $timestamp === false ? null : $timestamp;
    }

    private function firstNonEmpty(array $values): string
    {
        foreach ($values as $value) {
            if (!is_scalar($value)) {
                continue;
            }

            $stringValue = trim((string) $value);
            if ($stringValue !== '') {
                return $stringValue;
            }
        }

        return '';
    }

    private function normalizeOrders(array $orders): array
    {
        return array_values(array_filter($orders, static fn($order) => is_array($order)));
    }

    private function assertRuntimeClasses(): void
    {
        $requiredClasses = [
            '\\backend\\models\\Cartsbe',
            '\\backend\\models\\Cartlines',
            '\\backend\\models\\Productsbe',
        ];

        foreach ($requiredClasses as $requiredClass) {
            if (!class_exists($requiredClass)) {
                throw new \RuntimeException('Required runtime class not found: ' . $requiredClass);
            }
        }
    }
}
