<?php

namespace Core\App\Integrations\Shipping;

/**
 * Maystro Delivery API Client
 *
 * Base URL prod : https://orders-management.maystro-delivery.com/api
 * Base URL dev  : https://orders-management-dev.maystro-delivery.com/api
 * Auth          : Authorization: Token {token}   ← DRF TokenAuthentication (capital T)
 *
 * Endpoints confirmed from official GitBook docs:
 *   POST /orders                             → Create order(s) — array payload
 *   GET  /base/wilayas/                      → List wilayas   (?language=fr&country=1)
 *   GET  /base/communes/                     → List communes  (?wilaya=<id>)
 *   GET  /base/delivery-options/             → Delivery options by commune (?commune=<id>)
 *   GET  /base/delivery-prices/              → Delivery price  (?commune=<id>&delivery_type=<1|2|3>)
 *   GET  /base/pickup-points                 → Pickup points   (?commune=<id>)
 *   POST /starter_bordureau/                 → Bordereau PDF   body: {orders_ids:[display_id]}
 *   POST /create-exchange/                   → Create exchange
 *   POST /refunds/                           → Create refund
 *   GET  /stock/products/                    → Products
 *   POST /stock/products/                    → Create product
 */
class MaystroClient
{
    protected string $baseUrl = 'https://orders-management.maystro-delivery.com/api';
    protected string $apiToken;

    /** Error codes in response errors[] array */
    public const ERROR_CODES = [
        10  => 'Order price is not valid or not defined.',
        20  => 'Order address is not defined.',
        30  => 'Customer info (name/phone) is not valid or not defined.',
        40  => 'Order wilaya or commune do not exist or were not defined.',
        45  => 'Delivery type does not exist for the provided commune.',
        50  => 'No products in details or they are invalid.',
        55  => 'Duplicate order (same customer/phone in last 24h).',
        60  => 'Unexpected error – contact Maystro support.',
        70  => 'Delivery pricing could not be detected – contact support.',
        80  => 'Orders list is empty.',
        90  => 'More than 1000 orders in request.',
        100 => "Order's External ID exceeded 100 characters.",
        101 => 'Not enough loyalty points to pay delivery.',
    ];

    /** Official Maystro status codes from the API documentation */
    public const STATUS_LABELS = [
        4  => 'CREATED',
        5  => 'PICK_UP_REQUESTED',
        6  => 'IN_PROCESS',
        8  => 'WAITING_TRANSIT',
        9  => 'IN_TRANSIT_TO_BE_SHIPPED',
        10 => 'IN_TRANSIT_TO_BE_RETURNED',
        11 => 'PENDING',
        12 => 'OUT_OF_STOCK',
        15 => 'READY_TO_SHIP',
        22 => 'ASSIGNED',
        31 => 'SHIPPED',
        32 => 'ALERTED',
        41 => 'DELIVERED',
        42 => 'POSTPONED',
        50 => 'ABORTED',
        51 => 'READY_TO_RETURN',
        52 => 'TAKEN_BY_STORE',
        53 => 'NOT_RECEIVED',
    ];

    /** delivery_type values */
    public const TYPE_HOME   = 1;
    public const TYPE_DESK   = 2;
    public const TYPE_PICKUP = 3;

    public function __construct(string $apiToken, string $baseUrl = '')
    {
        $this->apiToken = trim($apiToken);
        if ($baseUrl !== '') {
            // Allow dev/staging URL override via the api_id field in integrations settings
            // e.g. https://orders-management-dev.maystro-delivery.com/api
            $this->baseUrl = rtrim($baseUrl, '/');
        }
    }

    /**
     * Factory from ShippingIntegrationModel row.
     * Respects api_id as baseUrl override (dev/staging).
     */
    public static function fromIntegration(array $integration): self
    {
        $token   = trim((string)($integration['api_token'] ?? ''));
        $baseUrl = trim((string)($integration['api_id']    ?? ''));
        return new self($token, $baseUrl);
    }

    /* ─── Connection test ───────────────────────────────
       GET /base/wilayas/ — always exists, requires auth.
    ──────────────────────────────────────────────────── */
    public function testConnection(): array
    {
        return $this->request('GET', '/base/wilayas/', [], false, ['language' => 'fr', 'country' => 1]);
    }

