<?php

/**
 * Renewal sync helper functions.
 *
 * @package ArraySubs\Functions
 */

defined('ABSPATH') || exit;

/**
 * Check whether new subscriptions should align renewal dates to billing-cycle boundaries.
 *
 * @return bool
 */
function arraysubs_is_renewal_sync_enabled(): bool
{
    return (bool) arraysubs_get_setting('renewals.sync_to_billing_cycle', false);
}

/**
 * Get the first-charge mode for synced renewals.
 *
 * @return string
 */
function arraysubs_get_renewal_sync_first_charge_mode(): string
{
    $mode = sanitize_key((string) arraysubs_get_setting('renewals.sync_first_charge_mode', 'prorate'));

    return in_array($mode, ['prorate', 'full'], true) ? $mode : 'prorate';
}

/**
 * Determine whether a payment gateway supports synced renewal signup pricing.
 *
 * Core manual/offline gateways and Stripe are supported. Custom manual
 * gateways can opt in with the arraysubs_renewal_sync_manual_gateway_ids or
 * arraysubs_renewal_sync_supported_gateway filters.
 *
 * @param string $gateway_id Payment gateway ID.
 * @param mixed  $gateway    Optional gateway object.
 * @return bool
 */
function arraysubs_is_renewal_sync_supported_gateway(string $gateway_id = '', $gateway = null): bool
{
    $gateway_id = sanitize_key($gateway_id);

    if ('' === $gateway_id && is_object($gateway) && isset($gateway->id)) {
        $gateway_id = sanitize_key((string) $gateway->id);
    }

    if ('' === $gateway_id && null === $gateway) {
        return true;
    }

    $manual_gateway_ids = array_map(
        'sanitize_key',
        (array) apply_filters(
            'arraysubs_renewal_sync_manual_gateway_ids',
            [
                'bacs',
                'cheque',
                'check',
                'cod',
                'invoice',
                'manual',
                'offline',
                'bank_transfer',
                'bank-transfer',
            ]
        )
    );

    $supported = false;

    if (in_array($gateway_id, ['stripe', 'arraysubs_stripe'], true)) {
        $supported = true;
    } elseif (in_array($gateway_id, ['paypal', 'paddle', 'arraysubs_paypal', 'arraysubs_paddle'], true)) {
        $supported = false;
    } elseif (is_object($gateway) && method_exists($gateway, 'supportsSubscriptionCapability')) {
        $supported = ! $gateway->supportsSubscriptionCapability('automatic_payments');
    } elseif (in_array($gateway_id, $manual_gateway_ids, true)) {
        $supported = true;
    }

    /**
     * Filter whether a gateway supports synced renewal signup pricing.
     *
     * @param bool   $supported  Current support decision.
     * @param string $gateway_id Payment gateway ID.
     * @param mixed  $gateway    Optional gateway object.
     */
    return (bool) apply_filters('arraysubs_renewal_sync_supported_gateway', $supported, $gateway_id, $gateway);
}

/**
 * Check whether renewal sync can apply to a subscription product data array.
 *
 * @param array $subscription_data Subscription product data.
 * @return bool
 */
function arraysubs_subscription_data_supports_renewal_sync(array $subscription_data): bool
{
    $period = (string) ($subscription_data['period'] ?? 'month');
    $trial_length = (int) ($subscription_data['trial_length'] ?? 0);

    $supports = arraysubs_is_renewal_sync_enabled()
        && 'lifetime' !== $period
        && $trial_length <= 0;

    /**
     * Filter whether renewal sync applies to subscription product data.
     *
     * @param bool  $supports          Current support decision.
     * @param array $subscription_data Subscription product data.
     */
    return (bool) apply_filters('arraysubs_subscription_data_supports_renewal_sync', $supports, $subscription_data);
}

