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

namespace PortalPeople\palletforce\components;

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

/**
 * PalletforceClient
 *
 * Wraps the Palletforce Alliance REST API.
 * All methods use HTTP POST with a JSON body containing `accessKey`.
 *
 * Rate limit: maximum one call every 30 minutes. Error code -6 is returned when
 * the limit is reached.
 *
 * Base URLs and API keys are issued by Palletforce IT after testing sign-off.
 */
class PalletforceClient extends Component
{
    // -------------------------------------------------------------------------
    // API endpoint path constants (appended to $apiUrl)
    // -------------------------------------------------------------------------
    const ENDPOINT_UPLOAD_MANIFEST        = '/UploadManifest';
    const ENDPOINT_ACCEPT_CONSIGNMENTS    = '/AcceptConsignments';
    const ENDPOINT_DELETE_CONSIGNMENT     = '/DeleteConsignment';
    const ENDPOINT_DOWNLOAD_MANIFEST      = '/DownloadManifest';
    const ENDPOINT_DOWNLOAD_SHIPPING_INFO = '/DownloadShippingInformation';
    const ENDPOINT_DOWNLOAD_TRACKING      = '/DownloadTrackingData';
    const ENDPOINT_DOWNLOAD_ETA           = '/DownloadETATimes';
    const ENDPOINT_DOWNLOAD_HUB_BUILDING  = '/DownloadHubBuilding';
    const ENDPOINT_PRINT_LABELS           = '/PrintLabels';

    // -------------------------------------------------------------------------
    // Configuration properties
    // -------------------------------------------------------------------------

    /** @var string Palletforce REST API base URL. */
    public $apiUrl;

    /** @var string Unique access key provided by Palletforce. Sent in every request body. */
    public $accessKey;

    /** @var string Customer account number issued by Palletforce. */
    public $accountNumber;

    /** @var string Requesting depot code (3 chars). */
    public $depotCode;

    /** @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('Palletforce API URL must be configured.');
        }

        if (empty($this->accessKey)) {
            throw new InvalidConfigException('Palletforce accessKey must be configured.');
        }

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

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

    /**
     * Upload / create a consignment (UploadManifest).
     *
     * The `accessKey`, `accountNumber` and `depotCode` are automatically
     * injected into the consignment if not already present.
     *
     * @param array  $consignment  Single consignment array as per API spec.
     * @param string $uniqueRef    Caller-supplied unique transaction reference.
     * @param array  $collectionAddress  Collection address fields.
     * @param array  $deliveryAddress    Delivery address fields.
     * @return array API response.
     */
    public function uploadManifest(
        array $consignment,
        string $uniqueRef,
        array $collectionAddress,
        array $deliveryAddress
    ): array {
        // Inject account number and depot if not set by caller
        if (empty($consignment['CustomerAccountNumber'])) {
            $consignment['CustomerAccountNumber'] = $this->accountNumber;
        }
        if (empty($consignment['requestingDepot'])) {
            $consignment['requestingDepot'] = $this->depotCode;
        }

        $payload = [
            'accessKey'               => $this->accessKey,
            'uniqueTransactionNumber' => $uniqueRef,
            'collectionAddress'       => $collectionAddress,
            'deliveryAddress'         => $deliveryAddress,
            'consignments'            => [$consignment],
        ];

        return $this->post(self::ENDPOINT_UPLOAD_MANIFEST, $payload);
    }

    /**
     * Accept one or more pending consignments.
     *
     * @param string[] $trackingCodes
     * @param string   $uniqueRef
     * @return array
     */
    public function acceptConsignments(array $trackingCodes, string $uniqueRef): array
    {
        return $this->post(self::ENDPOINT_ACCEPT_CONSIGNMENTS, [
            'accessKey'               => $this->accessKey,
            'trackingCodes'           => $trackingCodes,
            'uniqueTransactionNumber' => $uniqueRef,
        ]);
    }

    /**
     * Delete a consignment before acceptance.
     *
     * @param string $trackingCode
     * @param string $uniqueRef
     * @return array
     */
    public function deleteConsignment(string $trackingCode, string $uniqueRef): array
    {
        return $this->post(self::ENDPOINT_DELETE_CONSIGNMENT, [
            'accessKey'               => $this->accessKey,
            'trackingCode'            => $trackingCode,
            'uniqueTransactionNumber' => $uniqueRef,
        ]);
    }

    /**
     * Download consignments from Alliance.
     *
     * Supply one or more of: manifestDate, trackingCode, serviceCommitmentDate,
     * lastUpdated.
     *
     * @param array $criteria  e.g. ['manifestDate' => '20260318']
     * @return array
     */
    public function downloadManifest(array $criteria): array
    {
        return $this->post(self::ENDPOINT_DOWNLOAD_MANIFEST, array_merge(
            ['accessKey' => $this->accessKey],
            $criteria
        ));
    }

