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

namespace PortalPeople\upn\components;

use Yii;
use yii\base\Component;

/**
 * UpnClient
 *
 * Wraps the UPN (United Pallet Network) External Links SOAP API.
 *
 * Authentication is per-call: Depot + Username + Password are passed as
 * parameters to every SOAP function. There is no token/session to manage.
 *
 * SOAP service: https://services.upn.co.uk:20675/ExternalLinks
 * WSDL:         https://services.upn.co.uk:20675/ExternalLinks?wsdl
 *
 * Key functions:
 *   AddConsignment_V4            – Submit a pallet consignment
 *   DeleteImportedCon            – Remove a new (unprocessed) consignment
 *   PalletLabels_V2_asString     – Retrieve PDF pallet labels as Base64
 *   PalletLabels_V2_asByte       – Retrieve PDF pallet labels as byte array
 *   GetPODAndTrackingByDepot_V2  – All tracking/POD data for the depot
 *   GetPODAndTrackingByCustomer_V2 – Tracking/POD for a specific customer
 *   GetPostcodeRestrictions      – Services available for a postcode district
 *   CON_GetRunAndDelTime_V2      – Run and expected delivery time
 *   Collection_GetStatus         – Status of a collection request
 *   CON_GetPalletsOnCon          – Pallet barcodes for a consignment
 */
class UpnClient extends Component
{
    // -----------------------------------------------------------------------
    // Configuration
    // -----------------------------------------------------------------------

    /** @var string UPN External Links SOAP API URL. */
    public string $apiUrl = 'https://services.upn.co.uk:20675/ExternalLinks';

    /** @var int UPN depot number. */
    public int $depot = 0;

    /** @var int UPN customer ID (unique per customer at the depot). */
    public int $customerId = 0;

    /** @var string UPN API username. */
    public string $username = '';

    /** @var string UPN API password. */
    public string $password = '';

    /** @var float Fuel surcharge rate as a decimal fraction. */
    public float $fuelSurcharge = 0.0;

    /** @var float Discount rate as a decimal fraction. */
    public float $discount = 0.0;

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

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

    /** @var \SoapClient|null */
    private ?\SoapClient $client = null;

    // -----------------------------------------------------------------------
    // Initialisation
    // -----------------------------------------------------------------------

    public function init(): void
    {
        parent::init();
        $this->client = $this->buildClient();
    }

    /**
     * Build and return a SoapClient, connecting to the UPN WSDL.
     *
     * The WSDL is cached for the life of the request. SSL verification is
     * enabled; set 'verify_peer' => false in options only if testing against
     * a non-production endpoint.
     */
    private function buildClient(): \SoapClient
    {
        $wsdl = rtrim($this->apiUrl, '/') . '?wsdl';

        $options = [
            'trace'              => $this->debug,
            'exceptions'         => true,
            'connection_timeout' => $this->timeout,
            'stream_context'     => stream_context_create([
                'ssl' => ['verify_peer' => true, 'verify_peer_name' => true],
            ]),
            'cache_wsdl' => WSDL_CACHE_BOTH,
        ];

        try {
            return new \SoapClient($wsdl, $options);
        } catch (\SoapFault $e) {
            Yii::error('[UpnClient] Failed to initialise SoapClient: ' . $e->getMessage(), 'upn');
            throw $e;
        }
    }

    // -----------------------------------------------------------------------
    // Internal helper
    // -----------------------------------------------------------------------

    /**
     * Invoke a SOAP method, logging request/response when debug is enabled.
     *
     * @param string $method  SOAP function name
     * @param array  $params  Associative array of parameters
     * @return mixed          Decoded response (stdClass or array)
     * @throws \SoapFault on SOAP error
     */
    private function call(string $method, array $params): mixed
    {
        if ($this->debug) {
            Yii::info("[UpnClient] CALL {$method} | params: " . json_encode($params), 'upn');
        }

        try {
            $result = $this->client->__soapCall($method, [$params]);

            if ($this->debug) {
                Yii::info("[UpnClient] RESPONSE {$method}: " . json_encode($result), 'upn');
            }

            return $result;
        } catch (\SoapFault $e) {
            Yii::error("[UpnClient] SOAP fault in {$method}: " . $e->getMessage(), 'upn');
            throw $e;
        }
    }

    // -----------------------------------------------------------------------
    // Consignment (input) endpoints
    // -----------------------------------------------------------------------