/**
 * Build the renewal sync context for checkout displays, cart pricing, and subscription creation.
 *
 * @param array  $subscription_data Subscription product data.
 * @param int    $quantity          Cart/order item quantity.
 * @param string $start_date        UTC MySQL datetime. Defaults to now.
 * @param string $gateway_id        Optional gateway ID.
 * @param mixed  $gateway           Optional gateway object.
 * @return array<string,mixed>
 */
function arraysubs_get_renewal_sync_context(
    array $subscription_data,
    int $quantity = 1,
    string $start_date = '',
    string $gateway_id = '',
    $gateway = null
): array {
    $quantity = max(1, absint($quantity));
    $start_date = '' !== $start_date ? $start_date : current_time('mysql', true);
    $full_unit_amount = max(0, (float) ($subscription_data['price'] ?? 0));
    $full_line_amount = $full_unit_amount * $quantity;
    $mode = arraysubs_get_renewal_sync_first_charge_mode();
    $currency = function_exists('get_woocommerce_currency') ? get_woocommerce_currency() : 'USD';

    $context = [
        'applies'                 => false,
        'reason'                  => '',
        'mode'                    => $mode,
        'quantity'                => $quantity,
        'start_date'              => $start_date,
        'cycle_start_date'        => '',
        'next_payment_date'       => '',
        'full_unit_amount'        => $full_unit_amount,
        'full_line_amount'        => $full_line_amount,
        'initial_unit_amount'     => $full_unit_amount,
        'initial_line_amount'     => $full_line_amount,
        'proration_ratio'         => 1.0,
        'gateway_minimum_amount'  => 0.0,
        'gateway_id'              => sanitize_key($gateway_id),
        'gateway_supported'       => arraysubs_is_renewal_sync_supported_gateway($gateway_id, $gateway),
    ];

    if (! arraysubs_subscription_data_supports_renewal_sync($subscription_data)) {
        $context['reason'] = 'ineligible_product';
        return $context;
    }

    if (! $context['gateway_supported']) {
        $context['reason'] = 'unsupported_gateway';
        return $context;
    }

    $interval = max(1, (int) ($subscription_data['interval'] ?? 1));
    $period = (string) ($subscription_data['period'] ?? 'month');
    $dates = arraysubs_calculate_renewal_sync_cycle_dates($start_date, $interval, $period);

    if (empty($dates['next_payment_date'])) {
        $context['reason'] = 'invalid_cycle';
        return $context;
    }

    $context['applies'] = true;
    $context['reason'] = 'applied';
    $context['cycle_start_date'] = $dates['cycle_start_date'];
    $context['next_payment_date'] = $dates['next_payment_date'];

    if ('prorate' === $mode) {
        $proration = arraysubs_calculate_renewal_sync_prorated_amount(
            $full_unit_amount,
            $dates['cycle_start_date'],
            $dates['next_payment_date'],
            $start_date
        );

        $context['initial_unit_amount'] = $proration['amount'];
        $context['initial_line_amount'] = $proration['amount'] * $quantity;
        $context['proration_ratio'] = $proration['ratio'];

        $minimum_amount = arraysubs_get_renewal_sync_gateway_minimum_amount($gateway_id, $gateway, $currency);
        $signup_fee = max(0, (float) ($subscription_data['signup_fee'] ?? 0));

        if (
            $minimum_amount > 0
            && $context['initial_line_amount'] > 0
            && ($context['initial_line_amount'] + $signup_fee) < $minimum_amount
            && ($full_line_amount + $signup_fee) >= $minimum_amount
        ) {
            $context['gateway_minimum_amount'] = $minimum_amount;
            $context['initial_line_amount'] = $minimum_amount - $signup_fee;
            $context['initial_unit_amount'] = $context['initial_line_amount'] / $quantity;
        }
    }

    /**
     * Filter the renewal sync checkout context.
     *
     * @param array  $context           Renewal sync context.
     * @param array  $subscription_data Subscription product data.
     * @param int    $quantity          Cart/order item quantity.
     * @param string $start_date        UTC MySQL datetime.
     */
    return (array) apply_filters('arraysubs_renewal_sync_context', $context, $subscription_data, $quantity, $start_date);
}

