<?php
/**
 * Palletways REST API Client
 *
 * @category    PortalPeople
 * @package     Palletways
 * @author      Portal People Development Team
 * @copyright   Copyright (c) 2026 Portal People (https://portalpeople.co.uk)
 */

namespace PortalPeople\palletways\components;

use Yii;
use yii\base\Component;
use yii\base\InvalidConfigException;
use GuzzleHttp\Client as GuzzleClient;
use GuzzleHttp\Exception\RequestException;

/**
 * PalletwaysClient
 *
 * Wraps the Palletways RESTful web API (https://api.palletways.com).
 *
 * Authentication is via the `apikey` query parameter, which must be obtained
 * from Palletways IT (itintegration@palletways.com / 01543 440206).
 *
 * All JSON requests include `inputformat=json&outputformat=json` query params.
 * Label endpoints return raw PDF content.
 *
 * API documentation:
 *   https://palletways.freshservice.com/support/solutions/articles/26000059775
 *   https://palletways.freshservice.com/support/solutions/articles/26000060769
 */
class PalletwaysClient extends Component
{
    // -------------------------------------------------------------------------
    // Configuration properties
    // -------------------------------------------------------------------------

    /** @var string Palletways REST API base URL. Default: https://api.palletways.com */
    public $apiUrl = 'https://api.palletways.com';

    /** @var string Customer API key issued by Palletways IT. Used in all API calls. */
    public $apiKey;

    /** @var bool When true, sets Confirm=yes in the manifest (sends directly to depot). */
    public $autoConfirm = false;

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

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

    // -------------------------------------------------------------------------
    // Internal state
    // -------------------------------------------------------------------------

    /** @var GuzzleClient */
    private $_http;

    // -------------------------------------------------------------------------
    // Lifecycle
    // -------------------------------------------------------------------------

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

        if (empty($this->apiUrl)) {
            throw new InvalidConfigException('Palletways API URL must be configured.');
        }

        if (empty($this->apiKey)) {
            throw new InvalidConfigException('Palletways API key must be configured.');
        }

