<?php

/**
 * Proration Calculator
 *
 * Calculates proration amounts for subscription plan switches.
 *
 * @package ArraySubs\Features\PlanSwitching\Services
 * @since 1.0.0
 */

namespace ArraySubs\Features\PlanSwitching\Services;

defined('ABSPATH') || exit;

/**
 * Proration calculation service
 */
class ProrationCalculator
{
    /**
     * Proration type: Prorate immediately
     */
    const PRORATE_IMMEDIATELY = 'prorate_immediately';

    /**
     * Proration type: Apply at next renewal
     */
    const APPLY_AT_RENEWAL = 'apply_at_renewal';

    /**
     * Proration type: No proration
     */
    const NO_PRORATION = 'no_proration';

    /**
     * Calculate proration for a plan switch
     *
     * @param int    $subscription_id  Subscription ID
     * @param int    $new_product_id   Target product/variation ID
     * @param string $proration_type   Proration type (see constants)
     * @return array Proration details
     */
    public static function calculate(int $subscription_id, int $new_product_id, string $proration_type = self::PRORATE_IMMEDIATELY): array
    {
        $subscription_id = absint($subscription_id);
        $new_product_id = absint($new_product_id);

        $subscription = arraysubs_get_subscription($subscription_id);
        if (!$subscription instanceof \WP_Post) {
            return [
                'success' => false,
                'error' => __('Subscription not found', 'arraysubs'),
            ];
        }

        // Get current subscription details
        $current_product_id = get_post_meta($subscription_id, '_variation_id', true)
            ?: get_post_meta($subscription_id, '_product_id', true);
        $current_price = floatval(get_post_meta($subscription_id, '_recurring_amount', true));
        $billing_period = get_post_meta($subscription_id, '_billing_period', true);
        $billing_interval = absint(get_post_meta($subscription_id, '_billing_interval', true)) ?: 1;
        $last_payment_date = get_post_meta($subscription_id, '_last_payment_date', true);
        $next_payment_date = get_post_meta($subscription_id, '_next_payment_date', true);
        $currency = get_post_meta($subscription_id, '_currency', true) ?: get_woocommerce_currency();

        // Get new product details
        $new_product = wc_get_product($new_product_id);
        if (!$new_product) {
            return [
                'success' => false,
                'error' => __('Target product not found', 'arraysubs'),
            ];
        }

        $new_price = self::resolveProductPrice($new_product);

        // Get new billing period from product meta
        $new_billing_period = get_post_meta($new_product_id, '_subscription_period', true) ?: $billing_period;
        $new_billing_interval = absint(get_post_meta($new_product_id, '_subscription_interval', true)) ?: $billing_interval;

        // Determine switch type
        $switch_type = self::determineSwitchType(absint($current_product_id), $new_product_id, $subscription_id);

        // Calculate days in cycle and days remaining
        $cycle_days = self::getCycleDays($billing_period, $billing_interval);
        $days_used = self::getDaysUsed($last_payment_date);
        $days_remaining = max(0, $cycle_days - $days_used);

        // Calculate daily rate for current plan
        $current_daily_rate = $cycle_days > 0 ? $current_price / $cycle_days : 0;

        // Calculate credit for unused time on current plan
        $credit_amount = round($current_daily_rate * $days_remaining, 2);

        // Handle different billing cycles (e.g., monthly to annual)
        $is_cycle_change = ($billing_period !== $new_billing_period || $billing_interval !== $new_billing_interval);

        // Calculate new cycle days
        $new_cycle_days = self::getCycleDays($new_billing_period, $new_billing_interval);
        $new_daily_rate = $new_cycle_days > 0 ? $new_price / $new_cycle_days : 0;

        // Calculate what to charge
        $current_product = wc_get_product(absint($current_product_id));
        $proration_result = [
            'success' => true,
            'subscription_id' => $subscription_id,
            'current_product_id' => absint($current_product_id),
            'new_product_id' => $new_product_id,
            'switch_type' => $switch_type,
            'proration_type' => $proration_type,
            'currency' => $currency,
            'current_plan' => [
                'product_name' => $current_product ? arraysubs_get_subscription_product_name($current_product) : '',
                'product_url' => $current_product ? $current_product->get_permalink() : '',
                'price' => $current_price,
                'billing_period' => $billing_period,
                'billing_interval' => $billing_interval,
                'daily_rate' => round($current_daily_rate, 4),
                'cycle_days' => $cycle_days,
            ],
            'new_plan' => [
                'product_name' => arraysubs_get_subscription_product_name($new_product),
                'product_url' => $new_product->get_permalink(),
                'price' => $new_price,
                'billing_period' => $new_billing_period,
                'billing_interval' => $new_billing_interval,
                'daily_rate' => round($new_daily_rate, 4),
                'cycle_days' => $new_cycle_days,
            ],
            'days_used' => $days_used,
            'days_remaining' => $days_remaining,
            'is_cycle_change' => $is_cycle_change,
            'next_payment_date' => $next_payment_date,
        ];

        if ($proration_type === self::NO_PRORATION) {
            // No proration - charge full new price
            $proration_result['credit_amount'] = 0;
            $proration_result['charge_amount'] = $new_price;
            $proration_result['net_amount'] = $new_price;
            $proration_result['new_next_payment_date'] = gmdate('Y-m-d H:i:s', strtotime("+{$new_billing_interval} {$new_billing_period}"));
        } elseif ($proration_type === self::APPLY_AT_RENEWAL) {
            // Apply at next renewal - no immediate charge
            $proration_result['credit_amount'] = 0;
            $proration_result['charge_amount'] = 0;
            $proration_result['net_amount'] = 0;
            $proration_result['new_next_payment_date'] = $next_payment_date; // Keep same date
            $proration_result['new_renewal_price'] = $new_price;
        } else {
            // Prorate immediately
            $proration_result['credit_amount'] = $credit_amount;

            if ($is_cycle_change) {
                // Cycle change - charge full new price minus credit
                $proration_result['charge_amount'] = $new_price;
                $proration_result['net_amount'] = max(0, $new_price - $credit_amount);
                $proration_result['new_next_payment_date'] = gmdate('Y-m-d H:i:s', strtotime("+{$new_billing_interval} {$new_billing_period}"));
            } else {
                // Same cycle - charge for remaining days at new rate
                $charge_amount = round($new_daily_rate * $days_remaining, 2);
                $proration_result['charge_amount'] = $charge_amount;
                $proration_result['net_amount'] = round($charge_amount - $credit_amount, 2);
                $proration_result['new_next_payment_date'] = $next_payment_date; // Keep same renewal date
            }

            // Handle negative amounts (downgrade with credit)
            if ($proration_result['net_amount'] < 0) {
                $proration_result['refund_amount'] = abs($proration_result['net_amount']);
                $proration_result['net_amount'] = 0;
            }
        }

        // Switch fee is a one-time charge tied to the act of switching plans.
        // Always include it in `switch_fee` so the UI/order can surface it,
        // and add it on top of the immediate net for charge-on-switch modes.
        // For "Apply at Renewal" we surface it but do not add it to the
        // immediate net (the fee will be charged with the next invoice).
        $switch_fee = self::getSwitchFee($switch_type);
        if ($switch_fee > 0) {
            $proration_result['switch_fee'] = round($switch_fee, 2);

            if ($proration_type !== self::APPLY_AT_RENEWAL) {
                $proration_result['net_amount'] = round($proration_result['net_amount'] + $switch_fee, 2);
            }
        }

        // Add formatted amounts for display
        $proration_result['formatted'] = [
            'credit' => wc_price($proration_result['credit_amount'], ['currency' => $currency]),
            'charge' => wc_price($proration_result['charge_amount'] ?? 0, ['currency' => $currency]),
            'net' => wc_price($proration_result['net_amount'], ['currency' => $currency]),
            'refund' => isset($proration_result['refund_amount'])
                ? wc_price($proration_result['refund_amount'], ['currency' => $currency])
                : null,
            'switch_fee' => isset($proration_result['switch_fee'])
                ? wc_price($proration_result['switch_fee'], ['currency' => $currency])
                : null,
        ];

        return apply_filters('arraysubs_proration_calculation', $proration_result, $subscription_id, $new_product_id);
    }

