<?php
declare(strict_types=1);

namespace App\Modules\Shipments;

use App\Core\Exceptions\IntegrationConfigException;
use App\Core\Http\SslCertificateResolver;
use App\Core\Exceptions\ShipmentException;
use App\Modules\Orders\OrdersRepository;
use App\Modules\Settings\CompanySettingsRepository;
use App\Modules\Settings\InpostIntegrationRepository;
use RuntimeException;
use Throwable;

final class InpostShipmentService implements ShipmentProviderInterface
{
    private const API_BASE_PRODUCTION = 'https://api-shipx-pl.easypack24.net/v1';
    private const API_BASE_SANDBOX = 'https://sandbox-api-shipx-pl.easypack24.net/v1';

    public function __construct(
        private readonly InpostIntegrationRepository $inpostRepository,
        private readonly ShipmentPackageRepository $packages,
        private readonly CompanySettingsRepository $companySettings,
        private readonly OrdersRepository $ordersRepository
    ) {
    }

    public function code(): string
    {
        return 'inpost';
    }

    /**
     * @return array<int, array<string, mixed>>
     */
    public function getDeliveryServices(): array
    {
        return [
            ['id' => 'inpost_locker_standard', 'name' => 'InPost Paczkomat - Standard', 'type' => 'locker'],
            ['id' => 'inpost_courier_standard', 'name' => 'InPost Kurier - Standard', 'type' => 'courier'],
            ['id' => 'inpost_courier_express', 'name' => 'InPost Kurier - Express', 'type' => 'courier'],
        ];
    }

