<?php
declare(strict_types=1);

namespace App\Modules\Automation;

use App\Modules\Accounting\ReceiptIssueException;
use App\Modules\Accounting\ReceiptRepository;
use App\Modules\Accounting\ReceiptService;
use App\Modules\Email\EmailSendingService;
use App\Modules\Orders\OrdersRepository;
use App\Modules\Settings\CompanySettingsRepository;
use App\Modules\Settings\ReceiptConfigRepository;
use App\Modules\Shipments\DeliveryStatus;
use App\Modules\Shipments\ShipmentPackageRepository;
use Throwable;

final class AutomationService
{
    private const CHAIN_STATE_KEY = '__automation_chain';
    private const MAX_CHAIN_DEPTH = 8;
    private const MAX_CHAIN_EXECUTIONS = 200;

    private const SHIPMENT_STATUS_OPTION_MAP = [
        'registered' => ['created', 'confirmed'],
        'ready_for_pickup' => ['ready_for_pickup'],
        'dropped_at_point' => ['confirmed', 'in_transit'],
        'picked_up' => ['delivered'],
        'cancelled' => ['cancelled'],
        'unclaimed' => ['problem'],
        'picked_up_return' => ['returned'],
    ];

    public function __construct(
        private readonly AutomationRepository $repository,
        private readonly AutomationExecutionLogRepository $executionLogs,
        private readonly EmailSendingService $emailService,
        private readonly OrdersRepository $orders,
        private readonly CompanySettingsRepository $companySettings,
        private readonly ReceiptRepository $receipts,
        private readonly ReceiptConfigRepository $receiptConfigs,
        private readonly ShipmentPackageRepository $shipmentPackages,
        private readonly ReceiptService $receiptService
    ) {
    }

    /**
     * @param array<string, mixed> $context
     */
    public function trigger(string $eventType, int $orderId, array $context = []): void
    {
        $chainState = $this->getChainState($context);
        if ($chainState['depth'] > self::MAX_CHAIN_DEPTH) {
            return;
        }
        $context = $this->withChainState($context, $chainState);

        $rules = $this->repository->findActiveByEvent($eventType);
        if ($rules === []) {
            return;
        }

        $details = $this->orders->findDetails($orderId);
        if ($details === null) {
            return;
        }

        $order = is_array($details['order'] ?? null) ? $details['order'] : [];

        foreach ($rules as $rule) {
            try {
                $ruleId = (int) ($rule['id'] ?? 0);
                $executionKey = $this->buildExecutionKey($eventType, $ruleId);
                if ($this->isExecutionAlreadyProcessed($chainState, $executionKey)) {
                    continue;
                }

                $conditions = is_array($rule['conditions'] ?? null) ? $rule['conditions'] : [];
                $actions = is_array($rule['actions'] ?? null) ? $rule['actions'] : [];
                $ruleName = (string) ($rule['name'] ?? '');
                $ruleContext = $this->withExecution($context, $executionKey);
                $ruleMatched = $this->evaluateConditions($conditions, $order, $ruleContext);

                if ($ruleMatched) {
                    $this->executeActions($actions, $orderId, $ruleName, $ruleContext);
                    $this->logExecution($eventType, $ruleId, $ruleName, $orderId, 'success', 'Wykonano akcje automatyzacji', $ruleContext);
                }
            } catch (Throwable $exception) {
                $this->logExecution(
                    $eventType,
                    (int) ($rule['id'] ?? 0),
                    (string) ($rule['name'] ?? ''),
                    $orderId,
                    'failed',
                    $exception->getMessage(),
                    $context
                );
                // Blad jednej reguly nie blokuje kolejnych
            }
        }
    }

    /**
     * @param list<array<string, mixed>> $conditions
     * @param array<string, mixed> $order
     * @param array<string, mixed> $context
     */
    private function evaluateConditions(array $conditions, array $order, array $context): bool
    {
        foreach ($conditions as $condition) {
            $type = (string) ($condition['condition_type'] ?? '');
            $value = is_array($condition['condition_value'] ?? null) ? $condition['condition_value'] : [];

            if (!$this->evaluateSingleCondition($type, $value, $order, $context)) {
                return false;
            }
        }

        return true;
    }