    /**
     * Resolve the recurring unit price for a product using the canonical fallback order.
     *
     * @param \WC_Product $product Product object.
     * @return float
     */
    public static function resolveProductPrice(\WC_Product $product): float
    {
        $price = (float) $product->get_price();
        if ($price <= 0) {
            $price = (float) $product->get_regular_price();
        }

        return max(0.0, $price);
    }

    /**
     * Apply subscription quantity to a proration result.
     *
     * @param array $proration Proration result.
     * @param int   $quantity  Subscription quantity.
     * @return array
     */
    public static function applyQuantity(array $proration, int $quantity): array
    {
        $quantity = max(1, $quantity);

        if ($quantity <= 1) {
            $proration['quantity'] = 1;
            return $proration;
        }

        $currency = $proration['currency'] ?? get_woocommerce_currency();

        $proration['quantity'] = $quantity;
        $proration['credit_amount'] = (float) ($proration['credit_amount'] ?? 0) * $quantity;
        $proration['charge_amount'] = (float) ($proration['charge_amount'] ?? 0) * $quantity;
        $proration['net_amount'] = (float) ($proration['net_amount'] ?? 0) * $quantity;

        if (isset($proration['refund_amount'])) {
            $proration['refund_amount'] = (float) $proration['refund_amount'] * $quantity;
        }

        if (isset($proration['switch_fee'])) {
            $proration['switch_fee'] = (float) $proration['switch_fee'] * $quantity;
        }

        $proration['formatted'] = [
            'credit' => wc_price($proration['credit_amount'], ['currency' => $currency]),
            'charge' => wc_price($proration['charge_amount'], ['currency' => $currency]),
            'net' => wc_price($proration['net_amount'], ['currency' => $currency]),
            'refund' => isset($proration['refund_amount'])
                ? wc_price($proration['refund_amount'], ['currency' => $currency])
                : null,
            'switch_fee' => isset($proration['switch_fee'])
                ? wc_price($proration['switch_fee'], ['currency' => $currency])
                : null,
        ];

        return $proration;
    }

