<?php

namespace Core\App\Services;

use Core\App\Models\OrderModel;
use Core\App\Models\ShippingIntegrationModel;
use Core\App\Integrations\Shipping\EcotrackClient;
use Core\App\Integrations\Shipping\DhdClient;
use Core\App\Integrations\Shipping\MaystroClient;
use Core\App\Integrations\Shipping\YalidineClient;
use Core\App\Support\OrderPricing;

/**
 * BulkShippingService
 *
 * Handles bulk shipment creation for batches of phone-confirmed orders.
 * Supports: Ecotrack, DHD, Anderson, Packers (Ecotrack-family), Maystro,
 *           ZR Express, Yalidine.
 *
 * Zero interference with the individual order confirmation flow
 * in OrderShippingService.
 */
class BulkShippingService
{
    /** Providers that share the Ecotrack API */
    private const ECOTRACK_FAMILY = ['ecotrack', 'dhd', 'anderson', 'packers'];

    /** Fallback base URLs when not stored in the integration record */
    private const PROVIDER_BASE_URLS = [
        'anderson' => 'https://anderson-ecommerce.ecotrack.dz',
        'packers'  => 'https://packers.ecotrack.dz',
        'dhd'      => 'https://api.dhd-dz.com',
    ];

    /** Max orders per Ecotrack bulk request (API limit: 100; we use 50) */
    private const ECOTRACK_CHUNK = 50;

    /** Max orders per Maystro bulk request (API limit: 1000; we use 100) */
    private const MAYSTRO_CHUNK = 100;

    public function __construct(
        private OrderModel $orders,
        private OrderShippingService $shippingService
    ) {}

    /* =====================================================================
       PUBLIC API
    ===================================================================== */

    /**
     * Return all confirmed orders that have NOT been sent to a carrier yet.
     */
    public function getEligibleOrders(): array
    {
        $stmt = \db()->query(
            "SELECT * FROM orders WHERE order_status = 'confirmed' ORDER BY id DESC"
        );
        $rows = $stmt->fetchAll(\PDO::FETCH_ASSOC);

        $eligible = [];
        foreach ($rows as $row) {
            $extra = $this->decodeExtra($row['extra'] ?? null);
            $row['extra'] = $extra;
            if (!empty($extra['shipping']['tracking'])) {
                continue; // already dispatched
            }
            $eligible[] = $row;
        }

        return $eligible;
    }

    /**
     * Process a list of order IDs with the given shipping provider.
     *
     * @param  int[]   $orderIds
     * @param  string  $provider  ecotrack|dhd|anderson|packers|maystro|zr_express|yalidine
     * @return array   {total, success, failed, results[], ?error}
     */
    public function processOrders(array $orderIds, string $provider): array
    {
        if (empty($orderIds)) {
            return ['total' => 0, 'success' => 0, 'failed' => 0, 'results' => []];
        }

        $integration = (new ShippingIntegrationModel())->getByProvider($provider);
        if (!$integration || empty($integration['api_token']) || empty($integration['is_active'])) {
            return $this->errorSummary(
                __('bulk_confirm.integration_error', 'Shipping integration not configured or inactive')
            );
        }

        $orders = $this->loadEligibleBatch($orderIds);
        if (empty($orders)) {
            return ['total' => 0, 'success' => 0, 'failed' => 0, 'results' => []];
        }

        if (in_array($provider, self::ECOTRACK_FAMILY, true)) {
            return $this->processEcotrackFamily($orders, $provider, $integration);
        }

        return match ($provider) {
            'maystro'    => $this->processMaystro($orders, $integration),
            'zr_express' => $this->processZrExpress($orders, $integration),
            'yalidine'   => $this->processYalidine($orders, $integration),
            default      => $this->errorSummary('Unknown provider: ' . $provider),
        };
    }

    /* =====================================================================
       ECOTRACK FAMILY  (ecotrack / dhd / anderson / packers)
    ===================================================================== */