/**
 * Calculate the site-local billing-cycle start and next synced renewal date.
 *
 * Returned values are UTC MySQL datetimes because subscription scheduling
 * stores UTC values.
 *
 * @param string $start_date UTC MySQL datetime.
 * @param int    $interval   Billing interval.
 * @param string $period     Billing period.
 * @return array{cycle_start_date:string,next_payment_date:string}
 */
function arraysubs_calculate_renewal_sync_cycle_dates(string $start_date, int $interval, string $period): array
{
    if ('lifetime' === $period) {
        return [
            'cycle_start_date'  => '',
            'next_payment_date' => '',
        ];
    }

    $interval = max(1, $interval);
    $site_timezone = function_exists('wp_timezone') ? wp_timezone() : new \DateTimeZone('UTC');

    try {
        $start_utc = new \DateTimeImmutable($start_date, new \DateTimeZone('UTC'));
    } catch (\Exception $e) {
        $start_utc = new \DateTimeImmutable('now', new \DateTimeZone('UTC'));
    }

    $start_site = $start_utc->setTimezone($site_timezone);

    switch ($period) {
        case 'day':
            $cycle_start = $start_site->setTime(0, 0, 0);
            $next_payment = $cycle_start->modify('+' . $interval . ' days');
            break;

        case 'week':
            $week_start = absint(get_option('start_of_week', 1)) % 7;
            $current_day = (int) $start_site->format('w');
            $days_since_start = ($current_day - $week_start + 7) % 7;
            $cycle_start = $start_site->modify('-' . $days_since_start . ' days')->setTime(0, 0, 0);
            $next_payment = $cycle_start->modify('+' . $interval . ' weeks');
            break;

        case 'year':
            $cycle_start = $start_site->setDate((int) $start_site->format('Y'), 1, 1)->setTime(0, 0, 0);
            $next_payment = $cycle_start->modify('+' . $interval . ' years');
            break;

        case 'month':
        default:
            $cycle_start = $start_site->modify('first day of this month')->setTime(0, 0, 0);
            $next_payment = $cycle_start->modify('+' . $interval . ' months');
            break;
    }

    if ($next_payment <= $start_site) {
        $next_payment = arraysubs_increment_renewal_sync_boundary($next_payment, $interval, $period);
    }

    return [
        'cycle_start_date'  => arraysubs_renewal_sync_to_utc_mysql($cycle_start),
        'next_payment_date' => arraysubs_renewal_sync_to_utc_mysql($next_payment),
    ];
}

/**
 * Calculate the prorated initial amount for a synced first charge.
 *
 * @param float  $full_amount       Full recurring unit amount.
 * @param string $cycle_start_date  UTC MySQL datetime.
 * @param string $next_payment_date UTC MySQL datetime.
 * @param string $start_date        UTC MySQL datetime.
 * @return array{amount:float,ratio:float}
 */