    /**
     * Download shipping information for a consignment.
     *
     * @param string $trackingCode
     * @return array
     */
    public function downloadShippingInfo(string $trackingCode): array
    {
        return $this->post(self::ENDPOINT_DOWNLOAD_SHIPPING_INFO, [
            'accessKey'   => $this->accessKey,
            'TrackingCode' => $trackingCode,
        ]);
    }

    /**
     * Download tracking events and POD for a consignment.
     *
     * @param string $trackingNumber
     * @param string $uniqueRef
     * @param array  $filters  Optional: manifestDate, serviceCommitmentDate, lastUpdated
     * @return array
     */
    public function downloadTrackingData(
        string $trackingNumber,
        string $uniqueRef,
        array $filters = []
    ): array {
        return $this->post(self::ENDPOINT_DOWNLOAD_TRACKING, array_merge([
            'accessKey'               => $this->accessKey,
            'trackingNumber'          => $trackingNumber,
            'uniqueTransactionNumber' => $uniqueRef,
        ], $filters));
    }

    /**
     * Get ETA times for one or more consignments.
     *
     * @param string[] $trackingCodes
     * @param string   $uniqueRef
     * @return array
     */
    public function downloadETATimes(array $trackingCodes, string $uniqueRef): array
    {
        return $this->post(self::ENDPOINT_DOWNLOAD_ETA, [
            'accessKey'               => $this->accessKey,
            'trackingCodes'           => $trackingCodes,
            'uniqueTransactionNumber' => $uniqueRef,
        ]);
    }

    /**
     * Determine hub building for one or more UK postcodes.
     *
     * @param string[] $postcodes  e.g. ['BS37', 'NG2']
     * @param string   $uniqueRef
     * @param string   $country    ISO 3166-1 alpha-2 (default 'GB')
     * @return array
     */
    public function downloadHubBuilding(
        array $postcodes,
        string $uniqueRef,
        string $country = 'GB'
    ): array {
        return $this->post(self::ENDPOINT_DOWNLOAD_HUB_BUILDING, [
            'accessKey'               => $this->accessKey,
            'postcodes'               => $postcodes,
            'country'                 => $country,
            'uniqueTransactionNumber' => $uniqueRef,
        ]);
    }

    /**
     * Print labels for a consignment (returns base64-encoded thermal label images).
     *
     * Leave $palletNumber null (or 0) for all pallets.
     *
     * @param string   $trackingNumber
     * @param string   $uniqueRef
     * @param int|null $palletNumber
     * @return array
     */
    public function printLabels(
        string $trackingNumber,
        string $uniqueRef,
        ?int $palletNumber = null
    ): array {
        return $this->post(self::ENDPOINT_PRINT_LABELS, [
            'accessKey'               => $this->accessKey,
            'trackingNumber'          => $trackingNumber,
            'palletNumber'            => $palletNumber,
            'uniqueTransactionNumber' => $uniqueRef,
        ]);
    }

    /**
     * Test connectivity by attempting a hub lookup for a known postcode.
     *
     * @return array ['success' => bool, 'message' => string]
     */
    public function testConnection(): array
    {
        try {
            $response = $this->downloadHubBuilding(['BS37'], 'TEST-' . time());

            if ($response['success'] ?? false) {
                return ['success' => true, 'message' => 'Connection to Palletforce API successful.'];
            }

            return [
                'success' => false,
                'message' => 'API reachable but returned error: ' . ($response['message'] ?? 'unknown'),
            ];
        } catch (\Exception $e) {
            return ['success' => false, 'message' => $e->getMessage()];
        }
    }

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

    /**
     * Execute a POST request to the given endpoint.
     *
     * @param string $endpoint  Path relative to $apiUrl.
     * @param array  $payload   Data to JSON-encode.
     * @return array Decoded response body.
     * @throws \RuntimeException on HTTP or network errors.
     */
    private function post(string $endpoint, array $payload): array
    {
        $this->log("POST {$endpoint}", $payload);

        try {
            $response = $this->_http->post($endpoint, ['json' => $payload]);
            $body     = (string) $response->getBody();
            $data     = json_decode($body, true) ?? [];

            $this->log("Response {$endpoint}", $data);

            return $data;
        } catch (RequestException $e) {
            $msg = "Palletforce API error [{$endpoint}]: " . $e->getMessage();
            Yii::error($msg, 'palletforce');
            throw new \RuntimeException($msg, (int) $e->getCode(), $e);
        }
    }

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