    private function processEcotrackFamily(
        array $orders,
        string $provider,
        array $integration
    ): array {
        $client  = $this->makeEcotrackClient($provider, $integration);
        $success = 0;
        $failed  = 0;
        $results = [];

        // Build payloads; skip orders with missing data immediately
        $payloadMap = []; // reference (orderId string) => ['order' => ..., 'payload' => ...]
        foreach ($orders as $order) {
            $payload = $this->buildEcotrackPayload($order);
            if ($payload === null) {
                $results[] = $this->makeRow($order, false, __('bulk_confirm.missing_data', 'Incomplete order data'));
                $failed++;
                continue;
            }
            $payloadMap[(string)$order['id']] = ['order' => $order, 'payload' => $payload];
        }

        if (empty($payloadMap)) {
            return compact('failed', 'success', 'results') + ['total' => count($orders)];
        }

        // Chunk into groups and send
        foreach (array_chunk($payloadMap, self::ECOTRACK_CHUNK, true) as $chunk) {
            // Ecotrack bulk format: {"0":{...}, "1":{...}}
            $bulkOrders = [];
            $idx        = 0;
            foreach ($chunk as $item) {
                $bulkOrders[(string)$idx] = $item['payload'];
                $idx++;
            }

            try {
                $response   = $client->createOrdersBulk($bulkOrders);
                $apiResults = $response['results'] ?? [];

                foreach ($chunk as $ref => $item) {
                    $order   = $item['order'];
                    $apiItem = $apiResults[$ref] ?? null; // key = reference = orderId

                    if (!empty($apiItem['success']) && !empty($apiItem['tracking'])) {
                        $tracking = (string)$apiItem['tracking'];
                        $this->finalizeOrder((int)$order['id'], $order, $provider, $tracking);
                        $results[] = $this->makeRow($order, true, '', $tracking);
                        $success++;
                    } else {
                        $errMsg = $this->extractEcotrackErrors($apiItem);
                        $results[] = $this->makeRow($order, false, $errMsg);
                        $failed++;
                    }
                }
            } catch (\Throwable $e) {
                foreach ($chunk as $item) {
                    $results[] = $this->makeRow($item['order'], false,
                        __('bulk_confirm.send_failed', 'API request failed'));
                    $failed++;
                }
            }
        }

        return ['total' => count($orders), 'success' => $success, 'failed' => $failed, 'results' => $results];
    }

    /* =====================================================================
       MAYSTRO
    ===================================================================== */

    private function processMaystro(array $orders, array $integration): array
    {
        $client  = MaystroClient::fromIntegration($integration);
        $success = 0;
        $failed  = 0;
        $results = [];

        $payloadMap = []; // external_id => ['order', 'payload']
        foreach ($orders as $order) {
            $payload = $this->buildMaystroPayload($order);
            if ($payload === null) {
                $results[] = $this->makeRow($order, false, __('bulk_confirm.missing_data', 'Incomplete order data'));
                $failed++;
                continue;
            }
            $payloadMap[(string)$order['id']] = ['order' => $order, 'payload' => $payload];
        }

        if (empty($payloadMap)) {
            return ['total' => count($orders), 'success' => 0, 'failed' => $failed, 'results' => $results];
        }

        foreach (array_chunk($payloadMap, self::MAYSTRO_CHUNK, true) as $chunk) {
            $bulkPayloads = array_column(array_values($chunk), 'payload');

            try {
                $response = $client->createOrdersBulk($bulkPayloads);

                if (!is_array($response)) {
                    foreach ($chunk as $item) {
                        $results[] = $this->makeRow($item['order'], false,
                            __('bulk_confirm.api_error', 'Invalid API response'));
                        $failed++;
                    }
                    continue;
                }

                // Index by external_id for quick lookup
                $byExtId = [];
                foreach ($response as $res) {
                    if (!empty($res['external_id'])) {
                        $byExtId[(string)$res['external_id']] = $res;
                    }
                }

                foreach ($chunk as $extId => $item) {
                    $order   = $item['order'];
                    $apiItem = $byExtId[$extId] ?? null;

                    if ($apiItem !== null && !empty($apiItem['success']) && !empty($apiItem['tracking'])) {
                        $tracking = (string)$apiItem['tracking'];
                        $this->finalizeMaystroOrder((int)$order['id'], $order, $tracking, $apiItem);
                        $results[] = $this->makeRow($order, true, '', $tracking);
                        $success++;
                    } else {
                        $errMsg = !empty($apiItem['errors'])
                            ? implode(', ', MaystroClient::decodeErrors((array)$apiItem['errors']))
                            : __('bulk_confirm.send_failed', 'Failed to create shipment');
                        $results[] = $this->makeRow($order, false, $errMsg);
                        $failed++;
                    }
                }
            } catch (\Throwable $e) {
                foreach ($chunk as $item) {
                    $results[] = $this->makeRow($item['order'], false,
                        __('bulk_confirm.send_failed', 'API request failed'));
                    $failed++;
                }
            }
        }

        return ['total' => count($orders), 'success' => $success, 'failed' => $failed, 'results' => $results];
    }