    /**
     * Create a pallet consignment through the UPN network (AddConsignment_V4).
     *
     * The $data array should contain the consignment fields as returned
     * by ConsignmentForm::getConsignmentData():
     *   - CustRef (string, max 15) – unique consignment reference
     *   - AdditionalRef (string, max 30) – secondary reference
     *   - DespatchDate (string, YYYY-MM-DD)
     *   - MainSvc (int) – 1=Priority, 2=Standard, 3=International, 4=Return
     *   - AdditionalSvc (int) – premium service; 0 for none
     *   - TailLift (int) – 1=required, 0=not required
     *   - ExtraSvc (int) – extra service; 0 for none
     *   - TotalWeight (int) – kg
     *   - SpecialInstructions (string, max 255)
     *   - PalletType (int) – 1=Full, 2=Euro, 3=Half, 4=Quarter, 5=Micro, 6=Quarter+
     *   - PalletSize (int) – 1=Normal, 2=Oversize, 3=OOG2, 4=OOG3
     *   - TotalPallets (int) – number of pallets (line-per-consignment approach)
     *   - DeliveryCoName (string, max 100)
     *   - DeliveryAdd1 (string, max 100)
     *   - DeliveryAdd2 (string, max 100)
     *   - DeliveryTown (string, max 100)
     *   - DeliveryCounty (string, max 100)
     *   - DeliveryPostcode (string, max 8)
     *   - ContactName (string, max 100)
     *   - ContactPhone (string, max 15)
     *   - ContactEmail (string, max 100)
     *   - DeliveryDateTime (string, YYYY-MM-DD HH:MM) – for Timed/Booked
     *   - PalletDescription (string) – free text contents description
     *   - PaperworkType (int) – 0=none, 1=electronic, 2=hard-copy
     *   - CustPaperwork (int) – number of paperwork pages
     *   - LocalArea (int) – 0=network routing, 1=local only, or depot number
     *
     * @param array $data Consignment payload
     * @return mixed SOAP response (typically contains ConBarcode)
     * @throws \SoapFault
     */
    public function addConsignment(array $data): mixed
    {
        return $this->call('AddConsignment_V4', array_merge($data, [
            'Depot'    => $this->depot,
            'Username' => $this->username,
            'Password' => $this->password,
        ]));
    }

    /**
     * Delete a new (unprocessed) consignment from UPN.
     *
     * Can only be called before the consignment has been processed by UPN.
     *
     * @param string $conBarcode UPN consignment barcode
     * @return mixed SOAP response
     * @throws \SoapFault
     */
    public function deleteConsignment(string $conBarcode): mixed
    {
        return $this->call('DeleteImportedCon', [
            'ConBarcode' => $conBarcode,
            'Username'   => $this->username,
            'Password'   => $this->password,
        ]);
    }

    // -----------------------------------------------------------------------
    // Label endpoints
    // -----------------------------------------------------------------------

    /**
     * Retrieve pallet labels as a Base64-encoded PDF string.
     *
     * @param string $conBarcode UPN consignment barcode
     * @return string Base64-encoded PDF
     * @throws \SoapFault
     */
    public function getPalletLabelsAsString(string $conBarcode): string
    {
        $result = $this->call('PalletLabels_V2_asString', [
            'ConBarcode' => $conBarcode,
            'Username'   => $this->username,
            'Password'   => $this->password,
        ]);

        return is_string($result) ? $result : (string) ($result->PalletLabels_V2_asStringResult ?? '');
    }

    /**
     * Retrieve pallet labels as raw PDF bytes.
     *
     * @param string $conBarcode UPN consignment barcode
     * @return string Raw PDF binary data
     * @throws \SoapFault
     */
    public function getPalletLabelsAsBytes(string $conBarcode): string
    {
        $result = $this->call('PalletLabels_V2_asByte', [
            'ConBarcode' => $conBarcode,
            'Username'   => $this->username,
            'Password'   => $this->password,
        ]);

        return is_string($result) ? $result : (string) ($result->PalletLabels_V2_asByteResult ?? '');
    }

    // -----------------------------------------------------------------------
    // Tracking & POD endpoints
    // -----------------------------------------------------------------------

    /**
     * Get all tracking and POD data for the depot since last call.
     *
     * Returns all data logged since the function was last called.
     *
     * @return mixed Array of trkConsignment structures
     * @throws \SoapFault
     */
    public function getPodAndTrackingByDepot(): mixed
    {
        return $this->call('GetPODAndTrackingByDepot_V2', [
            'Depot'    => $this->depot,
            'Username' => $this->username,
            'Password' => $this->password,
        ]);
    }

    /**
     * Get tracking and POD data for a specific customer.
     *
     * @param int $customerId The depot's unique customer ID
     * @return mixed Array of trkConsignment structures
     * @throws \SoapFault
     */
    public function getPodAndTrackingByCustomer(int $customerId = 0): mixed
    {
        return $this->call('GetPODAndTrackingByCustomer_V2', [
            'Depot'    => $this->depot,
            'Customer' => $customerId ?: $this->customerId,
            'Username' => $this->username,
            'Password' => $this->password,
        ]);
    }