    /**
     * @param array<string, mixed> $formData
     * @return array<string, mixed>
     */
    public function createShipment(int $orderId, array $formData): array
    {
        $order = $this->ordersRepository->findDetails($orderId);
        if ($order === null) {
            throw new ShipmentException('Zamowienie nie znalezione.');
        }

        $token = $this->resolveToken();
        $settings = $this->inpostRepository->getSettings();
        $organizationId = trim((string) ($settings['organization_id'] ?? ''));
        if ($organizationId === '') {
            throw new IntegrationConfigException('Brak organization_id w konfiguracji InPost.');
        }

        $company = $this->companySettings->getSettings();
        $sender = $this->companySettings->getSenderAddress();
        $this->validateSenderAddress($sender);

        $receiver = $this->buildReceiverFromOrder($order, $formData);
        $senderPayload = $this->buildSenderPayload($sender);

        $serviceType = $this->resolveServiceType($formData, $settings);
        $parcelPayload = $this->buildParcelPayload($formData, $settings, $company, $serviceType);

        $apiPayload = [
            'receiver' => $receiver,
            'sender' => $senderPayload,
            'parcels' => [$parcelPayload],
            'service' => $serviceType,
            'reference' => $this->buildReference($order, $orderId),
        ];

        $codAmount = (float) ($formData['cod_amount'] ?? 0);
        if ($codAmount > 0) {
            $apiPayload['cod'] = [
                'amount' => number_format($codAmount, 2, '.', ''),
                'currency' => strtoupper(trim((string) ($formData['cod_currency'] ?? 'PLN'))),
            ];
        }

        $insuranceAmount = (float) ($formData['insurance_amount'] ?? 0);
        if ($insuranceAmount <= 0 && !empty($settings['auto_insurance_value'])) {
            $orderData = is_array($order['order'] ?? null) ? $order['order'] : [];
            $totalWithTax = (float) ($orderData['total_with_tax'] ?? 0);
            if ($totalWithTax > 0) {
                $insuranceAmount = $totalWithTax;
            }
        }
        if ($insuranceAmount > 0) {
            $apiPayload['insurance'] = [
                'amount' => number_format($insuranceAmount, 2, '.', ''),
                'currency' => strtoupper(trim((string) ($formData['insurance_currency'] ?? 'PLN'))),
            ];
        }

        $labelFormat = trim((string) ($formData['label_format'] ?? ($settings['label_format'] ?? 'Pdf')));

        $packageId = $this->packages->create([
            'order_id' => $orderId,
            'provider' => 'inpost',
            'delivery_method_id' => $serviceType,
            'credentials_id' => null,
            'command_id' => null,
            'status' => 'pending',
            'carrier_id' => 'inpost',
            'package_type' => $parcelPayload['dimensions'] ? 'PACKAGE' : 'LOCKER',
            'weight_kg' => isset($parcelPayload['weight']['amount']) ? (float) $parcelPayload['weight']['amount'] : null,
            'length_cm' => isset($parcelPayload['dimensions']['length']) ? (float) $parcelPayload['dimensions']['length'] : null,
            'width_cm' => isset($parcelPayload['dimensions']['width']) ? (float) $parcelPayload['dimensions']['width'] : null,
            'height_cm' => isset($parcelPayload['dimensions']['height']) ? (float) $parcelPayload['dimensions']['height'] : null,
            'insurance_amount' => $insuranceAmount > 0 ? $insuranceAmount : null,
            'insurance_currency' => $insuranceAmount > 0 ? strtoupper(trim((string) ($formData['insurance_currency'] ?? 'PLN'))) : null,
            'cod_amount' => $codAmount > 0 ? $codAmount : null,
            'cod_currency' => $codAmount > 0 ? strtoupper(trim((string) ($formData['cod_currency'] ?? 'PLN'))) : null,
            'label_format' => $labelFormat,
            'receiver_point_id' => trim((string) ($formData['receiver_point_id'] ?? '')),
            'sender_point_id' => trim((string) ($formData['sender_point_id'] ?? '')),
            'reference_number' => $apiPayload['reference'],
            'payload_json' => $apiPayload,
        ]);

        $env = (string) ($settings['environment'] ?? 'sandbox');
        $url = $this->apiBaseUrl($env) . '/organizations/' . rawurlencode($organizationId) . '/shipments';

        try {
            $response = $this->apiRequest('POST', $url, $token, $apiPayload);
        } catch (Throwable $exception) {
            $this->packages->update($packageId, [
                'status' => 'error',
                'error_message' => $exception->getMessage(),
            ]);
            throw $exception;
        }

        $shipmentId = trim((string) ($response['id'] ?? ''));
        $trackingNumber = trim((string) ($response['tracking_number'] ?? ''));
        $status = strtolower(trim((string) ($response['status'] ?? 'created')));

        $this->packages->update($packageId, [
            'shipment_id' => $shipmentId !== '' ? $shipmentId : null,
            'tracking_number' => $trackingNumber !== '' ? $trackingNumber : null,
            'status' => $status === 'created' || $status === 'confirmed' ? 'created' : 'pending',
            'payload_json' => $response,
        ]);

        return [
            'package_id' => $packageId,
            'command_id' => $shipmentId,
        ];
    }

    /**
     * @return array<string, mixed>
     */
    public function checkCreationStatus(int $packageId): array
    {
        $package = $this->packages->findById($packageId);
        if ($package === null) {
            throw new ShipmentException('Paczka nie znaleziona.');
        }

        $shipmentId = trim((string) ($package['shipment_id'] ?? ''));
        if ($shipmentId === '') {
            return ['status' => 'error', 'error' => 'Brak shipment_id — przesylka nie zostala utworzona.'];
        }

        $token = $this->resolveToken();
        $settings = $this->inpostRepository->getSettings();
        $env = (string) ($settings['environment'] ?? 'sandbox');
        $url = $this->apiBaseUrl($env) . '/shipments/' . rawurlencode($shipmentId);

        $response = $this->apiRequest('GET', $url, $token);
        $status = strtolower(trim((string) ($response['status'] ?? '')));
        $trackingNumber = trim((string) ($response['tracking_number'] ?? ''));

        if (in_array($status, ['created', 'confirmed', 'dispatched', 'collected', 'delivered'], true)) {
            $this->packages->update($packageId, [
                'status' => 'created',
                'tracking_number' => $trackingNumber !== '' ? $trackingNumber : null,
                'payload_json' => $response,
            ]);

            return [
                'status' => 'created',
                'shipment_id' => $shipmentId,
                'tracking_number' => $trackingNumber,
            ];
        }

        if (in_array($status, ['cancelled', 'expired'], true)) {
            $this->packages->update($packageId, [
                'status' => 'error',
                'error_message' => 'Przesylka anulowana/wygasla (status: ' . $status . ')',
                'payload_json' => $response,
            ]);

            return ['status' => 'error', 'error' => 'Przesylka: ' . $status];
        }

        return ['status' => 'in_progress'];
    }

