<?php

/**
 * Copyright 2024-2025 Simply.IN Sp. z o.o.
 *
 * NOTICE OF LICENSE
 *
 * Licensed under the EUPL-1.2 or later.
 * You may not use this work except in compliance with the Licence.
 *
 * Copy of the Licence is available at:
 * https://joinup.ec.europa.eu/software/page/eupl
 * It is bundled with this package in the file LICENSE.txt
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the Licence is distributed on an as is basis,
 * without warranties or conditions of any kind, either express or implied.
 * Check the Licence for the specific language governing permissions
 * and limitations under the License.
 *
 * @author   Simply.IN Sp. z o.o.
 * @copyright 2024-2025 Simply.IN Sp. z o.o.
 * @license   https://joinup.ec.europa.eu/software/page/eupl
 */

declare(strict_types=1);

namespace SimplyIn\Payment;

use Configuration;
use Order;
use SimplyIn\Api\SimplyInApiClient;
use SimplyInPayments;
use Validate;

if (!defined('_PS_VERSION_')) {
    exit;
}

/**
 * The one place that asks SIN.CORE for the authoritative status of a payment attempt and applies
 * its answer. Used both from the confirmation-page poller ({@see PaymentStatusChecker}) and from
 * the cron reconciliation pass ({@see PaymentReconciliationSweep}) - the two share this instead of
 * each deciding locally, because deciding locally is exactly what this whole surface must not do
 * (see {@see PaymentStatus::AWAITING_CONFIRMATION}).
 *
 * Never resolves anything on its own: only PAID and REJECTED, reported by SIN.CORE itself, are
 * acted on. Any other answer - a pending status, an unfamiliar one, a failed lookup - leaves the
 * attempt exactly as it was, for the same reason {@see PaymentRetryPolicy} refuses a retry on it:
 * an unknown outcome is never safe to read as a decline.
 */
final class PaymentStatusReconciler implements PaymentStatusReconcilerInterface
{
    public function __construct(
        private readonly SimplyInApiClient $apiClient,
        private readonly OrderStateUpdaterInterface $orderStateUpdater,
    ) {
    }

    public function reconcile(string $uuid, int $idCart): ?array
    {
        $response = $this->apiClient->apiCall(
            false,
            'GET',
            'payments/getPaymentStatus?paymentId=' . urlencode($uuid),
        );

        $apiStatus = is_array($response) ? ($response['status'] ?? null) : null;
        if (!$apiStatus || !in_array($apiStatus, ['PAID', 'REJECTED'], true)) {
            return null;
        }

        $newState = $apiStatus === 'PAID'
            ? (int) Configuration::get('PS_OS_PAYMENT')
            : (int) Configuration::get('SIMPLYIN_PAYMENT_STATUS_CANCELED');
        $modelCallback = $apiStatus === 'PAID' ? 'markAsPaid' : 'markAsRejected';

        // Attempt-scoped write FIRST, exactly as the webhook leg does (see WebhookProcessor):
        // this answer is about the attempt identified by $uuid, not necessarily the newest attempt
        // recorded for the cart - an order can carry more than one, and a later attempt may be
        // live while this answer concerns an earlier, already-abandoned one. Writing by uuid keeps
        // this answer off that later row; only when no stored attempt matches this uuid (should
        // not happen for an id this class itself just read from a stored row, but the check costs
        // nothing) does the cart-scoped writer below run, as the fallback of last resort - it has
        // to be unconditional, unlike WebhookProcessor's, because unlike a webhook this class can
        // be reconciling a cart whose checkout never produced an order at all.
        $attemptUpdated = SimplyInPayments::applyStatusByUuid($uuid, $apiStatus);
        if (!$attemptUpdated && method_exists('SimplyInPayments', $modelCallback)) {
            SimplyInPayments::$modelCallback($idCart);
        }

        // The attempt's own status is fully settled by one of the two branches above, whether or
        // not an order exists for this cart - so the updater below is asked ONLY for the order's
        // state/history transition. Passing the model callback into it as well (as WebhookProcessor
        // does, safely, because it never runs the fallback above in the first place) would make
        // it re-invoke SimplyInPayments::markAsPaid()/markAsRejected() a second time for a value
        // already written.
        $order = Order::getByCartId($idCart);
        if ($order instanceof Order && Validate::isLoadedObject($order)) {
            $this->orderStateUpdater->update($order, $newState, '');
        }

        return [
            'status' => $apiStatus,
            'date' => date('Y-m-d H:i:s'),
            // A live status lookup never carries a reason of its own - that only ever rides the
            // payment-creation response or the webhook. '' keeps this response's shape identical to
            // PaymentStatusChecker's terminal branch, so the poller never has to treat one source as
            // optional and the other as not.
            'reject_reason' => '',
        ];
    }
}
