<?php

/**
 * Gateway Helper Functions
 *
 * Helper functions for automatic payment gateway integration.
 * These functions provide the core conditions and lookups that pro's
 * gateway system hooks into. When pro is not active, all functions
 * return safe defaults (manual payment behavior).
 *
 * @package ArraySubs\Functions
 * @since 1.0.0
 */

defined('ABSPATH') || exit;

/**
 * Check if a subscription uses an automatic payment gateway.
 *
 * Returns true when: (a) the subscription has a _payment_gateway meta key set,
 * AND (b) the pro plugin responds to the arraysubs_is_automatic_payment filter
 * confirming the gateway is registered and enabled.
 *
 * When pro is deactivated, the filter returns false (default), so this helper
 * returns false — the subscription falls back to manual payment.
 *
 * @param int $subscription_id The subscription post ID.
 * @return bool True if the subscription uses an automatic payment gateway.
 */
function arraysubs_is_automatic_payment_subscription(int $subscription_id): bool
{
    if ($subscription_id <= 0) {
        return false;
    }

    $gateway_slug = get_post_meta($subscription_id, '_payment_gateway', true);

    if (empty($gateway_slug)) {
        return false;
    }

    /**
     * Filter whether a subscription uses automatic payments.
     *
     * Pro responds to this filter by checking if the gateway slug
     * is registered and enabled in WooCommerce.
     *
     * @param bool $is_automatic Whether automatic payment is active for this subscription.
     * @param int  $subscription_id The subscription post ID.
     */
    return (bool) apply_filters('arraysubs_is_automatic_payment', false, $subscription_id);
}

/**
 * Get the payment gateway slug for a subscription.
 *
 * Returns the gateway slug stored in _payment_gateway meta, filtered
 * through arraysubs_gateway_resolution to allow pro to override resolution.
 *
 * @param int $subscription_id The subscription post ID.
 * @return string Gateway slug (e.g. 'stripe') or empty string if manual.
 */
function arraysubs_get_subscription_gateway(int $subscription_id): string
{
    if ($subscription_id <= 0) {
        return '';
    }

    $gateway_slug = get_post_meta($subscription_id, '_payment_gateway', true);

    if (empty($gateway_slug)) {
        return '';
    }

    /**
     * Filter the resolved gateway slug for a subscription.
     *
     * @param string $gateway_slug    The gateway slug from meta.
     * @param int    $subscription_id The subscription post ID.
     */
    return (string) apply_filters('arraysubs_gateway_resolution', $gateway_slug, $subscription_id);
}

/**
 * Check if any automatic payment gateways are currently registered and enabled.
 *
 * Queries WooCommerce's available payment gateways list for instances of
 * the pro AbstractArraySubsGateway class. Returns false if pro is not
 * active or no gateways are enabled.
 *
 * @return bool True if at least one automatic payment gateway is available.
 */
function arraysubs_has_automatic_gateways(): bool
{
    /**
     * Filter whether any automatic payment gateways are available.
     *
     * Pro responds to this filter by checking WooCommerce's gateway list
     * for instances of AbstractArraySubsGateway.
     *
     * @param bool $has_gateways Whether any automatic gateways are available.
     */
    return (bool) apply_filters('arraysubs_has_automatic_gateways', false);
}

/**
 * Get the gateway status for a subscription.
 *
 * Returns the _gateway_status meta value which tracks the health of the
 * gateway integration for a specific subscription.
 *
 * @param int $subscription_id The subscription post ID.
 * @return string Status string: 'active', 'inactive', 'errored', 'detached', or empty.
 */
function arraysubs_get_gateway_status(int $subscription_id): string
{
    if ($subscription_id <= 0) {
        return '';
    }

    return (string) get_post_meta($subscription_id, '_gateway_status', true);
}

/**
 * Check if a subscription supports retention offers.
 *
 * Manual renewals are supported by default. Automatic-payment subscriptions
 * are opt-in via a filter so addon gateways can explicitly declare support.
 *
 * @param int $subscription_id The subscription post ID.
 * @return bool True if retention offers should be shown for this subscription.
 */