    /**
     * @return array<string, mixed>
     */
    public function downloadLabel(int $packageId, string $storagePath): array
    {
        $package = $this->packages->findById($packageId);
        if ($package === null) {
            throw new ShipmentException('Paczka nie znaleziona.');
        }

        $shipmentId = trim((string) ($package['shipment_id'] ?? ''));
        if ($shipmentId === '') {
            throw new ShipmentException('Przesylka nie zostala jeszcze utworzona.');
        }

        $token = $this->resolveToken();
        $settings = $this->inpostRepository->getSettings();
        $env = (string) ($settings['environment'] ?? 'sandbox');
        $labelFormat = trim((string) ($package['label_format'] ?? ($settings['label_format'] ?? 'Pdf')));

        $url = $this->apiBaseUrl($env) . '/shipments/' . rawurlencode($shipmentId) . '/label';
        $queryParams = ['format' => $labelFormat, 'type' => 'normal'];
        $url .= '?' . http_build_query($queryParams);

        $binary = $this->apiRequestRaw('GET', $url, $token);

        $dir = rtrim($storagePath, '/\\') . '/labels';
        if (!is_dir($dir)) {
            mkdir($dir, 0775, true);
        }

        $ext = strtolower($labelFormat) === 'zpl' ? 'zpl' : 'pdf';
        $filename = 'label_' . $packageId . '_inpost_' . $shipmentId . '.' . $ext;
        $filePath = $dir . '/' . $filename;
        file_put_contents($filePath, $binary);

        $updateFields = [
            'status' => 'label_ready',
            'label_path' => 'labels/' . $filename,
        ];

        if (trim((string) ($package['tracking_number'] ?? '')) === '') {
            try {
                $detailsUrl = $this->apiBaseUrl($env) . '/shipments/' . rawurlencode($shipmentId);
                $details = $this->apiRequest('GET', $detailsUrl, $token);
                $trackingNumber = trim((string) ($details['tracking_number'] ?? ''));
                if ($trackingNumber !== '') {
                    $updateFields['tracking_number'] = $trackingNumber;
                }
            } catch (Throwable) {
            }
        }

        $this->packages->update($packageId, $updateFields);

        return [
            'label_path' => 'labels/' . $filename,
            'full_path' => $filePath,
        ];
    }

    private function resolveToken(): string
    {
        $token = $this->inpostRepository->getDecryptedToken();
        if ($token === null || trim($token) === '') {
            throw new IntegrationConfigException('Brak tokenu API InPost. Skonfiguruj w Ustawienia > Integracje > InPost.');
        }
        return trim($token);
    }

    private function apiBaseUrl(string $environment): string
    {
        return strtolower(trim($environment)) === 'production'
            ? self::API_BASE_PRODUCTION
            : self::API_BASE_SANDBOX;
    }

    /**
     * @param array<string, mixed>|null $body
     * @return array<string, mixed>
     */
    private function apiRequest(string $method, string $url, string $token, ?array $body = null): array
    {
        $binary = $this->apiRequestRaw($method, $url, $token, $body, 'application/json');
        $json = json_decode($binary, true);
        if (!is_array($json)) {
            throw new ShipmentException('Nieprawidlowa odpowiedz JSON z InPost API.');
        }

        return $json;
    }