    /**
     * Calculate prorated refund amount for cancellation
     *
     * @param int         $subscription_id Subscription ID
     * @param string|null $cancel_date     Optional cancel date (defaults to now)
     * @return array Refund calculation details
     */
    public static function calculateProrationRefund(int $subscription_id, ?string $cancel_date = null): array
    {
        $subscription_id = absint($subscription_id);
        $subscription = arraysubs_get_subscription($subscription_id);

        if (!$subscription instanceof \WP_Post) {
            return [
                'success' => false,
                'error' => __('Subscription not found', 'arraysubs'),
            ];
        }

        $last_payment_date = get_post_meta($subscription_id, '_last_payment_date', true);
        $next_payment_date = get_post_meta($subscription_id, '_next_payment_date', true);
        $recurring_amount = floatval(get_post_meta($subscription_id, '_recurring_amount', true));
        $billing_period = get_post_meta($subscription_id, '_billing_period', true);
        $billing_interval = absint(get_post_meta($subscription_id, '_billing_interval', true)) ?: 1;
        $currency = get_post_meta($subscription_id, '_currency', true) ?: get_woocommerce_currency();

        $cancel_date = $cancel_date ?? current_time('mysql', true);

        // Calculate days
        $cycle_days = self::getCycleDays($billing_period, $billing_interval);
        $days_used = self::getDaysUsed($last_payment_date, $cancel_date);
        $days_unused = max(0, $cycle_days - $days_used);

        // Calculate prorated refund
        $daily_rate = $cycle_days > 0 ? $recurring_amount / $cycle_days : 0;
        $refund_amount = round($daily_rate * $days_unused, 2);

        // Apply rounding method from settings
        $rounding_method = arraysubs_get_setting('proration.rounding_method', 'round');
        switch ($rounding_method) {
            case 'round_down':
                $refund_amount = floor($refund_amount * 100) / 100;
                break;
            case 'round_up':
                $refund_amount = ceil($refund_amount * 100) / 100;
                break;
            default:
                $refund_amount = round($refund_amount, 2);
        }

        return [
            'success' => true,
            'subscription_id' => $subscription_id,
            'cancel_date' => $cancel_date,
            'cycle_days' => $cycle_days,
            'days_used' => $days_used,
            'days_unused' => $days_unused,
            'recurring_amount' => $recurring_amount,
            'daily_rate' => round($daily_rate, 4),
            'refund_amount' => $refund_amount,
            'currency' => $currency,
            'formatted' => [
                'refund' => wc_price($refund_amount, ['currency' => $currency]),
            ],
        ];
    }