    /* ─── Wilayas ────────────────────────────────────── */
    public function getWilayas(string $language = 'fr'): array
    {
        return $this->request('GET', '/base/wilayas/', [], false, ['language' => $language, 'country' => 1]);
    }

    /* ─── Communes ───────────────────────────────────── */
    public function getCommunes(int $wilayaId): array
    {
        return $this->request('GET', '/base/communes/', [], false, ['wilaya' => $wilayaId]);
    }

    /* ─── Delivery options by commune ───────────────────
       Returns home/stopdesk/pickup prices for a commune
    ──────────────────────────────────────────────────── */
    public function getDeliveryOptions(int $communeId): array
    {
        return $this->request('GET', '/base/delivery-options/', [], false, ['commune' => $communeId]);
    }

    /* ─── Delivery price (specific type) ────────────── */
    public function getDeliveryPrice(int $communeId, int $deliveryType = 1): array
    {
        return $this->request('GET', '/base/delivery-prices/', [], false, [
            'commune'       => $communeId,
            'delivery_type' => $deliveryType,
        ]);
    }

    /* ─── Pickup points by commune ───────────────────── */
    public function getPickupPoints(int $communeId = 0): array
    {
        $params = $communeId > 0 ? ['commune' => $communeId] : [];
        return $this->request('GET', '/base/pickup-points/', [], false, $params);
    }

    /* ─── Products ───────────────────────────────────── */
    public function getProducts(): array
    {
        return $this->request('GET', '/stock/products/');
    }

    public function createProduct(array $data): array
    {
        return $this->request('POST', '/stock/products/', $data);
    }

    /* ─── Create order(s) ────────────────────────────────
       Payload MUST be array: [{ ...order }, ...]
       Response: [{ id, external_id, tracking, success, delivery_price, errors }]
    ──────────────────────────────────────────────────── */
    public function createOrder(array $orderData): array
    {
        return $this->request('POST', '/orders/', [$orderData]);
    }

    public function createOrdersBulk(array $orders): array
    {
        return $this->request('POST', '/orders/', $orders);
    }

    /* ─── Get order by tracking ──────────────────────── */
    public function getOrder(string $tracking): array
    {
        return $this->request('GET', '/orders/' . urlencode($tracking) . '/');
    }

    /** Alias for cron task backward compat */
    public function track(string $tracking): array
    {
        return $this->getOrder($tracking);
    }

    /* ─── Bordereau PDF ──────────────────────────────────
       POST /starter_bordureau/
       body: { "orders_ids": [maystro_display_id] }
       returns: PDF binary
    ──────────────────────────────────────────────────── */
    /**
     * Get bordereau PDF.
     * Tries orders_ids with the provided ID.
     * Per Maystro doc: orders_ids = array of display_id_order values.
     */
    public function getLabel(string $displayIdOrTracking): array
    {
        return $this->request(
            'POST',
            '/starter_bordureau/',
            ['orders_ids' => [$displayIdOrTracking]],
            true
        );
    }

    /**
     * Get order details — includes display_id_order needed for bordereau.
     * GET /orders/{tracking}/
     */
    public function getOrderDetails(string $tracking): array
    {
        return $this->request('GET', '/orders/' . urlencode($tracking) . '/');
    }

    /* ─── Exchange ───────────────────────────────────────
       POST /create-exchange/
       body must include: display_old_order, is_exchange=true
    ──────────────────────────────────────────────────── */
    public function createExchange(array $data): array
    {
        return $this->request('POST', '/create-exchange/', $data);
    }

    /* ─── Refund ─────────────────────────────────────────
       POST /refunds/
       body must include: old_order_display_id, is_refund=true
    ──────────────────────────────────────────────────── */
    public function createRefund(array $data): array
    {
        return $this->request('POST', '/refunds/', $data);
    }

    /* ─── Static helpers ─────────────────────────────── */

    public static function decodeErrors(array $errors): array
    {
        $messages = [];
        foreach ($errors as $code) {
            $messages[] = self::ERROR_CODES[(int)$code] ?? ('Unknown error code: ' . $code);
        }
        return $messages;
    }