function arraysubs_calculate_renewal_sync_prorated_amount(
    float $full_amount,
    string $cycle_start_date,
    string $next_payment_date,
    string $start_date
): array {
    if ($full_amount <= 0) {
        return [
            'amount' => 0.0,
            'ratio'  => 0.0,
        ];
    }

    $site_timezone = function_exists('wp_timezone') ? wp_timezone() : new \DateTimeZone('UTC');

    try {
        $cycle_start = (new \DateTimeImmutable($cycle_start_date, new \DateTimeZone('UTC')))->setTimezone($site_timezone)->setTime(0, 0, 0);
        $next_payment = (new \DateTimeImmutable($next_payment_date, new \DateTimeZone('UTC')))->setTimezone($site_timezone)->setTime(0, 0, 0);
        $start = (new \DateTimeImmutable($start_date, new \DateTimeZone('UTC')))->setTimezone($site_timezone)->setTime(0, 0, 0);
    } catch (\Exception $e) {
        return [
            'amount' => $full_amount,
            'ratio'  => 1.0,
        ];
    }

    if ($next_payment <= $cycle_start) {
        return [
            'amount' => $full_amount,
            'ratio'  => 1.0,
        ];
    }

    $cycle_days = max(1, (int) $cycle_start->diff($next_payment)->format('%a'));

    if ($start <= $cycle_start) {
        $remaining_days = $cycle_days;
    } elseif ($start >= $next_payment) {
        $remaining_days = 0;
    } else {
        $days_until_next_cycle = (int) $start->diff($next_payment)->format('%a');
        $remaining_days = max(1, $days_until_next_cycle - 1);
    }

    $ratio = min(1, $remaining_days / $cycle_days);
    $raw_amount = $full_amount * $ratio;
    $decimals = function_exists('wc_get_price_decimals') ? wc_get_price_decimals() : 2;
    $amount = round($raw_amount, $decimals);

    if ($raw_amount > 0 && $amount <= 0) {
        $amount = $decimals > 0 ? (float) (1 / (10 ** $decimals)) : 1.0;
    }

    /**
     * Filter the prorated synced-renewal signup amount.
     *
     * @param float  $amount            Rounded prorated unit amount.
     * @param float  $full_amount       Full recurring unit amount.
     * @param float  $ratio             Proration ratio.
     * @param string $cycle_start_date  UTC MySQL datetime.
     * @param string $next_payment_date UTC MySQL datetime.
     * @param string $start_date        UTC MySQL datetime.
     */
    $amount = (float) apply_filters(
        'arraysubs_renewal_sync_prorated_amount',
        $amount,
        $full_amount,
        $ratio,
        $cycle_start_date,
        $next_payment_date,
        $start_date
    );

    return [
        'amount' => max(0, $amount),
        'ratio'  => $ratio,
    ];
}

/**
 * Convert a DateTime object to a UTC MySQL datetime string.
 *
 * @param \DateTimeInterface $date Date object.
 * @return string
 */
function arraysubs_renewal_sync_to_utc_mysql(\DateTimeInterface $date): string
{
    $utc = $date instanceof \DateTimeImmutable
        ? $date->setTimezone(new \DateTimeZone('UTC'))
        : \DateTimeImmutable::createFromInterface($date)->setTimezone(new \DateTimeZone('UTC'));

    return $utc->format('Y-m-d H:i:s');
}

/**
 * Parse a UTC MySQL datetime string to a Unix timestamp.
 *
 * @param string $date UTC MySQL datetime.
 * @return int
 */
function arraysubs_renewal_sync_utc_timestamp(string $date): int
{
    try {
        return (new \DateTimeImmutable($date, new \DateTimeZone('UTC')))->getTimestamp();
    } catch (\Exception $e) {
        return 0;
    }
}

/**
 * Get the minimum charge amount for a gateway in store currency.
 *
 * @param string $gateway_id Payment gateway ID.
 * @param mixed  $gateway    Optional gateway object.
 * @param string $currency   Currency code.
 * @return float
 */