    /**
     * @param array<string, mixed> $value
     * @param array<string, mixed> $order
     * @param array<string, mixed> $context
     */
    private function evaluateSingleCondition(string $type, array $value, array $order, array $context): bool
    {
        if ($type === 'integration') {
            return $this->evaluateIntegrationCondition($value, $order);
        }
        if ($type === 'shipment_status') {
            return $this->evaluateShipmentStatusCondition($value, $context);
        }
        if ($type === 'payment_status') {
            return $this->evaluatePaymentStatusCondition($value, $context);
        }
        if ($type === 'order_status') {
            return $this->evaluateOrderStatusCondition($value, $context);
        }
        if ($type === 'days_in_status') {
            return $this->evaluateDaysInStatusCondition($value, $context);
        }

        return false;
    }

    /**
     * @param array<string, mixed> $value
     * @param array<string, mixed> $order
     */
    private function evaluateIntegrationCondition(array $value, array $order): bool
    {
        $allowedIds = is_array($value['integration_ids'] ?? null) ? $value['integration_ids'] : [];
        if ($allowedIds === []) {
            return false;
        }

        $orderIntegrationId = (int) ($order['integration_id'] ?? 0);
        if ($orderIntegrationId === 0) {
            return false;
        }

        return in_array($orderIntegrationId, array_map('intval', $allowedIds), true);
    }

    /**
     * @param array<string, mixed> $value
     * @param array<string, mixed> $context
     */
    private function evaluateShipmentStatusCondition(array $value, array $context): bool
    {
        $statusKeys = is_array($value['status_keys'] ?? null) ? $value['status_keys'] : [];
        if ($statusKeys === []) {
            return false;
        }

        $deliveryStatus = trim((string) ($context['delivery_status'] ?? ''));
        if ($deliveryStatus === '') {
            return false;
        }

        $allowedStatuses = [];
        foreach ($statusKeys as $statusKeyRaw) {
            $statusKey = trim((string) $statusKeyRaw);
            if ($statusKey === '' || !isset(self::SHIPMENT_STATUS_OPTION_MAP[$statusKey])) {
                continue;
            }
            foreach (self::SHIPMENT_STATUS_OPTION_MAP[$statusKey] as $mappedStatus) {
                $allowedStatuses[$mappedStatus] = true;
            }
        }

        if ($allowedStatuses === []) {
            return false;
        }

        return isset($allowedStatuses[$deliveryStatus]);
    }

    /**
     * @param array<string, mixed> $value
     * @param array<string, mixed> $context
     */
    private function evaluatePaymentStatusCondition(array $value, array $context): bool
    {
        $statusKeys = is_array($value['status_keys'] ?? null) ? $value['status_keys'] : [];
        if ($statusKeys === []) {
            return false;
        }

        $newPaymentStatus = trim((string) ($context['new_payment_status'] ?? ''));
        if ($newPaymentStatus === '') {
            return false;
        }

        return in_array($newPaymentStatus, array_map(static fn (mixed $k): string => trim((string) $k), $statusKeys), true);
    }

    /**
     * @param array<string, mixed> $value
     * @param array<string, mixed> $context
     */
    private function evaluateOrderStatusCondition(array $value, array $context): bool
    {
        $orderStatusCodes = is_array($value['order_status_codes'] ?? null) ? $value['order_status_codes'] : [];
        if ($orderStatusCodes === []) {
            return false;
        }

        $newStatus = strtolower(trim((string) ($context['new_status'] ?? '')));
        if ($newStatus === '') {
            return false;
        }

        $normalizedCodes = array_map(
            static fn (mixed $code): string => strtolower(trim((string) $code)),
            $orderStatusCodes
        );

        return in_array($newStatus, $normalizedCodes, true);
    }

    /**
     * @param array<string, mixed> $value
     * @param array<string, mixed> $context
     */
    private function evaluateDaysInStatusCondition(array $value, array $context): bool
    {
        $requiredDays = (int) ($value['days'] ?? 0);
        if ($requiredDays < 1) {
            return false;
        }

        $actualDays = (int) ($context['days_in_status'] ?? 0);

        return $actualDays >= $requiredDays;
    }

    /**
     * @param list<array<string, mixed>> $actions
     * @param array<string, mixed> $context
     */
    private function executeActions(array $actions, int $orderId, string $ruleName, array $context): void
    {
        foreach ($actions as $action) {
            $type = (string) ($action['action_type'] ?? '');
            $config = is_array($action['action_config'] ?? null) ? $action['action_config'] : [];

            if ($type === 'send_email') {
                $this->handleSendEmail($config, $orderId, $ruleName);
                continue;
            }

            if ($type === 'issue_receipt') {
                $this->handleIssueReceipt($config, $orderId, $ruleName, $context);
                continue;
            }

            if ($type === 'update_shipment_status') {
                $this->handleUpdateShipmentStatus($config, $orderId, $ruleName, $context);
                continue;
            }

            if ($type === 'update_order_status') {
                $this->handleUpdateOrderStatus($config, $orderId, $ruleName, $context);
            }
        }
    }