    public static function statusLabel(int $code): string
    {
        return self::STATUS_LABELS[$code] ?? ('STATUS_' . $code);
    }

    public static function isReturnStatus($status): bool
    {
        if (is_int($status) || (is_string($status) && ctype_digit($status))) {
            return in_array((int)$status, [10, 42, 50, 51, 52, 53], true);
        }

        $normalized = strtoupper(trim((string)$status));
        return in_array($normalized, [
            'IN_TRANSIT_TO_BE_RETURNED',
            'POSTPONED',
            'ABORTED',
            'READY_TO_RETURN',
            'TAKEN_BY_STORE',
            'NOT_RECEIVED',
        ], true);
    }

    public static function isDeliveredStatus($status): bool
    {
        if (is_int($status) || (is_string($status) && ctype_digit($status))) {
            return (int)$status === 41;
        }
        $s = mb_strtolower(trim((string)$status));
        foreach (['delivered', 'livr'] as $kw) {
            if (str_contains($s, $kw)) return true;
        }
        return false;
    }

    /* ─── Internal HTTP client ───────────────────────── */
    protected function request(
        string $method,
        string $endpoint,
        array  $payload     = [],
        bool   $rawResponse = false,
        array  $queryParams = []
    ): array {
        $method = strtoupper($method);

        // Always normalize URL and keep the trailing slash behavior expected by DRF.
        $url = rtrim($this->baseUrl, '/') . '/' . ltrim($endpoint, '/');

        if (!empty($queryParams)) {
            $url .= '?' . http_build_query($queryParams);
        }

        $ch = curl_init($url);

        $headers = [
            'Accept: application/json',
            'Authorization: Token ' . $this->apiToken,
        ];

        $opts = [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT        => 30,
            CURLOPT_SSL_VERIFYPEER => true,
            CURLOPT_FOLLOWLOCATION => false,
            CURLOPT_MAXREDIRS      => 0,
        ];

        // Use CURLOPT_POST for POST to match the standalone script that succeeds.
        if ($method === 'POST') {
            $opts[CURLOPT_POST] = true;
        } elseif (in_array($method, ['PUT', 'PATCH', 'DELETE'], true)) {
            $opts[CURLOPT_CUSTOMREQUEST] = $method;
        }

        if (in_array($method, ['POST', 'PUT', 'PATCH'], true)) {
            $headers[] = 'Content-Type: application/json';
            $opts[CURLOPT_POSTFIELDS] = json_encode($payload, JSON_UNESCAPED_UNICODE);
        }

        // Important: set headers only once after they are fully built.
        $opts[CURLOPT_HTTPHEADER] = $headers;

        curl_setopt_array($ch, $opts);

        $body      = curl_exec($ch);
        $httpCode  = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $curlError = curl_error($ch);
        curl_close($ch);

        if ($curlError) {
            return ['success' => false, 'error' => 'CURL_ERROR', 'message' => $curlError, 'status' => 0];
        }

        if ($rawResponse) {
            return ['success' => $httpCode >= 200 && $httpCode < 300, 'raw' => $body, 'status' => $httpCode];
        }

        $decoded = json_decode((string)$body, true);

        if ($httpCode >= 200 && $httpCode < 300) {
            return ['success' => true, 'status' => $httpCode, 'data' => $decoded];
        }

        // Capture body for debugging even when not JSON (e.g. 500 HTML page)
        $errorMsg = null;
        if (is_array($decoded)) {
            $errorMsg = $decoded['detail'] ?? $decoded['message'] ?? null;
            // Maystro sometimes wraps errors in 'non_field_errors' array
            if ($errorMsg === null && isset($decoded['non_field_errors'])) {
                $errorMsg = implode(', ', (array)$decoded['non_field_errors']);
            }
        }
        $errorMsg = $errorMsg ?? ('HTTP ' . $httpCode);

        return [
            'success'  => false,
            'status'   => $httpCode,
            'error'    => $errorMsg,
            'raw'      => $decoded,           // parsed JSON if available
            'raw_body' => substr((string)$body, 0, 2000), // first 2000 chars of raw body
        ];
    }
}