function arraysubs_subscription_supports_retention_offers(int $subscription_id): bool
{
    if ($subscription_id <= 0) {
        return false;
    }

    if (!arraysubs_is_automatic_payment_subscription($subscription_id)) {
        return true;
    }

    $gateway_slug = arraysubs_get_subscription_gateway($subscription_id);

    /**
     * Filter whether retention offers are supported for a subscription.
     *
     * Addon gateways should explicitly opt in here. Returning false hides
     * retention offers and sends the customer straight to the final cancel
     * confirmation step.
     *
     * @param bool   $supports        Whether retention offers are supported.
     * @param int    $subscription_id The subscription post ID.
     * @param string $gateway_slug    The resolved gateway slug.
     */
    return (bool) apply_filters(
        'arraysubs_subscription_supports_retention_offers',
        false,
        $subscription_id,
        $gateway_slug
    );
}

/**
 * Get retry configuration for a subscription's renewal failures.
 *
 * Returns a normalized array describing how the renewal pipeline should retry
 * after a failed automatic charge. Pro gateways publish their own settings
 * via the `arraysubs_payment_retry_config` filter (Stripe gateway exposes
 * max_attempts and interval as admin-configurable). When no gateway responds
 * the defaults below apply.
 *
 * @param int $subscription_id The subscription post ID.
 * @return array{enabled: bool, max_attempts: int, interval_seconds: int}
 */
function arraysubs_get_payment_retry_config(int $subscription_id): array
{
    $defaults = [
        'enabled'          => true,
        'max_attempts'     => 3,
        'interval_seconds' => DAY_IN_SECONDS,
    ];

    /**
     * Filter the retry configuration for a subscription.
     *
     * Pro gateways hook this to publish their per-gateway retry settings.
     * Returning array overrides the defaults.
     *
     * @param array $config         Default retry configuration.
     * @param int   $subscription_id The subscription post ID.
     */
    $config = apply_filters('arraysubs_payment_retry_config', $defaults, $subscription_id);

    if (! is_array($config)) {
        return $defaults;
    }

    return [
        'enabled'          => ! empty($config['enabled']),
        'max_attempts'     => max(0, (int) ($config['max_attempts'] ?? $defaults['max_attempts'])),
        'interval_seconds' => max(HOUR_IN_SECONDS, (int) ($config['interval_seconds'] ?? $defaults['interval_seconds'])),
    ];
}

/**
 * Get the current automatic-payment retry attempt counter for a subscription.
 *
 * Counter is incremented at the moment a retry is scheduled, so a value of N
 * means N retries have been queued or run; the first attempt is 0.
 *
 * @param int $subscription_id The subscription post ID.
 * @return int
 */
function arraysubs_get_payment_retry_attempts(int $subscription_id): int
{
    return (int) get_post_meta($subscription_id, '_payment_retry_attempts', true);
}

/**
 * Reset the automatic-payment retry attempt counter and clear the
 * recorded failure state so the admin Retry button disappears.
 *
 * Called after a successful charge (automatic or manual retry), after a
 * gateway-sync reconciliation, or whenever the subscription transitions
 * out of a state where retries make sense.
 *
 * @param int $subscription_id The subscription post ID.
 * @return void
 */
function arraysubs_reset_payment_retry_attempts(int $subscription_id): void
{
    delete_post_meta($subscription_id, '_payment_retry_attempts');
    delete_post_meta($subscription_id, '_payment_retry_last_attempt_at');
    delete_post_meta($subscription_id, '_payment_retry_next_attempt_at');
    delete_post_meta($subscription_id, '_last_payment_failure');
}

/**
 * Get the cancellation source for a subscription.
 *
 * Returns who initiated the cancellation: 'admin', 'customer', 'system',
 * 'gateway' (from webhook), or 'refund'.
 *
 * @param int $subscription_id The subscription post ID.
 * @return string Cancellation source or empty string.
 */
function arraysubs_get_cancellation_source(int $subscription_id): string
{
    if ($subscription_id <= 0) {
        return '';
    }

    return (string) get_post_meta($subscription_id, '_cancellation_source', true);
}

/**
 * Check if auto-renew is enabled for a subscription.
 *
 * Returns true (default) when the subscription has not explicitly
 * disabled auto-renew. Only subscriptions using automatic payment
 * gateways should be checked against this function.
 *
 * When the meta key '_auto_renew' is absent or set to 'on', the function
 * returns true (normal automatic payment behavior). When set to 'off',
 * the subscription will fall back to manual renewal invoices.
 *
 * @param int $subscription_id The subscription post ID.
 * @return bool True if auto-renew is enabled or not explicitly disabled.
 */
function arraysubs_is_auto_renew_enabled(int $subscription_id): bool
{
    if ($subscription_id <= 0) {
        return true;
    }

    $auto_renew = get_post_meta($subscription_id, '_auto_renew', true);

    // Default behavior: auto-renew is enabled when meta is absent or set to 'on'
    return empty($auto_renew) || 'on' === $auto_renew;
}

