<?php
declare(strict_types=1);

namespace App\Modules\Shipments;

use App\Modules\Automation\AutomationService;
use App\Core\Http\Request;
use App\Core\Http\Response;
use App\Core\I18n\Translator;
use App\Core\Security\Csrf;
use App\Core\Support\Flash;
use App\Core\View\Template;
use App\Modules\Auth\AuthService;
use App\Modules\Orders\OrdersRepository;
use App\Modules\Settings\CarrierDeliveryMethodMappingRepository;
use App\Modules\Settings\CompanySettingsRepository;
use App\Core\Exceptions\ShipmentException;
use Throwable;

final class ShipmentController
{
    public function __construct(
        private readonly Template $template,
        private readonly Translator $translator,
        private readonly AuthService $auth,
        private readonly OrdersRepository $ordersRepository,
        private readonly CompanySettingsRepository $companySettings,
        private readonly ShipmentProviderRegistry $providerRegistry,
        private readonly ShipmentPackageRepository $packageRepository,
        private readonly AutomationService $automationService,
        private readonly string $storagePath,
        private readonly ?CarrierDeliveryMethodMappingRepository $deliveryMappings = null,
        private readonly ?\App\Modules\Printing\PrintJobRepository $printJobRepo = null
    ) {
    }

    public function prepare(Request $request): Response
    {
        $orderId = max(0, (int) $request->input('id', 0));
        $details = $this->ordersRepository->findDetails($orderId);
        if ($details === null) {
            return Response::html('Not found', 404);
        }

        $order = is_array($details['order'] ?? null) ? $details['order'] : [];
        $addresses = is_array($details['addresses'] ?? null) ? $details['addresses'] : [];
        $items = is_array($details['items'] ?? null) ? $details['items'] : [];
        $company = $this->companySettings->getSettings();
        $existingPackages = $this->packageRepository->findByOrderId($orderId);

        $deliveryAddr = null;
        $customerAddr = null;
        foreach ($addresses as $addr) {
            $type = (string) ($addr['address_type'] ?? '');
            if ($type === 'delivery') {
                $deliveryAddr = $addr;
            }
            if ($type === 'customer') {
                $customerAddr = $addr;
            }
        }

        $receiverAddr = $this->buildReceiverAddress($deliveryAddr, $customerAddr);
        $preferences = is_array($order['preferences_json'] ?? null)
            ? $order['preferences_json']
            : (is_string($order['preferences_json'] ?? null) ? (json_decode($order['preferences_json'], true) ?: []) : []);

        $deliveryServices = [];
        $apaczkaServices = [];
        $deliveryServicesError = '';

        $allegroProvider = $this->providerRegistry->get('allegro_wza');
        if ($allegroProvider !== null) {
            try {
                $deliveryServices = $allegroProvider->getDeliveryServices();
            } catch (Throwable $exception) {
                $deliveryServicesError = $exception->getMessage();
            }
        }

        $apaczkaProvider = $this->providerRegistry->get('apaczka');
        if ($apaczkaProvider !== null) {
            try {
                $apaczkaServices = $apaczkaProvider->getDeliveryServices();
            } catch (Throwable $exception) {
                if ($deliveryServicesError === '') {
                    $deliveryServicesError = $exception->getMessage();
                }
            }
        }

        $inpostServices = array_values(array_filter(
            $deliveryServices,
            static fn(array $svc) => stripos((string) ($svc['carrierId'] ?? ''), 'inpost') !== false
        ));

        $flashSuccess = (string) Flash::get('shipment.success', '');
        $flashError = (string) Flash::get('shipment.error', '');

        $deliveryMapping = null;
        $deliveryMappingDiagnostic = '';
        $orderCarrierName = trim((string) ($order['external_carrier_id'] ?? ''));
        $source = strtolower(trim((string) ($order['source'] ?? '')));
        $sourceIntegrationId = $source === 'shoppro'
            ? max(0, (int) ($order['integration_id'] ?? 0))
            : 0;
        if ($orderCarrierName !== '' && $this->deliveryMappings !== null && in_array($source, ['allegro', 'shoppro'], true)) {
            $deliveryMapping = $this->deliveryMappings->findByOrderMethod($source, $sourceIntegrationId, $orderCarrierName);
            if ($deliveryMapping === null) {
                $hasMappingsForSource = $this->deliveryMappings->hasMappingsForSource($source, $sourceIntegrationId);
                if (!$hasMappingsForSource && $source === 'shoppro' && $sourceIntegrationId > 0) {
                    $deliveryMappingDiagnostic = 'Brak mapowan form dostawy dla tej instancji shopPRO (ID integracji: '
                        . $sourceIntegrationId
                        . ').';
                } elseif (!$hasMappingsForSource) {
                    $deliveryMappingDiagnostic = 'Brak skonfigurowanych mapowan form dostawy dla tego zrodla zamowienia.';
                } else {
                    $deliveryMappingDiagnostic = 'Brak mapowania dla metody dostawy: ' . $orderCarrierName . '.';
                }
            }
        }

        foreach ($existingPackages as &$pkg) {
            $lp = trim((string) ($pkg['label_path'] ?? ''));
            if ($lp !== '' && !file_exists($this->storagePath . '/' . $lp)) {
                $pkg['label_path'] = '';
            }
        }
        unset($pkg);

        $html = $this->template->render('shipments/prepare', [
            'title' => $this->translator->get('shipments.prepare.title') . ' #' . $orderId,
            'activeMenu' => 'orders',
            'activeOrders' => 'list',
            'user' => $this->auth->user(),
            'csrfToken' => Csrf::token(),
            'orderId' => $orderId,
            'order' => $order,
            'items' => $items,
            'receiverAddr' => $receiverAddr,
            'preferences' => $preferences,
            'company' => $company,
            'deliveryServices' => $deliveryServices,
            'apaczkaServices' => $apaczkaServices,
            'deliveryServicesError' => $deliveryServicesError,
            'existingPackages' => $existingPackages,
            'flashSuccess' => $flashSuccess,
            'flashError' => $flashError,
            'deliveryMapping' => $deliveryMapping,
            'deliveryMappingDiagnostic' => $deliveryMappingDiagnostic,
            'inpostServices' => $inpostServices,
            'pendingPrintPackageIds' => $this->printJobRepo !== null ? $this->printJobRepo->pendingPackageIds() : [],
        ], 'layouts/app');

        return Response::html($html);
    }