    /**
     * @param array<string, mixed> $config
     */
    private function handleSendEmail(array $config, int $orderId, string $ruleName): void
    {
        $templateId = (int) ($config['template_id'] ?? 0);
        if ($templateId <= 0) {
            return;
        }

        $recipient = (string) ($config['recipient'] ?? 'client');
        $actorName = 'Automatyzacja: ' . $ruleName;

        if ($recipient === 'client' || $recipient === 'client_and_company') {
            $this->emailService->send($orderId, $templateId, null, $actorName);
        }

        if ($recipient === 'company' || $recipient === 'client_and_company') {
            $this->sendToCompany($orderId, $templateId, $actorName);
        }
    }

    private function sendToCompany(int $orderId, int $templateId, string $actorName): void
    {
        $settings = $this->companySettings->getSettings();
        $companyEmail = trim((string) ($settings['email'] ?? ''));

        if ($companyEmail === '' || filter_var($companyEmail, FILTER_VALIDATE_EMAIL) === false) {
            $this->orders->recordActivity(
                $orderId,
                'automation_email_failed',
                $actorName . ' — brak adresu e-mail firmy w ustawieniach',
                ['reason' => 'missing_company_email'],
                'system',
                $actorName
            );
            return;
        }

        $companyName = trim((string) ($settings['company_name'] ?? ''));

        $this->emailService->send(
            $orderId,
            $templateId,
            null,
            $actorName,
            $companyEmail,
            $companyName
        );
    }

    /**
     * @param array<string, mixed> $config
     * @param array<string, mixed> $context
     */
    private function handleIssueReceipt(array $config, int $orderId, string $ruleName, array $context): void
    {
        $actorName = 'Automatyzacja: ' . $ruleName;

        $configId = (int) ($config['receipt_config_id'] ?? 0);
        if ($configId <= 0) {
            return;
        }
        $duplicatePolicy = (string) ($config['duplicate_policy'] ?? 'skip_if_exists');

        $existingReceipts = $this->receipts->findByOrderId($orderId);
        if ($duplicatePolicy === 'skip_if_exists' && $existingReceipts !== []) {
            $this->orders->recordActivity(
                $orderId,
                'automation_receipt_skipped',
                $actorName . ' - pomieto, paragon juz istnieje dla zamowienia',
                ['duplicate_policy' => $duplicatePolicy, 'existing_count' => count($existingReceipts)],
                'system',
                $actorName
            );
            return;
        }

        try {
            $result = $this->receiptService->issue([
                'order_id' => $orderId,
                'config_id' => $configId,
                'issue_date_mode' => (string) ($config['issue_date_mode'] ?? 'today'),
                'created_by' => null,
            ]);

            $this->orders->recordActivity(
                $orderId,
                'receipt_issued',
                'Wystawiono paragon: ' . $result['receipt_number'],
                ['receipt_number' => $result['receipt_number'], 'config_id' => $configId, 'total_gross' => $result['total_gross']],
                'system',
                $actorName
            );

            $this->emitEvent(
                'receipt.created',
                $orderId,
                $context,
                [
                    'automation_source' => 'issue_receipt',
                    'automation_rule' => $ruleName,
                    'receipt_number' => $result['receipt_number'],
                    'receipt_config_id' => $configId,
                ]
            );
        } catch (ReceiptIssueException $e) {
            $this->orders->recordActivity(
                $orderId,
                'automation_receipt_failed',
                $actorName . ' - ' . $e->getMessage(),
                ['receipt_config_id' => $configId],
                'system',
                $actorName
            );
        } catch (Throwable $exception) {
            $this->orders->recordActivity(
                $orderId,
                'automation_receipt_failed',
                $actorName . ' - blad wystawiania paragonu',
                ['error' => $exception->getMessage(), 'receipt_config_id' => $configId],
                'system',
                $actorName
            );
        }
    }