        $this->_http = new GuzzleClient([
            'timeout' => $this->timeout,
            'headers' => [
                'Accept' => 'application/json',
            ],
        ]);
    }

    // =========================================================================
    // Public API methods
    // =========================================================================

    /**
     * Create a consignment on the Palletways system (CreateConsignment).
     *
     * @param array $manifest  Full manifest array as returned by ConsignmentForm::getManifest().
     * @param bool  $commit    If true, data is stored as a live consignment.
     *                         If false, data is only validated (no import occurs).
     * @return array Decoded JSON response.
     */
    public function createConsignment(array $manifest, bool $commit = false): array
    {
        $endpoint = $this->baseUrl() . '/createconsignment';

        $query = $this->defaultQuery();
        $query['inputformat'] = 'json';
        $query['commit']      = $commit ? 'true' : 'false';

        $json = json_encode($manifest);

        $this->log('POST /createconsignment (commit=' . ($commit ? 'true' : 'false') . ')', $manifest);

        try {
            $response = $this->_http->post($endpoint, [
                'query'       => $query,
                'form_params' => ['data' => $json],
            ]);

            $body = (string) $response->getBody();
            $data = json_decode($body, true) ?? [];

            $this->log('Response /createconsignment', $data);

            return $data;
        } catch (RequestException $e) {
            return $this->handleError('/createconsignment', $e);
        }
    }

    /**
     * Get consignment status using the Palletways Response ID.
     *
     * Returns dc_syscon (Palletways Tracking ID) and other details.
     *
     * @param string $responseId  Response ID returned by createConsignment.
     * @return array
     */
    public function getConStatusById(string $responseId): array
    {
        return $this->get('/constatusbyid/' . rawurlencode($responseId));
    }

    /**
     * Get consignment status using the customer-supplied consignment number.
     *
     * Returns Palletways Tracking ID and tracking link.
     *
     * @param string $conNo  Customer consignment number (the Number field).
     * @return array
     */
    public function getConStatusByConNo(string $conNo): array
    {
        return $this->get('/constatusbyconno/' . rawurlencode($conNo));
    }

    /**
     * Get consignment status using the Palletways Tracking ID.
     *
     * @param string $trackingId  Palletways Tracking ID (dc_syscon).
     * @return array
     */
    public function getConStatusByTrackingId(string $trackingId): array
    {
        return $this->get('/constatusbytrackingId/' . rawurlencode($trackingId));
    }

    /**
     * Get all consignments entered on a specific day.
     *
     * @param string $date  Date in YYYY-MM-DD format. Omit for today.
     * @return array
     */
    public function getConsEntered(string $date = ''): array
    {
        $path = '/consEntered';
        if (!empty($date)) {
            $path .= '/' . rawurlencode($date);
        }
        return $this->get($path);
    }

    /**
     * Get full consignment details using the Palletways Tracking ID.
     *
     * @param string $trackingId
     * @return array
     */
    public function getConsignment(string $trackingId): array
    {
        return $this->get('/getconsignment/' . rawurlencode($trackingId));
    }

    /**
     * Manifest (confirm and submit) a consignment from the customer account.
     *
     * Only applicable to consignments created with Confirm=no.
     *
     * @param string $responseId
     * @return array
     */
    public function manifestConsignment(string $responseId): array
    {
        return $this->get('/manifestconsignment/' . rawurlencode($responseId));
    }

    /**
     * Delete a consignment from the customer account.
     *
     * Only applicable to consignments created with Confirm=no.
     *
     * @param string $responseId
     * @return array
     */
    public function deleteConsignment(string $responseId): array
    {
        return $this->get('/deletepcconsignment/' . rawurlencode($responseId));
    }

    /**
     * Get tracking notes and tracking URL by customer consignment number.
     *
     * @param string $conNo  Customer consignment number.
     * @return array
     */
    public function getTrackingNotesByConNo(string $conNo): array
    {
        return $this->get('/gettrackingnotesbyconno/' . rawurlencode($conNo));
    }

    /**
     * Get all tracking note updates for a consignment by Palletways Tracking ID.
     *
     * @param string $trackingId  Palletways Tracking ID.
     * @return array
     */
    public function getTrackingNotes(string $trackingId): array
    {
        return $this->get('/gettrackingnotes/' . rawurlencode($trackingId));
    }

    /**
     * Get available services between two locations.
     *
     * Highly recommended to call before selecting a service level.
     *
     * @param string $consignmentType  D=Delivery, C=Collection, 3=ThirdParty
     * @param string $collectionCountry  ISO country code (e.g. GB)
     * @param string $collectionPostcode
     * @param string $deliveryCountry
     * @param string $deliveryPostcode
     * @return array
     */
    public function getAvailableServices(
        string $consignmentType,
        string $collectionCountry,
        string $collectionPostcode,
        string $deliveryCountry,
        string $deliveryPostcode
    ): array {
        $path = '/availableservices'
            . '/' . rawurlencode($consignmentType)
            . '/' . rawurlencode($collectionCountry)
            . '/' . rawurlencode($collectionPostcode)
            . '/' . rawurlencode($deliveryCountry)
            . '/' . rawurlencode($deliveryPostcode);

        return $this->get($path);
    }

    /**
     * Retrieve a consignment label as PDF by Response ID.
     *
     * @param string $responseId
     * @return string  Raw PDF binary content.
     */
    public function getLabelsById(string $responseId): string
    {
        return $this->getPdf('/GetLabelsById/' . rawurlencode($responseId));
    }

    /**
     * Retrieve a consignment label as PDF by consignment number.
     *
     * @param string $conNo  Customer consignment number.
     * @return string  Raw PDF binary content.
     */
    public function getLabelsByConNo(string $conNo): string
    {
        return $this->getPdf('/GetLabelsByConNo/' . rawurlencode($conNo));
    }

    /**
     * Get POD (Proof of Delivery) PDF by Palletways Tracking ID.
     *
     * @param string $trackingId
     * @return string  Raw PDF binary content.
     */
    public function getPodByTrackingId(string $trackingId): string
    {
        return $this->getPdf('/getpodbytrackingid/' . rawurlencode($trackingId));
    }

    /**
     * Test connectivity and API key validity.
     *
     * Uses the AvailableServices endpoint with known Palletways UK postcodes.
     *
     * @return array ['success' => bool, 'message' => string]
     */
    public function testConnection(): array
    {
        try {
            $result = $this->getAvailableServices('D', 'GB', 'WS138NE', 'GB', 'BS37');

            $code = $result['Response']['Status']['Code'] ?? null;

            if ($code === 'OK') {
                return ['success' => true, 'message' => 'Connection to Palletways API successful.'];
            }

            $desc = $result['Response']['Status']['Description'] ?? 'unknown';
            return [
                'success' => false,
                'message' => 'API reachable but returned: ' . $code . ' – ' . $desc,
            ];
        } catch (\Exception $e) {
            return ['success' => false, 'message' => $e->getMessage()];
        }
    }

    // =========================================================================
    // Private helpers
    // =========================================================================

    /**
     * Return the base URL (trimmed of trailing slash).
     */
    private function baseUrl(): string
    {
        return rtrim($this->apiUrl, '/');
    }

    /**
     * Default query parameters for all requests.
     */
    private function defaultQuery(): array
    {
        return [
            'apikey'       => $this->apiKey,
            'outputformat' => 'json',
        ];
    }

    /**
     * Execute a GET request and return decoded JSON.
     *
     * @param string $path  Path relative to base URL (with leading slash).
     * @param array  $extraQuery  Additional query parameters to merge.
     * @return array
     */
    private function get(string $path, array $extraQuery = []): array
    {
        $url = $this->baseUrl() . $path;
        $this->log('GET ' . $path, $extraQuery ?: null);

        try {
            $response = $this->_http->get($url, [
                'query' => array_merge($this->defaultQuery(), $extraQuery),
            ]);

            $body = (string) $response->getBody();
            $data = json_decode($body, true) ?? [];

            $this->log('Response ' . $path, $data);

            return $data;
        } catch (RequestException $e) {
            return $this->handleError($path, $e);
        }
    }

    /**
     * Execute a GET request and return binary PDF content.
     *
     * @param string $path
     * @return string  Raw binary content.
     * @throws \RuntimeException on HTTP or network errors.
     */
    private function getPdf(string $path): string
    {
        $url = $this->baseUrl() . $path;
        $this->log('GET (PDF) ' . $path);

        try {
            $response = $this->_http->get($url, [
                'query' => $this->defaultQuery(),
                'headers' => ['Accept' => 'application/pdf'],
            ]);

            return (string) $response->getBody();
        } catch (RequestException $e) {
            $msg = 'Palletways API error [' . $path . ']: ' . $e->getMessage();
            Yii::error($msg, 'palletways');
            throw new \RuntimeException($msg, (int) $e->getCode(), $e);
        }
    }

    /**
     * Handle a Guzzle request exception and return a structured error array.
     *
     * @param string           $path
     * @param RequestException $e
     * @return array
     */
    private function handleError(string $path, RequestException $e): array
    {
        $msg = 'Palletways API error [' . $path . ']: ' . $e->getMessage();
        Yii::error($msg, 'palletways');

        // Try to parse a JSON body from the error response
        if ($e->hasResponse()) {
            $body = (string) $e->getResponse()->getBody();
            $data = json_decode($body, true);
            if ($data !== null) {
                return $data;
            }
        }

        return [
            'Response' => [
                'Status' => [
                    'Code'        => 'ERROR',
                    'Description' => $e->getMessage(),
                ],
            ],
        ];
    }

    /**
     * Conditional debug/info logger.
     *
     * @param string $message
     * @param mixed  $context
     */
    private function log(string $message, $context = null): void
    {
        if (!$this->debug) {
            return;
        }
        $msg = '[Palletways] ' . $message;
        if ($context !== null) {
            $msg .= ' | ' . json_encode($context, JSON_UNESCAPED_SLASHES);
        }
        Yii::info($msg, 'palletways');
    }
}