    public function create(Request $request): Response
    {
        $orderId = max(0, (int) $request->input('id', 0));
        if ($orderId <= 0) {
            return Response::html('Not found', 404);
        }

        $csrfToken = (string) $request->input('_token', '');
        if (!Csrf::validate($csrfToken)) {
            Flash::set('shipment.error', $this->translator->get('auth.errors.csrf_expired'));
            return Response::redirect('/orders/' . $orderId . '/shipment/prepare');
        }

        $user = $this->auth->user();
        $actorName = is_array($user) ? trim((string) ($user['name'] ?? $user['email'] ?? '')) : null;
        $actorName = ($actorName !== null && $actorName !== '') ? $actorName : null;

        try {
            $providerCode = strtolower(trim((string) $request->input('provider_code', 'allegro_wza')));
            $provider = $this->providerRegistry->get($providerCode);
            if ($provider === null) {
                throw new ShipmentException('Nieznany provider przesylek: ' . $providerCode);
            }

            $result = $provider->createShipment($orderId, [
                'provider_code' => $providerCode,
                'delivery_method_id' => (string) $request->input('delivery_method_id', ''),
                'credentials_id' => (string) $request->input('credentials_id', ''),
                'carrier_id' => (string) $request->input('carrier_id', ''),
                'package_type' => (string) $request->input('package_type', 'PACKAGE'),
                'length_cm' => (string) $request->input('length_cm', ''),
                'width_cm' => (string) $request->input('width_cm', ''),
                'height_cm' => (string) $request->input('height_cm', ''),
                'weight_kg' => (string) $request->input('weight_kg', ''),
                'insurance_amount' => (string) $request->input('insurance_amount', '0'),
                'insurance_currency' => (string) $request->input('insurance_currency', 'PLN'),
                'cod_amount' => (string) $request->input('cod_amount', '0'),
                'cod_currency' => (string) $request->input('cod_currency', 'PLN'),
                'label_format' => (string) $request->input('label_format', 'PDF'),
                'receiver_name' => (string) $request->input('receiver_name', ''),
                'receiver_company' => (string) $request->input('receiver_company', ''),
                'receiver_street' => (string) $request->input('receiver_street', ''),
                'receiver_city' => (string) $request->input('receiver_city', ''),
                'receiver_postal_code' => (string) $request->input('receiver_postal_code', ''),
                'receiver_country_code' => (string) $request->input('receiver_country_code', 'PL'),
                'receiver_phone' => (string) $request->input('receiver_phone', ''),
                'receiver_email' => (string) $request->input('receiver_email', ''),
                'receiver_point_id' => (string) $request->input('receiver_point_id', ''),
                'sender_point_id' => (string) $request->input('sender_point_id', ''),
            ]);

            $packageId = (int) ($result['package_id'] ?? 0);
            $this->ordersRepository->recordActivity(
                $orderId,
                'shipment_created',
                'Zlecono utworzenie przesylki (' . $providerCode . ', ID paczki: ' . $packageId . ')',
                ['package_id' => $packageId, 'command_id' => $result['command_id'] ?? null, 'provider' => $providerCode],
                'user',
                $actorName
            );

            $this->triggerShipmentCreatedAutomation($orderId, $packageId, $providerCode);
            Flash::set('order.success', 'Przesylka utworzona. Sprawdz status w zakladce Przesylki.');
            return Response::redirect('/orders/' . $orderId);
        } catch (Throwable $exception) {
            $this->ordersRepository->recordActivity(
                $orderId,
                'shipment_error',
                'Blad tworzenia przesylki: ' . $exception->getMessage(),
                null,
                'user',
                $actorName
            );
            Flash::set('shipment.error', 'Blad tworzenia przesylki: ' . $exception->getMessage());
            return Response::redirect('/orders/' . $orderId . '/shipment/prepare');
        }
    }