    /* =====================================================================
       ZR EXPRESS  (individual loop — same auth pattern as OrderShippingService)
    ===================================================================== */

    private function processZrExpress(array $orders, array $integration): array
    {
        $baseUrl = rtrim((string)($integration['api_id']    ?? ''), '/');
        $token   = trim((string)($integration['api_token'] ?? ''));

        if ($baseUrl === '' || $token === '') {
            return $this->errorSummary(
                __('bulk_confirm.integration_error', 'ZR Express integration not configured')
            );
        }

        $headers = [
            'Authorization: Bearer ' . $token,
            'Accept: application/json',
            'Content-Type: application/json',
        ];

        $success = 0;
        $failed  = 0;
        $results = [];

        foreach ($orders as $order) {
            $payload = $this->buildZrExpressPayload($order);
            if ($payload === null) {
                $results[] = $this->makeRow($order, false, __('bulk_confirm.missing_data', 'Incomplete order data'));
                $failed++;
                continue;
            }

            try {
                $ch = curl_init($baseUrl . '/add_colis');
                curl_setopt_array($ch, [
                    CURLOPT_RETURNTRANSFER => true,
                    CURLOPT_POST           => true,
                    CURLOPT_HTTPHEADER     => $headers,
                    CURLOPT_POSTFIELDS     => json_encode($payload, JSON_UNESCAPED_UNICODE),
                    CURLOPT_TIMEOUT        => 20,
                ]);
                $raw = curl_exec($ch);
                curl_close($ch);
                $res = json_decode((string)$raw, true);

                if (empty($res['tracking'])) {
                    $results[] = $this->makeRow($order, false,
                        __('bulk_confirm.send_failed', 'No tracking number returned'));
                    $failed++;
                    continue;
                }

                $tracking = (string)$res['tracking'];

                // Mark as "pret" (ready to ship) — same as individual flow
                $ch2 = curl_init($baseUrl . '/pret');
                curl_setopt_array($ch2, [
                    CURLOPT_RETURNTRANSFER => true,
                    CURLOPT_POST           => true,
                    CURLOPT_HTTPHEADER     => $headers,
                    CURLOPT_POSTFIELDS     => json_encode(['tracking' => $tracking]),
                    CURLOPT_TIMEOUT        => 10,
                ]);
                curl_exec($ch2);
                curl_close($ch2);

                $this->finalizeOrder((int)$order['id'], $order, 'zr_express', $tracking, $res);
                $results[] = $this->makeRow($order, true, '', $tracking);
                $success++;

            } catch (\Throwable $e) {
                $results[] = $this->makeRow($order, false,
                    __('bulk_confirm.send_failed', 'API request failed'));
                $failed++;
            }
        }

        return ['total' => count($orders), 'success' => $success, 'failed' => $failed, 'results' => $results];
    }

    /* =====================================================================
       YALIDINE  (individual loop)
    ===================================================================== */

    private function processYalidine(array $orders, array $integration): array
    {
        $client  = new YalidineClient($integration['api_token'], $integration['api_id'] ?? '');
        $success = 0;
        $failed  = 0;
        $results = [];

        foreach ($orders as $order) {
            $payload = $this->buildYalidinePayload($order);
            if ($payload === null) {
                $results[] = $this->makeRow($order, false, __('bulk_confirm.missing_data', 'Incomplete order data'));
                $failed++;
                continue;
            }

            try {
                $response = $client->createParcel($payload);

                $tracking = null;
                if (!empty($response['success'])) {
                    $tracking = $response['data']['data'][0]['tracking']
                             ?? $response['data']['tracking']
                             ?? null;
                }

                if ($tracking) {
                    $this->finalizeOrder((int)$order['id'], $order, 'yalidine', (string)$tracking, $response);
                    $results[] = $this->makeRow($order, true, '', (string)$tracking);
                    $success++;
                } else {
                    $err = (string)($response['message'] ?? $response['error']
                        ?? __('bulk_confirm.send_failed', 'No tracking returned'));
                    $results[] = $this->makeRow($order, false, $err);
                    $failed++;
                }
            } catch (\Throwable $e) {
                $results[] = $this->makeRow($order, false,
                    __('bulk_confirm.send_failed', 'API request failed'));
                $failed++;
            }
        }

        return ['total' => count($orders), 'success' => $success, 'failed' => $failed, 'results' => $results];
    }