/**
 * Check if the auto-renew toggle feature is enabled in settings.
 *
 * This is a store-level setting that controls whether customers are
 * allowed to toggle auto-renew on/off from the customer portal.
 *
 * @return bool True if the auto-renew toggle feature is enabled.
 */
function arraysubs_is_auto_renew_toggle_allowed(): bool
{
    if (!function_exists('arraysubs_get_setting')) {
        return false;
    }

    return (bool) arraysubs_get_setting('automatic_payments.allow_auto_renew_toggle', false);
}

/**
 * Classify a payment failure reason into a stable internal category.
 *
 * Pro gateways pass their raw decline_code / error code (Stripe) or
 * adjacent payload (Paddle / PayPal) and an optional human-readable
 * message. The category is used to drive consistent customer-facing copy
 * and to gate retry / dunning behavior — e.g. an `expired_card` failure
 * is not worth automatic retries, while `processing_error` usually is.
 *
 * Categories returned (stable identifiers — do not rename without an
 * audit of consumers):
 *  - insufficient_funds
 *  - card_declined
 *  - expired_card
 *  - incorrect_cvc
 *  - authentication_required
 *  - processing_error
 *  - invalid_card
 *  - generic_decline
 *  - unknown
 *
 * @param string $code    Gateway code (e.g. Stripe `decline_code` or `error.code`).
 * @param string $message Optional human message used as a fallback signal.
 * @return string One of the category identifiers above.
 */
function arraysubs_classify_payment_failure_reason(string $code, string $message = ''): string
{
    $code = strtolower(trim($code));
    $message_lower = strtolower($message);

    $direct = [
        'insufficient_funds'              => 'insufficient_funds',
        'card_declined'                   => 'card_declined',
        'do_not_honor'                    => 'card_declined',
        'transaction_not_allowed'         => 'card_declined',
        'generic_decline'                 => 'generic_decline',
        'expired_card'                    => 'expired_card',
        'card_expired'                    => 'expired_card',
        'incorrect_cvc'                   => 'incorrect_cvc',
        'invalid_cvc'                     => 'incorrect_cvc',
        'incorrect_number'                => 'invalid_card',
        'invalid_number'                  => 'invalid_card',
        'invalid_expiry_month'            => 'invalid_card',
        'invalid_expiry_year'             => 'invalid_card',
        'authentication_required'         => 'authentication_required',
        'requires_action'                 => 'authentication_required',
        'processing_error'                => 'processing_error',
        'rate_limit'                      => 'processing_error',
        'try_again_later'                 => 'processing_error',
    ];

    if (isset($direct[$code])) {
        return $direct[$code];
    }

    // Fallback: sniff common phrases when only a free-text message is available.
    if ($message_lower !== '') {
        if (str_contains($message_lower, 'insufficient')) {
            return 'insufficient_funds';
        }
        if (str_contains($message_lower, 'expired')) {
            return 'expired_card';
        }
        if (str_contains($message_lower, 'cvc') || str_contains($message_lower, 'security code')) {
            return 'incorrect_cvc';
        }
        if (str_contains($message_lower, 'authenticate') || str_contains($message_lower, '3d secure')) {
            return 'authentication_required';
        }
        if (str_contains($message_lower, 'declined')) {
            return 'card_declined';
        }
    }

    return 'unknown';
}

/**
 * Map a payment failure category to a customer-facing description.
 *
 * @param string $category Category from arraysubs_classify_payment_failure_reason().
 * @return string Translated short description suitable for emails and notes.
 */
function arraysubs_describe_payment_failure_category(string $category): string
{
    switch ($category) {
        case 'insufficient_funds':
            return __('insufficient funds on the card', 'arraysubs');
        case 'card_declined':
            return __('the card was declined by the issuer', 'arraysubs');
        case 'generic_decline':
            return __('the card was declined', 'arraysubs');
        case 'expired_card':
            return __('the card has expired', 'arraysubs');
        case 'incorrect_cvc':
            return __('the card security code (CVC) was incorrect', 'arraysubs');
        case 'invalid_card':
            return __('the card details on file are invalid', 'arraysubs');
        case 'authentication_required':
            return __('the bank requires customer authentication (3D Secure)', 'arraysubs');
        case 'processing_error':
            return __('a temporary processing error occurred at the gateway', 'arraysubs');
        case 'unknown':
        default:
            return __('an unknown error', 'arraysubs');
    }
}