    public function checkStatus(Request $request): Response
    {
        $orderId = max(0, (int) $request->input('id', 0));
        $packageId = max(0, (int) $request->input('packageId', 0));
        if ($orderId <= 0 || $packageId <= 0) {
            return Response::json(['error' => 'Not found'], 404);
        }

        try {
            $package = $this->packageRepository->findById($packageId);
            if ($package === null) {
                return Response::json(['error' => 'Not found'], 404);
            }

            $providerCode = strtolower(trim((string) ($package['provider'] ?? 'allegro_wza')));
            $provider = $this->providerRegistry->get($providerCode);
            if ($provider === null) {
                return Response::json(['status' => 'error', 'error' => 'Brak providera: ' . $providerCode]);
            }

            $result = $provider->checkCreationStatus($packageId);

            if (($result['status'] ?? '') === 'created') {
                try {
                    $provider->downloadLabel($packageId, $this->storagePath);
                    $result['status'] = 'label_ready';
                } catch (Throwable) {
                    // label generation failed, user can retry manually
                }
            }

            return Response::json($result);
        } catch (Throwable $exception) {
            return Response::json(['status' => 'error', 'error' => $exception->getMessage()]);
        }
    }

    public function label(Request $request): Response
    {
        $orderId = max(0, (int) $request->input('id', 0));
        $packageId = max(0, (int) $request->input('packageId', 0));
        if ($orderId <= 0 || $packageId <= 0) {
            return Response::html('Not found', 404);
        }

        $csrfToken = (string) $request->input('_token', '');
        if (!Csrf::validate($csrfToken)) {
            Flash::set('shipment.error', $this->translator->get('auth.errors.csrf_expired'));
            return Response::redirect('/orders/' . $orderId . '/shipment/prepare');
        }

        $user = $this->auth->user();
        $actorName = is_array($user) ? trim((string) ($user['name'] ?? $user['email'] ?? '')) : null;
        $actorName = ($actorName !== null && $actorName !== '') ? $actorName : null;

        try {
            $package = $this->packageRepository->findById($packageId);
            if ($package === null) {
                throw new ShipmentException('Paczka nie znaleziona.');
            }

            $providerCode = strtolower(trim((string) ($package['provider'] ?? 'allegro_wza')));
            $provider = $this->providerRegistry->get($providerCode);
            if ($provider === null) {
                throw new ShipmentException('Brak providera: ' . $providerCode);
            }

            $result = $provider->downloadLabel($packageId, $this->storagePath);
            $fullPath = (string) ($result['full_path'] ?? '');
            if ($fullPath !== '' && file_exists($fullPath)) {
                $labelFormat = strtoupper(trim((string) ($package['label_format'] ?? 'PDF')));
                $contentType = $labelFormat === 'ZPL' ? 'application/octet-stream' : 'application/pdf';
                $filename = basename($fullPath);

                $this->ordersRepository->recordActivity(
                    $orderId,
                    'shipment_label_downloaded',
                    'Pobrano etykiete dla przesylki #' . $packageId,
                    ['package_id' => $packageId, 'filename' => $filename, 'provider' => $providerCode],
                    'user',
                    $actorName
                );

                return new Response(
                    (string) file_get_contents($fullPath),
                    200,
                    [
                        'Content-Type' => $contentType,
                        'Content-Disposition' => 'attachment; filename="' . $filename . '"',
                    ]
                );
            }

            Flash::set('shipment.success', 'Etykieta pobrana.');
        } catch (Throwable $exception) {
            $this->ordersRepository->recordActivity(
                $orderId,
                'shipment_error',
                'Blad pobierania etykiety (paczka #' . $packageId . '): ' . $exception->getMessage(),
                null,
                'user',
                $actorName
            );
            Flash::set('shipment.error', 'Blad pobierania etykiety: ' . $exception->getMessage());
        }

        return Response::redirect('/orders/' . $orderId . '/shipment/prepare');
    }