    /* =====================================================================
       PAYLOAD BUILDERS
    ===================================================================== */

    /**
     * Ecotrack payload. Returns null when required fields are missing.
     */
    private function buildEcotrackPayload(array $order): ?array
    {
        $extra        = $this->decodeExtra($order['extra'] ?? null);
        $destination  = $extra['shipping']['destination'] ?? [];
        $shippingType = $extra['shipping']['type'] ?? 'home';

        $codeWilaya = (int)ltrim((string)($destination['state_code'] ?? ''), '0');
        $commune    = trim((string)($destination['city_name']  ?? ''));
        $adresse    = trim((string)($order['address']          ?? ''));
        $montant    = (float)OrderPricing::effectiveTotal($order);

        if ($codeWilaya <= 0 || $commune === '' || $adresse === '' || $montant <= 0) {
            return null;
        }

        $phone = preg_replace('/\D+/', '', (string)($order['phone'] ?? ''));
        if (str_starts_with($phone, '213')) $phone = substr($phone, 3);
        if (str_starts_with($phone, '0'))   $phone = substr($phone, 1);

        // Build product label
        $label       = trim((string)($extra['product']['name'] ?? ''));
        $variantBits = [];
        if (!empty($extra['variants']['size']))  $variantBits[] = 'Taille: '  . $extra['variants']['size'];
        if (!empty($extra['variants']['color'])) $variantBits[] = 'Couleur: ' . $extra['variants']['color'];
        if ($variantBits) {
            $label .= ($label !== '' ? ' - ' : '') . implode(' | ', $variantBits);
        }
        if (!empty($extra['quantity']) && (int)$extra['quantity'] > 1) {
            $label .= ($label !== '' ? ' - ' : '') . 'Qte: ' . (int)$extra['quantity'];
        }
        if ($label === '') $label = 'Commande #' . $order['id'];

        return [
            'reference'   => (string)$order['id'],
            'nom_client'  => trim((string)($order['full_name'] ?? '')),
            'telephone'   => $phone,
            'adresse'     => $adresse,
            'commune'     => $commune,
            'code_wilaya' => $codeWilaya,
            'montant'     => $montant,
            'type'        => '1',    // 1 = Livraison
            'stop_desk'   => $shippingType === 'desk' ? 1 : 0,
            'produit'     => $label,
        ];
    }

    /**
     * Maystro payload. Returns null when required fields are missing.
     */
    private function buildMaystroPayload(array $order): ?array
    {
        $extra        = $this->decodeExtra($order['extra'] ?? null);
        $destination  = $extra['shipping']['destination'] ?? [];
        $shippingType = $extra['shipping']['type'] ?? 'home';

        $communeValue = $destination['city_id']   ?? 0;
        $communeName  = trim((string)($destination['city_name']  ?? ''));
        $stateName    = trim((string)($destination['state_name'] ?? ''));

        if ((empty($communeValue) || (is_int($communeValue) && $communeValue <= 0)) && $communeName === '') {
            return null;
        }

        $typeMap     = ['home' => 1, 'desk' => 2, 'pickup' => 3];
        $deliveryInt = $typeMap[$shippingType] ?? 1;

        $description = trim((string)($extra['product']['name'] ?? ('Commande #' . $order['id'])));
        $quantity    = max(1, (int)($extra['quantity'] ?? 1));
        $address     = $stateName !== ''
            ? $stateName . ($communeName !== '' ? ' - ' . $communeName : '')
            : trim((string)($order['address'] ?? ''));

        $commune = (is_int($communeValue) && $communeValue > 0) ? $communeValue : $communeName;

        $payload = [
            'external_id'      => (string)$order['id'],
            'customer_name'    => trim((string)($order['full_name'] ?? '')),
            'customer_phone'   => trim((string)($order['phone']     ?? '')),
            'destination_text' => $address !== '' ? $address : trim((string)($order['address'] ?? '')),
            'total_price'      => (int)round((float)OrderPricing::effectiveTotal($order)),
            'delivery_type'    => $deliveryInt,
            'commune'          => $commune,
            'express'          => false,
            'note_to_driver'   => null,
            'details'          => [[
                'product'     => '',
                'description' => $description !== '' ? $description : ('Commande #' . $order['id']),
                'quantity'    => $quantity,
            ]],
        ];

        if (is_string($commune) && $stateName !== '') {
            $payload['wilaya'] = $stateName;
        }

        return $payload;
    }

