<?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;

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

/**
 * The single answer to "has an AWAITING_CONFIRMATION attempt gone stale enough that a merchant
 * needs to look at it?" - never "has it failed". That question does not belong here at all: only
 * {@see PaymentStatusReconciler}, asking SIN.CORE directly, is allowed to answer it.
 *
 * ## Why elapsed time is read against the payment type, not on its own
 *
 * "How long is silence normal?" has no single answer across payment types. A method that answers
 * on our own page while the shopper waits - BLIK acceptance today - should be confirmed within
 * seconds, so staying unknown for long is itself an anomaly. A card attempt may instead have sent
 * the shopper to their issuer's own authentication page: {@see PaymentStatus::AWAITING_CONFIRMATION}
 * documents that exact case (a usable redirect target with no readable status), and silence there
 * is normal for as long as the shopper is away completing it. Treating both the same way would
 * either flag a card attempt while the shopper is still legitimately mid-3DS, or let a genuinely
 * stuck BLIK attempt sit unnoticed for hours.
 *
 * ## Why "card"/"add-card" are not read as evidence of a redirect
 *
 * Whether a redirect was actually issued for a given attempt is not persisted - only the attempt's
 * own type is. So this class cannot tell "a card attempt that went to 3DS"
 * from "a card attempt that never redirected at all"; it can only tell "a card attempt, which MAY
 * have redirected" from "BLIK, which never does". The safe read of that gap is to grant every type
 * capable of a redirect the long grace period, whether or not this particular attempt used it - the
 * cost of that is a slower flag on an attempt that never redirected, never a premature one on an
 * attempt whose shopper is still away.
 *
 * The same reasoning is why an unrecognised or blank type also gets the long grace period: nothing
 * here can rule out a redirect for a type it does not know either, so guessing "immediate" for it
 * would be the one guess that could fire while a shopper is still legitimately away.
 *
 * ## Why this class never resolves anything
 *
 * "Overdue" means exactly one thing: a human should look. It is never read as evidence that the
 * payment failed, and nothing downstream treats it that way - {@see PaymentReconciliationSweep}
 * only logs when this returns true; it never changes an attempt's or an order's state on the
 * strength of it. The one and only way an attempt's status changes is an authoritative answer from
 * {@see PaymentStatusReconciler::reconcile()}, which this class has no part in.
 */
final class PaymentReconciliationPolicy
{
    /**
     * Payment types known to answer on our own page while the shopper waits, with no possibility
     * of a redirect to a third party in between. Only BLIK exists as such a type in this plugin
     * today (see {@see \SimplyIn\Checkout\PaymentMethodDataBuilder}); the owner's framing also
     * names a frictionless card charge, Apple Pay and Google Pay as belonging here, but none
     * of those exist as a distinct `type` value in this plugin's checkout yet. Add them here if and
     * when they do, rather than guessing at a type string nothing writes today.
     *
     * @var list<string>
     */
    private const IMMEDIATE_TYPES = ['blik'];

    /**
     * Grace period, in seconds, for one of {@see self::IMMEDIATE_TYPES}. Thirty minutes is two
     * cron ticks ({@see PaymentReconciliationSweep}, which runs every 15 minutes) past the point
     * such a method should have answered - long enough that one slow tick never trips it, short
     * enough that a genuinely stuck attempt is flagged the same day it would be noticed by a
     * merchant checking their orders.
     */
    public const IMMEDIATE_GRACE_SECONDS = 1800;

    /**
     * Grace period for every other type: every attempt that may have sent the shopper to a third
     * party's own page, and every type this class does not recognise. Six hours is longer than any
     * realistic issuer-authentication or pay-by-link session, so it does not fire while the shopper
     * could plausibly still be there - but it is still same-day, so an attempt that is genuinely
     * abandoned does not sit unnoticed indefinitely.
     */
    public const REDIRECT_GRACE_SECONDS = 21600;

    /**
     * @param string $paymentType the stored attempt's OWN type - never inferred, never another
     *                             attempt's (see {@see PaymentRetryPolicy} for why that distinction
     *                             matters on this same data)
     * @param int    $ageSeconds  time elapsed since the attempt was opened. A negative value (clock
     *                            skew, bad input) is never overdue - inventing staleness from a
     *                            broken clock would be exactly the kind of guess this class exists
     *                            to avoid
     */
    public static function isOverdue(string $paymentType, int $ageSeconds): bool
    {
        return $ageSeconds >= 0 && $ageSeconds >= self::graceSeconds($paymentType);
    }

    public static function graceSeconds(string $paymentType): int
    {
        return in_array($paymentType, self::IMMEDIATE_TYPES, true)
            ? self::IMMEDIATE_GRACE_SECONDS
            : self::REDIRECT_GRACE_SECONDS;
    }
}