    /**
     * @param array<string, mixed> $config
     * @param array<string, mixed> $context
     */
    private function handleUpdateShipmentStatus(array $config, int $orderId, string $ruleName, array $context): void
    {
        $statusKey = trim((string) ($config['status_key'] ?? ''));
        $targetStatus = $this->resolveStatusFromActionKey($statusKey);
        if ($targetStatus === null) {
            return;
        }

        $targetPackage = $this->resolveTargetPackage($orderId, $context);
        if ($targetPackage === null) {
            return;
        }

        $packageId = (int) ($targetPackage['id'] ?? 0);
        if ($packageId <= 0) {
            return;
        }

        $previousStatus = trim((string) ($targetPackage['delivery_status'] ?? DeliveryStatus::UNKNOWN));
        if ($previousStatus === '') {
            $previousStatus = DeliveryStatus::UNKNOWN;
        }
        if ($previousStatus === $targetStatus) {
            return;
        }

        $this->shipmentPackages->updateDeliveryStatus($packageId, $targetStatus, null);

        $actorName = 'Automatyzacja: ' . $ruleName;
        $this->orders->recordActivity(
            $orderId,
            'automation_shipment_status_updated',
            $actorName . ' - zaktualizowano status przesylki',
            [
                'package_id' => $packageId,
                'previous_status' => $previousStatus,
                'delivery_status' => $targetStatus,
                'status_key' => $statusKey,
            ],
            'system',
            $actorName
        );

        $this->emitEvent(
            'shipment.status_changed',
            $orderId,
            $context,
            [
                'package_id' => $packageId,
                'provider' => (string) ($targetPackage['provider'] ?? ''),
                'delivery_status' => $targetStatus,
                'delivery_status_raw' => '',
                'previous_status' => $previousStatus,
                'previous_status_raw' => '',
                'automation_source' => 'update_shipment_status',
                'automation_rule' => $ruleName,
            ]
        );
    }

    /**
     * @param array<string, mixed> $config
     * @param array<string, mixed> $context
     */
    private function handleUpdateOrderStatus(array $config, int $orderId, string $ruleName, array $context): void
    {
        $statusCode = trim((string) ($config['status_code'] ?? ''));
        if ($statusCode === '') {
            return;
        }

        $details = $this->orders->findDetails($orderId);
        $order = is_array($details['order'] ?? null) ? $details['order'] : [];
        $oldStatus = strtolower(trim((string) ($order['external_status_id'] ?? '')));

        $actorName = 'Automatyzacja: ' . $ruleName;
        $updated = $this->orders->updateOrderStatus($orderId, $statusCode, 'system', $actorName);
        if (!$updated) {
            $this->orders->recordActivity(
                $orderId,
                'automation_order_status_failed',
                $actorName . ' - nie udalo sie zmienic statusu zamowienia',
                ['target_status_code' => $statusCode],
                'system',
                $actorName
            );
            return;
        }

        $newStatus = strtolower(trim($statusCode));
        if ($oldStatus !== $newStatus) {
            $this->emitEvent(
                'order.status_changed',
                $orderId,
                $context,
                [
                    'old_status' => $oldStatus,
                    'new_status' => $newStatus,
                    'automation_source' => 'update_order_status',
                    'automation_rule' => $ruleName,
                ]
            );
        }
    }

    private function resolveStatusFromActionKey(string $statusKey): ?string
    {
        if ($statusKey === '' || !isset(self::SHIPMENT_STATUS_OPTION_MAP[$statusKey])) {
            return null;
        }

        $mappedStatuses = self::SHIPMENT_STATUS_OPTION_MAP[$statusKey];
        if ($mappedStatuses === []) {
            return null;
        }

        $candidate = trim((string) $mappedStatuses[0]);
        if ($candidate === '' || !in_array($candidate, DeliveryStatus::ALL_STATUSES, true)) {
            return null;
        }

        return $candidate;
    }

    /**
     * @param array<string, mixed> $context
     * @return array<string, mixed>|null
     */
    private function resolveTargetPackage(int $orderId, array $context): ?array
    {
        $packageId = (int) ($context['package_id'] ?? 0);
        if ($packageId > 0) {
            $package = $this->shipmentPackages->findById($packageId);
            if (is_array($package) && (int) ($package['order_id'] ?? 0) === $orderId) {
                return $package;
            }
        }

        return $this->shipmentPackages->findLatestByOrderId($orderId);
    }