    /**
     * @param array<string, mixed>|null $body
     */
    private function apiRequestRaw(
        string $method,
        string $url,
        string $token,
        ?array $body = null,
        string $accept = 'application/pdf'
    ): string {
        $ch = curl_init($url);
        if ($ch === false) {
            throw new ShipmentException('Nie udalo sie zainicjowac polaczenia z InPost API.');
        }

        $headers = [
            'Authorization: Bearer ' . $token,
            'Accept: ' . $accept,
        ];

        $opts = [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT => 30,
            CURLOPT_CONNECTTIMEOUT => 10,
            CURLOPT_SSL_VERIFYPEER => true,
            CURLOPT_SSL_VERIFYHOST => 2,
            CURLOPT_CUSTOMREQUEST => $method,
        ];

        $caPath = SslCertificateResolver::resolve();
        if ($caPath !== null) {
            $opts[CURLOPT_CAINFO] = $caPath;
        }

        if ($body !== null) {
            $jsonBody = json_encode($body, JSON_UNESCAPED_UNICODE);
            $headers[] = 'Content-Type: application/json';
            $opts[CURLOPT_POSTFIELDS] = $jsonBody;
        }

        $opts[CURLOPT_HTTPHEADER] = $headers;
        curl_setopt_array($ch, $opts);

        $responseBody = curl_exec($ch);
        $httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $curlError = curl_error($ch);
        $ch = null;

        if ($responseBody === false) {
            throw new ShipmentException('Blad polaczenia z InPost API: ' . $curlError);
        }

        if ($httpCode < 200 || $httpCode >= 300) {
            $errorMsg = $this->extractApiErrorMessage((string) $responseBody, $httpCode);
            throw new ShipmentException($errorMsg);
        }

        return (string) $responseBody;
    }

    private function extractApiErrorMessage(string $body, int $httpCode): string
    {
        $json = json_decode($body, true);
        if (is_array($json)) {
            $message = trim((string) ($json['message'] ?? ''));
            if ($message !== '') {
                return 'InPost API [HTTP ' . $httpCode . ']: ' . $message;
            }
            $details = $json['details'] ?? $json['error'] ?? null;
            if (is_string($details) && trim($details) !== '') {
                return 'InPost API [HTTP ' . $httpCode . ']: ' . trim($details);
            }
        }

        return 'InPost API zwrocilo blad HTTP ' . $httpCode;
    }

    /**
     * @param array<string, mixed> $orderDetails
     * @param array<string, mixed> $formData
     * @return array<string, mixed>
     */
    private function buildReceiverFromOrder(array $orderDetails, array $formData): array
    {
        $addresses = is_array($orderDetails['addresses'] ?? null) ? $orderDetails['addresses'] : [];
        $deliveryAddr = null;
        $customerAddr = null;
        foreach ($addresses as $addr) {
            $type = (string) ($addr['address_type'] ?? '');
            if ($type === 'delivery') {
                $deliveryAddr = $addr;
            }
            if ($type === 'customer') {
                $customerAddr = $addr;
            }
        }

        $addr = $deliveryAddr ?? $customerAddr ?? [];

        $name = trim((string) ($formData['receiver_name'] ?? ($addr['name'] ?? '')));
        $company = trim((string) ($formData['receiver_company'] ?? ($addr['company_name'] ?? '')));
        $phone = trim((string) ($formData['receiver_phone'] ?? ($addr['phone'] ?? '')));
        $email = trim((string) ($formData['receiver_email'] ?? ($addr['email'] ?? '')));

        $receiver = [
            'name' => $name !== '' ? $name : ($company !== '' ? $company : 'Odbiorca'),
            'phone' => $phone,
            'email' => $email,
        ];

        if ($company !== '') {
            $receiver['company_name'] = $company;
        }

        $street = trim((string) ($formData['receiver_street'] ?? ($addr['street_name'] ?? '')));
        $city = trim((string) ($formData['receiver_city'] ?? ($addr['city'] ?? '')));
        $postalCode = trim((string) ($formData['receiver_postal_code'] ?? ($addr['zip_code'] ?? '')));
        $countryCode = strtoupper(trim((string) ($formData['receiver_country_code'] ?? ($addr['country'] ?? 'PL'))));

        $receiver['address'] = [
            'street' => $street,
            'city' => $city,
            'post_code' => $postalCode,
            'country_code' => $countryCode,
        ];

        return $receiver;
    }