    /**
     * Determine switch type based on prices
     *
     * @param int      $current_product_id Current product ID
     * @param int      $new_product_id     New product ID
     * @param int|null $subscription_id    Current subscription ID. When given, the
     *                                     current plan's price/period/interval are
     *                                     read from the subscription record (which
     *                                     may differ from product meta, e.g. for
     *                                     flexible subscriptions).
     * @return string 'upgrade', 'downgrade', or 'crossgrade'
     */
    public static function determineSwitchType(int $current_product_id, int $new_product_id, ?int $subscription_id = null): string
    {
        // Get prices
        $current_price = self::getProductRecurringPrice($current_product_id);
        $new_price = self::getProductRecurringPrice($new_product_id);

        // Get billing periods for normalization
        $current_period = get_post_meta($current_product_id, '_subscription_period', true) ?: 'month';
        $current_interval = absint(get_post_meta($current_product_id, '_subscription_interval', true)) ?: 1;
        $new_period = get_post_meta($new_product_id, '_subscription_period', true) ?: 'month';
        $new_interval = absint(get_post_meta($new_product_id, '_subscription_interval', true)) ?: 1;

        // Prefer the subscription's actual billing terms over product defaults.
        if ($subscription_id !== null && $subscription_id > 0) {
            $subscription_price = (float) get_post_meta($subscription_id, '_recurring_amount', true);
            $subscription_period = (string) get_post_meta($subscription_id, '_billing_period', true);
            $subscription_interval = absint(get_post_meta($subscription_id, '_billing_interval', true));

            if ($subscription_price > 0) {
                $current_price = $subscription_price;
            }
            if ('' !== $subscription_period) {
                $current_period = $subscription_period;
            }
            if ($subscription_interval > 0) {
                $current_interval = $subscription_interval;
            }
        }

        // Normalize to daily rates for comparison
        $current_daily = self::getCycleDays($current_period, $current_interval) > 0
            ? $current_price / self::getCycleDays($current_period, $current_interval)
            : 0;
        $new_daily = self::getCycleDays($new_period, $new_interval) > 0
            ? $new_price / self::getCycleDays($new_period, $new_interval)
            : 0;

        // Allow 5% tolerance for "same price" (crossgrade)
        $tolerance = $current_daily * 0.05;

        if ($new_daily > $current_daily + $tolerance) {
            return 'upgrade';
        } elseif ($new_daily < $current_daily - $tolerance) {
            return 'downgrade';
        }

        return 'crossgrade';
    }