    /**
     * @param array<string, mixed> $context
     * @return array{chain_id:string,depth:int,executions:list<string>}
     */
    private function getChainState(array $context): array
    {
        $raw = $context[self::CHAIN_STATE_KEY] ?? null;
        $rawState = is_array($raw) ? $raw : [];

        $chainId = trim((string) ($rawState['chain_id'] ?? ''));
        if ($chainId === '') {
            $chainId = $this->generateChainId();
        }

        $depth = max(0, (int) ($rawState['depth'] ?? 0));

        $executionsRaw = $rawState['executions'] ?? [];
        $executions = [];
        if (is_array($executionsRaw)) {
            foreach ($executionsRaw as $executionRaw) {
                $execution = trim((string) $executionRaw);
                if ($execution === '') {
                    continue;
                }
                $executions[] = $execution;
            }
        }

        return [
            'chain_id' => $chainId,
            'depth' => $depth,
            'executions' => array_values(array_unique($executions)),
        ];
    }

    /**
     * @param array<string, mixed> $context
     * @param array{chain_id:string,depth:int,executions:list<string>} $state
     * @return array<string, mixed>
     */
    private function withChainState(array $context, array $state): array
    {
        $context[self::CHAIN_STATE_KEY] = [
            'chain_id' => $state['chain_id'],
            'depth' => $state['depth'],
            'executions' => $state['executions'],
        ];

        return $context;
    }

    /**
     * @param array<string, mixed> $context
     * @return array<string, mixed>
     */
    private function withExecution(array $context, string $executionKey): array
    {
        $state = $this->getChainState($context);
        $state['executions'][] = $executionKey;
        if (count($state['executions']) > self::MAX_CHAIN_EXECUTIONS) {
            $state['executions'] = array_slice($state['executions'], -self::MAX_CHAIN_EXECUTIONS);
        }

        return $this->withChainState($context, $state);
    }

    private function buildExecutionKey(string $eventType, int $ruleId): string
    {
        if ($ruleId <= 0) {
            return $eventType . '#unknown';
        }

        return $eventType . '#' . $ruleId;
    }

    /**
     * @param array{chain_id:string,depth:int,executions:list<string>} $state
     */
    private function isExecutionAlreadyProcessed(array $state, string $executionKey): bool
    {
        return in_array($executionKey, $state['executions'], true);
    }

    /**
     * @param array<string, mixed> $parentContext
     * @param array<string, mixed> $eventContext
     */
    private function emitEvent(string $eventType, int $orderId, array $parentContext, array $eventContext): void
    {
        $parentState = $this->getChainState($parentContext);
        if ($parentState['depth'] >= self::MAX_CHAIN_DEPTH) {
            return;
        }

        $childState = $parentState;
        $childState['depth'] = $parentState['depth'] + 1;

        $childContext = $this->withChainState($eventContext, $childState);
        $this->trigger($eventType, $orderId, $childContext);
    }

    private function generateChainId(): string
    {
        try {
            return bin2hex(random_bytes(8));
        } catch (Throwable) {
            return uniqid('chain_', true);
        }
    }

    /**
     * @param array<string, mixed> $context
     */
    private function logExecution(
        string $eventType,
        int $ruleId,
        string $ruleName,
        int $orderId,
        string $status,
        string $message,
        array $context
    ): void {
        if ($ruleId <= 0 || $orderId <= 0 || $ruleName === '') {
            return;
        }

        try {
            $this->executionLogs->create([
                'event_type' => $eventType,
                'rule_id' => $ruleId,
                'rule_name' => $ruleName,
                'order_id' => $orderId,
                'execution_status' => $status,
                'result_message' => mb_substr(trim($message), 0, 500),
                'context' => $this->sanitizeContext($context),
                'executed_at' => date('Y-m-d H:i:s'),
            ]);
        } catch (Throwable) {
            // Historia automatyzacji nie moze blokowac glownego flow.
        }
    }

    /**
     * @param array<string, mixed> $context
     * @return array<string, mixed>
     */
    private function sanitizeContext(array $context): array
    {
        $sanitized = [];
        foreach ($context as $key => $value) {
            if (is_scalar($value) || $value === null) {
                $sanitized[(string) $key] = $value;
                continue;
            }

            if (!is_array($value)) {
                continue;
            }

            $sanitized[(string) $key] = $this->sanitizeArray($value, 2);
        }

        return $sanitized;
    }

    /**
     * @param array<mixed> $value
     * @return array<mixed>
     */
    private function sanitizeArray(array $value, int $depth): array
    {
        if ($depth <= 0) {
            return [];
        }

        $sanitized = [];
        foreach ($value as $key => $item) {
            if (is_scalar($item) || $item === null) {
                $sanitized[$key] = $item;
                continue;
            }

            if (is_array($item)) {
                $sanitized[$key] = $this->sanitizeArray($item, $depth - 1);
            }
        }

        return $sanitized;
    }
}