    /**
     * Get run and expected delivery time for consignments.
     *
     * @param array $barcodes   Array of UPN consignment barcodes (preferred)
     * @param array $conNumbers Array of consignment numbers (alternative to barcodes)
     * @return mixed List of string arrays with delivery info
     * @throws \SoapFault
     */
    public function getRunAndDeliveryTime(array $barcodes = [], array $conNumbers = []): mixed
    {
        return $this->call('CON_GetRunAndDelTime_V2', [
            'Barcodes'   => $barcodes,
            'ConNumbers' => $conNumbers,
            'Depot'      => $this->depot,
            'Username'   => $this->username,
            'Password'   => $this->password,
        ]);
    }

    /**
     * Get the status of one or more collections.
     *
     * Possible status values: Customer entered, Sending Depot Approved,
     * Accepted, Goods Collected, Goods Unavailable, Rejected,
     * Cancellation Requested, Cancelled, Cancellation Request Rejected.
     *
     * @param array $collectionBarcodes Array of UPN collection barcodes
     * @return mixed Dictionary of barcode => status string
     * @throws \SoapFault
     */
    public function getCollectionStatus(array $collectionBarcodes): mixed
    {
        return $this->call('Collection_GetStatus', [
            'CollectionBarcodes' => $collectionBarcodes,
            'Depot'              => $this->depot,
            'Username'           => $this->username,
            'Password'           => $this->password,
        ]);
    }

    /**
     * Get all pallet barcodes for a given consignment barcode.
     *
     * @param string $conBarcode UPN consignment barcode
     * @return mixed Array of pallet barcode strings
     * @throws \SoapFault
     */
    public function getPalletsOnConsignment(string $conBarcode): mixed
    {
        return $this->call('CON_GetPalletsOnCon', [
            'ConBarcode' => $conBarcode,
            'Username'   => $this->username,
            'Password'   => $this->password,
        ]);
    }

    // -----------------------------------------------------------------------
    // Services / postcode endpoint
    // -----------------------------------------------------------------------

    /**
     * Get service and pallet type restrictions for a postcode district.
     *
     * Pass "All Postcodes" to download all 3000+ postcode districts.
     *
     * @param string $postcode Postcode district, e.g. "AB10"
     * @return mixed Array of svcPostcode structures
     * @throws \SoapFault
     */
    public function getPostcodeRestrictions(string $postcode): mixed
    {
        return $this->call('GetPostcodeRestrictions', [
            'Postcode' => $postcode,
            'Username' => $this->username,
            'Password' => $this->password,
        ]);
    }

    // -----------------------------------------------------------------------
    // Delivery image / signature endpoints
    // -----------------------------------------------------------------------

    /**
     * Get a delivery signature image as Base64 string (jpg or png).
     *
     * @param string $conBarcode     UPN consignment barcode
     * @param int    $imageId        SignatureImageID from tracking output
     * @return string Base64-encoded image
     * @throws \SoapFault
     */
    public function getDeliverySignatureAsString(string $conBarcode, int $imageId): string
    {
        $result = $this->call('DeliverySignature_Get_AsString', [
            'ConBarcode' => $conBarcode,
            'ImageID'    => $imageId,
            'Username'   => $this->username,
            'Password'   => $this->password,
        ]);

        return is_string($result) ? $result : '';
    }

    /**
     * Get a delivery point photograph as Base64 string.
     *
     * @param string $conBarcode UPN consignment barcode
     * @param int    $imageId    PhotoID from trkPOD tracking output
     * @return string Base64-encoded image
     * @throws \SoapFault
     */
    public function getDeliveryPhotoAsString(string $conBarcode, int $imageId): string
    {
        $result = $this->call('DeliveryPhoto_Get_AsString', [
            'ConBarcode' => $conBarcode,
            'ImageID'    => $imageId,
            'Username'   => $this->username,
            'Password'   => $this->password,
        ]);

        return is_string($result) ? $result : '';
    }

    // -----------------------------------------------------------------------
    // Connection test
    // -----------------------------------------------------------------------

    /**
     * Test the API connection by fetching postcode restrictions for "SW1A".
     *
     * @return array ['success' => bool, 'message' => string]
     */
    public function testConnection(): array
    {
        try {
            $this->getPostcodeRestrictions('SW1A');
            return ['success' => true, 'message' => 'Connection successful.'];
        } catch (\SoapFault $e) {
            return ['success' => false, 'message' => 'SOAP fault: ' . $e->getMessage()];
        } catch (\Exception $e) {
            return ['success' => false, 'message' => $e->getMessage()];
        }
    }
}