    /**
     * Get product recurring price
     *
     * @param int $product_id Product ID
     * @return float
     */
    private static function getProductRecurringPrice(int $product_id): float
    {
        $product = wc_get_product($product_id);
        if (!$product) {
            return 0;
        }

        return self::resolveProductPrice($product);
    }

    /**
     * Get number of days in a billing cycle
     *
     * @param string $period   Billing period (day, week, month, year)
     * @param int    $interval Billing interval
     * @return int
     */
    public static function getCycleDays(string $period, int $interval = 1): int
    {
        $days_map = [
            'day' => 1,
            'week' => 7,
            'month' => 30, // Approximation
            'year' => 365,
        ];

        $base_days = $days_map[$period] ?? 30;
        return $base_days * $interval;
    }

    /**
     * Get days used since last payment
     *
     * @param string      $last_payment_date Last payment date
     * @param string|null $current_date      Current date (defaults to now)
     * @return float
     */
    public static function getDaysUsed(string $last_payment_date, ?string $current_date = null): float
    {
        if (empty($last_payment_date)) {
            return 0;
        }

        $current_date = $current_date ?? current_time('mysql', true);
        $diff = strtotime($current_date) - strtotime($last_payment_date);

        return round(max(0, $diff / DAY_IN_SECONDS), 2);
    }

    /**
     * Format a day count without dropping useful fractional precision.
     *
     * @param float $days      Day count.
     * @param int   $precision Maximum decimal places.
     * @return string
     */
    public static function formatDayCount(float $days, int $precision = 4): string
    {
        $rounded = round($days, $precision);

        if (abs($rounded - round($rounded)) < 0.000001) {
            return (string) (int) round($rounded);
        }

        return rtrim(rtrim(number_format($rounded, $precision, '.', ''), '0'), '.');
    }

    /**
     * Get switch fee based on switch type
     *
     * @param string $switch_type Switch type (upgrade, downgrade, crossgrade)
     * @return float
     */
    private static function getSwitchFee(string $switch_type): float
    {
        $fees = arraysubs_get_setting('proration.switch_fees', []);

        return floatval($fees[$switch_type] ?? 0);
    }