    /**
     * ZR Express payload. Returns null when required fields are missing.
     */
    private function buildZrExpressPayload(array $order): ?array
    {
        $extra       = $this->decodeExtra($order['extra'] ?? null);
        $destination = $extra['shipping']['destination'] ?? [];
        $address     = trim((string)($order['address'] ?? ''));
        $amount      = (float)OrderPricing::effectiveTotal($order);

        if ($address === '' || $amount <= 0) {
            return null;
        }

        return [
            'nom_client' => trim((string)($order['full_name'] ?? '')),
            'telephone'  => trim((string)($order['phone']     ?? '')),
            'wilaya'     => $destination['state_code'] ?? '',
            'commune'    => $destination['city_name']  ?? '',
            'adresse'    => $address,
            'montant'    => $amount,
            'poids'      => 0.5,
            'remarque'   => '',
            'produit'    => (string)$order['id'],
        ];
    }

    /**
     * Yalidine payload. Returns null when required fields are missing.
     */
    private function buildYalidinePayload(array $order): ?array
    {
        $extra        = $this->decodeExtra($order['extra'] ?? null);
        $destination  = $extra['shipping']['destination'] ?? [];
        $shippingType = $extra['shipping']['type'] ?? 'home';

        $toWilayaId  = (int)ltrim((string)($destination['state_code'] ?? ''), '0');
        $toCommuneId = (int)($destination['city_id'] ?? 0);
        $amount      = (float)OrderPricing::effectiveTotal($order);

        if ($toWilayaId <= 0 || $toCommuneId <= 0 || $amount <= 0) {
            return null;
        }

        $nameParts  = explode(' ', trim((string)($order['full_name'] ?? '')), 2);
        $firstName  = $nameParts[0] ?? '';
        $familyName = $nameParts[1] ?? '';

        return [
            'firstname'      => $firstName,
            'familyname'     => $familyName,
            'contact_phone'  => trim((string)($order['phone'] ?? '')),
            'address'        => trim((string)($order['address'] ?? '')),
            'from_wilaya_id' => 16,
            'to_wilaya_id'   => $toWilayaId,
            'to_commune_id'  => $toCommuneId,
            'product_list'   => trim((string)($extra['product']['name'] ?? 'colis')),
            'price'          => $amount,
            'do_insurance'   => 0,
            'declared_value' => $amount,
            'length'         => 1,
            'width'          => 1,
            'height'         => 1,
            'weight'         => 0.5,
            'freeshipping'   => 0,
            'is_stopdesk'    => $shippingType === 'desk' ? 1 : 0,
            'has_exchange'   => 0,
            'note'           => '',
            'reference'      => (string)$order['id'],
        ];
    }

    /* =====================================================================
       FINALIZE HELPERS
    ===================================================================== */

    /**
     * Persist tracking number and trigger all post-shipment side effects
     * (stock deduction, status -> confirmed, WhatsApp, lifecycle log).
     */
    private function finalizeOrder(
        int    $orderId,
        array  $order,
        string $provider,
        string $tracking,
        array  $response = []
    ): void {
        $extra = $this->decodeExtra($order['extra'] ?? null);
        $extra['shipping']['provider']     = $provider;
        $extra['shipping']['tracking']     = $tracking;
        $extra['shipping']['status']       = 'sent';
        $extra['shipping']['bulk_sent_at'] = date('Y-m-d H:i:s');
        if (!empty($response)) {
            $extra['shipping']['response'] = $response;
        }

        \db()->prepare("UPDATE orders SET extra = :extra WHERE id = :id")
            ->execute([':extra' => json_encode($extra, JSON_UNESCAPED_UNICODE), ':id' => $orderId]);

        $this->shippingService->handlePostShipmentActions($orderId, $extra, $provider);
    }

