<?php
/**
 * Decides what may be withdrawn, and why not when it may not.
 *
 * @package SkyServers\Retragere
 */

declare(strict_types=1);

namespace SkyServers\Retragere\Core;

use DateTimeImmutable;

/**
 * Every eligibility decision in the plugin goes through this class. It never
 * blocks a free declaration: the consumer's right cannot depend on our rules
 * being able to match an order.
 */
final class EligibilityRules
{
    public const REASON_WINDOW            = 'window';
    public const REASON_STATUS            = 'status';
    public const REASON_DIGITAL           = 'digital';
    public const REASON_NON_RETURNABLE    = 'non_returnable';
    public const REASON_ALREADY_REQUESTED = 'already_requested';
    public const REASON_B2B               = 'b2b';

    /** @var DeadlineCalculator */
    private $deadlines;

    /** @var string[] */
    private $eligibleStatuses;

    /** @var bool */
    private $allowB2b;

    /**
     * @param string[] $eligibleStatuses
     */
    public function __construct(DeadlineCalculator $deadlines, array $eligibleStatuses, bool $allowB2b = true)
    {
        $this->deadlines        = $deadlines;
        $this->eligibleStatuses = $eligibleStatuses;
        $this->allowB2b         = $allowB2b;
    }

    /**
     * @param array<string,mixed>            $order
     * @param array<int,array<string,mixed>> $lines
     *
     * @return array{eligible:bool,reason:?string,free_declaration_allowed:bool,lines:array}
     */
    public function evaluate(array $order, array $lines, DateTimeImmutable $now): array
    {
        $evaluated = [];
        foreach ($lines as $line) {
            $evaluated[] = $this->evaluateLine($line);
        }

        $reason   = $this->orderReason($order, $now);
        $eligible = null === $reason && $this->hasEligibleLine($evaluated);

        return [
            'eligible'                 => $eligible,
            'reason'                   => $eligible ? null : $reason,
            'free_declaration_allowed' => true,
            'lines'                    => $evaluated,
        ];
    }

    /**
     * @param array<string,mixed> $order
     */
    private function orderReason(array $order, DateTimeImmutable $now): ?string
    {
        $status = (string) ($order['status'] ?? '');
        if (!in_array($status, $this->eligibleStatuses, true)) {
            return self::REASON_STATUS;
        }

        if (!$this->allowB2b && (bool) ($order['is_b2b'] ?? false)) {
            return self::REASON_B2B;
        }

        $reference = $order['reference_date'] ?? null;
        if (!$reference instanceof DateTimeImmutable) {
            return self::REASON_WINDOW;
        }

        if (!$this->deadlines->isWithinWindow($reference, $now)) {
            return self::REASON_WINDOW;
        }

        return null;
    }

    /**
     * @param array<string,mixed> $line
     *
     * @return array<string,mixed>
     */
    private function evaluateLine(array $line): array
    {
        $line['available'] = 0;
        $line['eligible']  = false;
        $line['reason']    = null;

        if ((bool) ($line['is_digital'] ?? false)) {
            $line['reason'] = self::REASON_DIGITAL;

            return $line;
        }

        if ((bool) ($line['is_non_returnable'] ?? false)) {
            $line['reason'] = self::REASON_NON_RETURNABLE;

            return $line;
        }

        $available = (int) ($line['qty'] ?? 0) - (int) ($line['requested_qty'] ?? 0);
        if ($available < 1) {
            $line['reason'] = self::REASON_ALREADY_REQUESTED;

            return $line;
        }

        $line['available'] = $available;
        $line['eligible']  = true;

        return $line;
    }

    /**
     * @param array<int,array<string,mixed>> $lines
     */
    private function hasEligibleLine(array $lines): bool
    {
        foreach ($lines as $line) {
            if (true === $line['eligible']) {
                return true;
            }
        }

        return false;
    }
}