    /**
     * Get available switch options for a subscription
     *
     * @param int $subscription_id Subscription ID
     * @return array
     */
    public static function getAvailableSwitchOptions(int $subscription_id): array
    {
        $subscription_id = absint($subscription_id);
        $subscription = arraysubs_get_subscription($subscription_id);

        if (!$subscription instanceof \WP_Post) {
            return [];
        }

        // Get current product
        $current_product_id = get_post_meta($subscription_id, '_variation_id', true)
            ?: get_post_meta($subscription_id, '_product_id', true);

        if (!$current_product_id) {
            return [];
        }

        // Honor the global Allowed Switch Types toggles. When a switch type is
        // disabled, return an empty list for that bucket so the UI cannot offer
        // it even if the product is configured with target plans.
        $allow_upgrades = (bool) arraysubs_get_setting('plan_switching.allow_upgrades', true);
        $allow_downgrades = (bool) arraysubs_get_setting('plan_switching.allow_downgrades', true);
        $allow_crossgrades = (bool) arraysubs_get_setting('plan_switching.allow_crossgrades', true);

        // Get configured switch options
        $upgrade_products = $allow_upgrades
            ? (get_post_meta($current_product_id, '_arraysubs_upgrade_products', true) ?: [])
            : [];
        $downgrade_products = $allow_downgrades
            ? (get_post_meta($current_product_id, '_arraysubs_downgrade_products', true) ?: [])
            : [];
        $crossgrade_products = $allow_crossgrades
            ? (get_post_meta($current_product_id, '_arraysubs_crossgrade_products', true) ?: [])
            : [];

        $options = [
            'upgrades' => [],
            'downgrades' => [],
            'crossgrades' => [],
        ];

        // Always preview options against the admin-configured proration type
        // so what the customer sees in the modal matches what the server will
        // actually charge during executeSwitch.
        $configured_proration_type = (string) arraysubs_get_setting(
            'plan_switching.proration_type',
            self::PRORATE_IMMEDIATELY
        );
        if (!in_array($configured_proration_type, [self::PRORATE_IMMEDIATELY, self::APPLY_AT_RENEWAL, self::NO_PRORATION], true)) {
            $configured_proration_type = self::PRORATE_IMMEDIATELY;
        }

        // Process function
        $process_products = function ($products, $type) use ($current_product_id, $subscription_id, $configured_proration_type, &$options) {
            foreach ((array) $products as $product_id) {
                $product = wc_get_product(absint($product_id));
                if (!$product) {
                    continue;
                }

                $price = self::resolveProductPrice($product);
                $billing_period = get_post_meta(absint($product_id), '_subscription_period', true) ?: 'month';
                $billing_interval = absint(get_post_meta(absint($product_id), '_subscription_interval', true)) ?: 1;

                // Force switch type label based on category (settings)
                $label_type = $type;

                // Get product name with variation suffix and product URL
                $product_name = arraysubs_get_subscription_product_name($product);
                $product_url = $product->get_permalink();

                $option = [
                    'product_id' => absint($product_id),
                    'name' => $product_name,
                    'product_name' => $product_name,
                    'product_url' => $product_url,
                    'price' => $price,
                    'formatted_price' => self::formatPriceWithPeriod($price, $billing_period, $billing_interval),
                    'billing_period' => $billing_period,
                    'billing_interval' => $billing_interval,
                    'switch_type' => $label_type,
                ];

                // Calculate proration preview using the admin-configured type
                $proration = self::calculate($subscription_id, absint($product_id), $configured_proration_type);
                if ($proration['success']) {
                    $option['proration'] = [
                        'credit' => $proration['credit_amount'],
                        'charge' => $proration['charge_amount'] ?? 0,
                        'net' => $proration['net_amount'],
                        'refund' => $proration['refund_amount'] ?? 0,
                        'switch_fee' => $proration['switch_fee'] ?? 0,
                        'formatted' => $proration['formatted'],
                    ];
                }

                $options[$type . 's'][] = $option;
            }
        };

        $process_products($upgrade_products, 'upgrade');
        $process_products($downgrade_products, 'downgrade');
        $process_products($crossgrade_products, 'crossgrade');

        return $options;
    }

    /**
     * Format price with billing period for display
     *
     * @param float  $price           Price amount
     * @param string $billing_period  Billing period (day, week, month, year)
     * @param int    $billing_interval Billing interval
     * @return string Formatted price string
     */
    private static function formatPriceWithPeriod(float $price, string $billing_period, int $billing_interval): string
    {
        $formatted_price = wc_price($price);

        $period_labels = [
            'day' => sprintf(
                /* translators: %d: number of days subscription is paused */
                _n('%d day', '%d days', $billing_interval, 'arraysubs'),
                $billing_interval
            ),
            'week' => sprintf(
                /* translators: %d: number of weeks */
                _n('%d week', '%d weeks', $billing_interval, 'arraysubs'),
                $billing_interval
            ),
            'month' => sprintf(
                /* translators: %d: number of months */
                _n('%d month', '%d months', $billing_interval, 'arraysubs'),
                $billing_interval
            ),
            'year' => sprintf(
                /* translators: %d: number of years */
                _n('%d year', '%d years', $billing_interval, 'arraysubs'),
                $billing_interval
            ),
        ];

        $period_label = $period_labels[$billing_period] ?? $billing_period;

        if ($billing_interval > 1) {
            $period_label = sprintf($period_label, $billing_interval);
        }

        return sprintf(
            /* translators: 1: price, 2: billing period */
            __('%1$s / %2$s', 'arraysubs'),
            $formatted_price,
            $period_label
        );
    }
}