    /**
     * @param array<string, mixed> $sender
     * @return array<string, mixed>
     */
    private function buildSenderPayload(array $sender): array
    {
        $name = trim((string) ($sender['name'] ?? ''));
        $company = trim((string) ($sender['company'] ?? ''));

        return [
            'name' => $name !== '' ? $name : ($company !== '' ? $company : 'Nadawca'),
            'company_name' => $company !== '' ? $company : null,
            'phone' => trim((string) ($sender['phone'] ?? '')),
            'email' => trim((string) ($sender['email'] ?? '')),
            'address' => [
                'street' => trim((string) ($sender['street'] ?? '')),
                'city' => trim((string) ($sender['city'] ?? '')),
                'post_code' => trim((string) ($sender['postalCode'] ?? '')),
                'country_code' => strtoupper(trim((string) ($sender['countryCode'] ?? 'PL'))),
            ],
        ];
    }

    /**
     * @param array<string, mixed> $formData
     * @param array<string, mixed> $settings
     */
    private function resolveServiceType(array $formData, array $settings): string
    {
        $deliveryMethodId = trim((string) ($formData['delivery_method_id'] ?? ''));
        if ($deliveryMethodId !== '') {
            return $deliveryMethodId;
        }

        $pointId = trim((string) ($formData['receiver_point_id'] ?? ''));
        if ($pointId !== '') {
            return 'inpost_locker_standard';
        }

        $dispatchMethod = (string) ($settings['default_dispatch_method'] ?? 'pop');
        if ($dispatchMethod === 'parcel_locker') {
            return 'inpost_locker_standard';
        }

        return 'inpost_courier_standard';
    }

    /**
     * @param array<string, mixed> $formData
     * @param array<string, mixed> $settings
     * @param array<string, mixed> $company
     * @return array<string, mixed>
     */
    private function buildParcelPayload(array $formData, array $settings, array $company, string $serviceType): array
    {
        $isLocker = str_contains($serviceType, 'locker');

        if ($isLocker) {
            $size = trim((string) ($formData['locker_size'] ?? ($settings['default_locker_size'] ?? 'small')));
            if (!in_array($size, ['small', 'medium', 'large'], true)) {
                $size = 'small';
            }

            $targetPointId = trim((string) ($formData['receiver_point_id'] ?? ''));

            $parcel = [
                'template' => $size,
            ];
            if ($targetPointId !== '') {
                $parcel['target_point'] = $targetPointId;
            }

            return $parcel;
        }

        $lengthCm = (float) ($formData['length_cm'] ?? ($settings['default_courier_length'] ?? $company['default_package_length_cm'] ?? 20));
        $widthCm = (float) ($formData['width_cm'] ?? ($settings['default_courier_width'] ?? $company['default_package_width_cm'] ?? 15));
        $heightCm = (float) ($formData['height_cm'] ?? ($settings['default_courier_height'] ?? $company['default_package_height_cm'] ?? 8));
        $weightKg = (float) ($formData['weight_kg'] ?? ($company['default_package_weight_kg'] ?? 1));

        return [
            'dimensions' => [
                'length' => $lengthCm,
                'width' => $widthCm,
                'height' => $heightCm,
                'unit' => 'mm',
            ],
            'weight' => [
                'amount' => $weightKg,
                'unit' => 'kg',
            ],
        ];
    }

    /**
     * @param array<string, mixed> $orderDetails
     */
    private function buildReference(array $orderDetails, int $orderId): string
    {
        $orderData = is_array($orderDetails['order'] ?? null) ? $orderDetails['order'] : [];
        $sourceOrderId = trim((string) ($orderData['source_order_id'] ?? ''));

        return $sourceOrderId !== '' ? $sourceOrderId : (string) $orderId;
    }

    /**
     * @param array<string, mixed> $sender
     */
    private function validateSenderAddress(array $sender): void
    {
        $required = ['street', 'city', 'postalCode', 'phone', 'email'];
        foreach ($required as $field) {
            if (trim((string) ($sender[$field] ?? '')) === '') {
                throw new IntegrationConfigException('Uzupelnij dane nadawcy w Ustawienia > Dane firmy (brak: ' . $field . ').');
            }
        }

        $name = trim((string) ($sender['name'] ?? ''));
        $company = trim((string) ($sender['company'] ?? ''));
        if ($name === '' && $company === '') {
            throw new IntegrationConfigException('Uzupelnij dane nadawcy w Ustawienia > Dane firmy (brak nazwy/firmy).');
        }
    }
}