    public function createManual(Request $request): Response
    {
        $orderId = max(0, (int) $request->input('id', 0));
        if ($orderId <= 0) {
            return Response::html('Not found', 404);
        }

        $csrfToken = (string) $request->input('_token', '');
        if (!Csrf::validate($csrfToken)) {
            Flash::set('shipment.error', $this->translator->get('auth.errors.csrf_expired'));
            return Response::redirect('/orders/' . $orderId);
        }

        $trackingNumber = trim((string) $request->input('tracking_number', ''));
        if ($trackingNumber === '') {
            Flash::set('order.error', 'Numer przesylki jest wymagany.');
            return Response::redirect('/orders/' . $orderId);
        }

        $carrierName = trim((string) $request->input('carrier_name', ''));
        $user = $this->auth->user();
        $actorName = is_array($user) ? trim((string) ($user['name'] ?? $user['email'] ?? '')) : null;
        $actorName = ($actorName !== null && $actorName !== '') ? $actorName : null;

        $packageId = $this->packageRepository->createManual(
            $orderId,
            $trackingNumber,
            $carrierName !== '' ? $carrierName : null
        );

        $description = 'Dodano reczny numer przesylki: ' . $trackingNumber;
        if ($carrierName !== '') {
            $description .= ' (' . $carrierName . ')';
        }

        $this->ordersRepository->recordActivity(
            $orderId,
            'shipment_manual',
            $description,
            ['package_id' => $packageId, 'tracking_number' => $trackingNumber, 'carrier_name' => $carrierName],
            'user',
            $actorName
        );

        $this->triggerShipmentCreatedAutomation($orderId, $packageId, 'manual');
        Flash::set('order.success', 'Numer przesylki dodany.');
        return Response::redirect('/orders/' . $orderId);
    }

    private function triggerShipmentCreatedAutomation(int $orderId, int $packageId, string $providerCode): void
    {
        if ($orderId <= 0 || $packageId <= 0) {
            return;
        }

        $package = $this->packageRepository->findById($packageId);
        $context = [
            'package_id' => $packageId,
            'provider' => $providerCode,
            'package_status' => is_array($package) ? (string) ($package['status'] ?? '') : '',
            'tracking_number' => is_array($package) ? (string) ($package['tracking_number'] ?? '') : '',
            'delivery_status' => is_array($package)
                ? (string) ($package['delivery_status'] ?? DeliveryStatus::UNKNOWN)
                : DeliveryStatus::UNKNOWN,
        ];

        try {
            $this->automationService->trigger('shipment.created', $orderId, $context);
        } catch (Throwable) {
            // Blad automatyzacji nie powinien blokowac tworzenia przesylki
        }
    }

    /**
     * @param array<string, mixed>|null $deliveryAddr
     * @param array<string, mixed>|null $customerAddr
     * @return array<string, mixed>
     */
    private function buildReceiverAddress(?array $deliveryAddr, ?array $customerAddr): array
    {
        $delivery = is_array($deliveryAddr) ? $deliveryAddr : [];
        $customer = is_array($customerAddr) ? $customerAddr : [];
        if ($delivery === []) {
            return $customer;
        }

        $result = $delivery;
        $deliveryName = trim((string) ($delivery['name'] ?? ''));
        $customerName = trim((string) ($customer['name'] ?? ''));
        if (($this->isPickupPointDelivery($delivery) || $deliveryName === '') && $customerName !== '') {
            $result['name'] = $customerName;
        }
        if (trim((string) ($result['phone'] ?? '')) === '' && trim((string) ($customer['phone'] ?? '')) !== '') {
            $result['phone'] = $customer['phone'];
        }
        if (trim((string) ($result['email'] ?? '')) === '' && trim((string) ($customer['email'] ?? '')) !== '') {
            $result['email'] = $customer['email'];
        }

        return $result;
    }

    /**
     * @param array<string, mixed> $deliveryAddr
     */
    private function isPickupPointDelivery(array $deliveryAddr): bool
    {
        $parcelId = trim((string) ($deliveryAddr['parcel_external_id'] ?? ''));
        $parcelName = trim((string) ($deliveryAddr['parcel_name'] ?? ''));
        return $parcelId !== '' || $parcelName !== '';
    }
}