    /**
     * Maystro-specific finalizer (stores display_id from response when available).
     */
    private function finalizeMaystroOrder(
        int    $orderId,
        array  $order,
        string $tracking,
        array  $apiItem
    ): void {
        $extra = $this->decodeExtra($order['extra'] ?? null);
        $extra['shipping']['provider']     = 'maystro';
        $extra['shipping']['tracking']     = $tracking;
        $extra['shipping']['status']       = 'sent';
        $extra['shipping']['bulk_sent_at'] = date('Y-m-d H:i:s');
        $extra['shipping']['response']     = $apiItem;
        if (!empty($apiItem['id'])) {
            $extra['shipping']['maystro_id'] = $apiItem['id'];
        }

        \db()->prepare("UPDATE orders SET extra = :extra WHERE id = :id")
            ->execute([':extra' => json_encode($extra, JSON_UNESCAPED_UNICODE), ':id' => $orderId]);

        $this->shippingService->handlePostShipmentActions($orderId, $extra, 'maystro');
    }

    /* =====================================================================
       UTILITY
    ===================================================================== */

    private function loadEligibleBatch(array $orderIds): array
    {
        $ids = array_filter(array_map('intval', $orderIds), fn($id) => $id > 0);
        if (empty($ids)) return [];

        $placeholders = implode(',', array_fill(0, count($ids), '?'));
        $stmt = \db()->prepare(
            "SELECT * FROM orders
             WHERE id IN ({$placeholders}) AND order_status = 'confirmed'
             ORDER BY id ASC"
        );
        $stmt->execute(array_values($ids));
        $rows = $stmt->fetchAll(\PDO::FETCH_ASSOC);

        $result = [];
        foreach ($rows as $row) {
            $extra = $this->decodeExtra($row['extra'] ?? null);
            $row['extra'] = $extra;
            if (!empty($extra['shipping']['tracking'])) continue; // skip already-sent
            $result[] = $row;
        }
        return $result;
    }

    private function makeEcotrackClient(string $provider, array $integration): EcotrackClient
    {
        $token   = trim((string)($integration['api_token'] ?? ''));
        $baseUrl = trim((string)($integration['api_id']    ?? ''));

        if ($baseUrl === '') {
            $baseUrl = self::PROVIDER_BASE_URLS[$provider] ?? '';
        }

        if ($provider === 'dhd') {
            return new DhdClient($token, $baseUrl ?: self::PROVIDER_BASE_URLS['dhd']);
        }

        return new EcotrackClient($token, $baseUrl);
    }

    private function decodeExtra(mixed $raw): array
    {
        if (is_array($raw)) return $raw;
        return json_decode((string)($raw ?? '{}'), true) ?: [];
    }

    private function makeRow(
        array  $order,
        bool   $success,
        string $error    = '',
        string $tracking = ''
    ): array {
        return [
            'order_id'  => (int)$order['id'],
            'full_name' => (string)($order['full_name'] ?? ''),
            'phone'     => (string)($order['phone']     ?? ''),
            'success'   => $success,
            'tracking'  => $tracking,
            'error'     => $error,
        ];
    }

    private function errorSummary(string $message): array
    {
        return ['total' => 0, 'success' => 0, 'failed' => 0, 'results' => [], 'error' => $message];
    }

    private function extractEcotrackErrors(?array $apiItem): string
    {
        if ($apiItem === null) {
            return __('bulk_confirm.send_failed', 'No response for this order');
        }
        $parts = [];
        foreach ($apiItem as $field => $msgs) {
            if ($field === 'success') continue;
            if (is_array($msgs)) $parts[] = implode(', ', $msgs);
        }
        return $parts
            ? implode(' | ', $parts)
            : __('bulk_confirm.send_failed', 'Unknown error');
    }
}