function arraysubs_get_renewal_sync_gateway_minimum_amount(string $gateway_id = '', $gateway = null, string $currency = ''): float
{
    $gateway_id = sanitize_key($gateway_id);

    if ('' === $gateway_id && is_object($gateway) && isset($gateway->id)) {
        $gateway_id = sanitize_key((string) $gateway->id);
    }

    $currency = '' !== $currency
        ? strtoupper($currency)
        : (function_exists('get_woocommerce_currency') ? get_woocommerce_currency() : 'USD');
    $minimum_amount = 0.0;

    if (in_array($gateway_id, ['stripe', 'arraysubs_stripe'], true)) {
        $minimums = [
            'BDT' => 6500,
        ];
        $minimum_smallest_unit = $minimums[$currency]
            ?? (class_exists('\WC_Stripe_Helper') && method_exists('\WC_Stripe_Helper', 'get_minimum_amount')
                ? (int) \WC_Stripe_Helper::get_minimum_amount()
                : 50);

        $minimum_smallest_unit = (int) apply_filters('arraysubspro_stripe_minimum_amount', $minimum_smallest_unit, $currency);

        if (
            class_exists('\WC_Stripe_Helper')
            && method_exists('\WC_Stripe_Helper', 'convert_from_stripe_amount')
        ) {
            $minimum_amount = (float) \WC_Stripe_Helper::convert_from_stripe_amount($minimum_smallest_unit, $currency);
        } else {
            $minimum_amount = round($minimum_smallest_unit / 100, 2);
        }
    }

    /**
     * Filter the gateway minimum used for synced-renewal prorated signup charges.
     *
     * @param float  $minimum_amount Minimum gateway charge in store currency.
     * @param string $gateway_id      Gateway ID.
     * @param string $currency        Currency code.
     * @param mixed  $gateway         Gateway object.
     */
    return (float) apply_filters(
        'arraysubs_renewal_sync_gateway_minimum_amount',
        $minimum_amount,
        $gateway_id,
        $currency,
        $gateway
    );
}

/**
 * Get the currently selected checkout gateway for renewal sync pricing.
 *
 * @return array{id:string,gateway:mixed}
 */
function arraysubs_get_current_renewal_sync_gateway_context(): array
{
    $gateway_id = '';

    if (isset($_POST['payment_method'])) {
        $gateway_id = sanitize_key(wp_unslash((string) $_POST['payment_method']));
    }

    if ('' === $gateway_id && function_exists('WC') && WC()->session) {
        $gateway_id = sanitize_key((string) WC()->session->get('chosen_payment_method', ''));
    }

    // No explicit choice yet (fresh checkout session): fall back to the first
    // available gateway, which is what the checkout UI preselects. Without
    // this, gateway-aware pricing (e.g. the Stripe minimum bump) computes an
    // unbumped total while the customer is looking at the preselected Stripe
    // form, so Stripe's Payment Element refuses to render for tiny carts.
    if ('' === $gateway_id && function_exists('WC') && WC()->payment_gateways() && !is_admin()) {
        static $arraysubs_resolving_default_gateway = false;

        if (!$arraysubs_resolving_default_gateway) {
            $arraysubs_resolving_default_gateway = true;
            $available = WC()->payment_gateways()->get_available_payment_gateways();
            $arraysubs_resolving_default_gateway = false;

            if (!empty($available)) {
                $first = reset($available);
                $gateway_id = isset($first->id) ? sanitize_key((string) $first->id) : '';
            }
        }
    }

    $gateway = null;
    if ('' !== $gateway_id && function_exists('WC') && WC()->payment_gateways()) {
        $gateways = WC()->payment_gateways()->payment_gateways();
        $gateway = $gateways[$gateway_id] ?? null;
    }

    return [
        'id'      => $gateway_id,
        'gateway' => $gateway,
    ];
}

/**
 * Increment a renewal sync boundary by one billing interval.
 *
 * @param \DateTimeImmutable $boundary Boundary datetime.
 * @param int                $interval Billing interval.
 * @param string             $period   Billing period.
 * @return \DateTimeImmutable
 */
function arraysubs_increment_renewal_sync_boundary(\DateTimeImmutable $boundary, int $interval, string $period): \DateTimeImmutable
{
    switch ($period) {
        case 'day':
            return $boundary->modify('+' . $interval . ' days');

        case 'week':
            return $boundary->modify('+' . $interval . ' weeks');

        case 'year':
            return $boundary->modify('+' . $interval . ' years');

        case 'month':
        default:
            return $boundary->modify('+' . $interval . ' months');
    }
}
