<?php

/**
 * Skip Manager Service
 *
 * Handles all skip renewal operations including validation, execution,
 * and date calculations for skipped billing cycles.
 *
 * @package ArraySubs\Features\SkipRenewal\Services
 * @since 1.0.0
 */

namespace ArraySubs\Features\SkipRenewal\Services;

defined('ABSPATH') || exit;

use ArraySubs\Features\RecurringBilling\Services\RenewalScheduler;
use ArraySubs\Supports\Config;

/**
 * Skip Manager class
 */
class SkipManager
{
    /**
     * Check if a subscription can skip cycles
     *
     * Validates against settings, cutoff date, and current skip status.
     *
     * @param int $subscription_id Subscription post ID
     * @return array {
     *     @type bool   $can_skip Whether skip is allowed
     *     @type string $reason   Reason if not allowed (translated)
     *     @type int    $max_cycles Maximum cycles that can be skipped
     * }
     */
    public function canSkip(int $subscription_id): array
    {
        $subscription = get_post($subscription_id);

        if (!$subscription instanceof \WP_Post || $subscription->post_type !== 'arraysubs_data') {
            return [
                'can_skip'   => false,
                'reason'     => __('Invalid subscription.', 'arraysubs'),
                'max_cycles' => 0,
            ];
        }

        // Check if skip feature is enabled
        $settings = $this->getSkipSettings();
        if (!$settings['enabled']) {
            return [
                'can_skip'   => false,
                'reason'     => __('Skip renewal feature is disabled.', 'arraysubs'),
                'max_cycles' => 0,
            ];
        }

        // Check subscription status - only active subscriptions can skip
        $allowed_statuses = ['arraysubs-active', 'arraysubs-trial'];
        if (!in_array($subscription->post_status, $allowed_statuses, true)) {
            return [
                'can_skip'   => false,
                'reason'     => __('Only active subscriptions can skip renewals.', 'arraysubs'),
                'max_cycles' => 0,
            ];
        }

        // Check if already skipping
        $current_skip = (int) get_post_meta($subscription_id, '_skip_cycles_remaining', true);
        if ($current_skip > 0) {
            return [
                'can_skip'   => false,
                'reason'     => sprintf(
                    /* translators: %d: number of cycles already being skipped */
                    __('Already skipping %d cycle(s). Modify or undo existing skip first.', 'arraysubs'),
                    $current_skip
                ),
                'max_cycles' => 0,
            ];
        }

        // Check cutoff date
        $next_payment_date = get_post_meta($subscription_id, '_next_payment_date', true);
        if (!empty($next_payment_date)) {
            $days_until_renewal = $this->getDaysUntilDate($next_payment_date);
            if ($days_until_renewal !== false && $days_until_renewal < $settings['cutoff_days']) {
                return [
                    'can_skip'   => false,
                    'reason'     => sprintf(
                        /* translators: %d: number of days before renewal when skip is blocked */
                        __('Cannot skip within %d day(s) of renewal date.', 'arraysubs'),
                        $settings['cutoff_days']
                    ),
                    'max_cycles' => 0,
                ];
            }
        }

        return [
            'can_skip'   => true,
            'reason'     => '',
            'max_cycles' => $settings['max_cycles'],
        ];
    }

    /**
     * Skip renewal cycles for a subscription
     *
     * @param int $subscription_id Subscription post ID
     * @param int $cycles         Number of cycles to skip
     * @param int $user_id        User ID who requested the skip (0 = system)
     * @param string $reason      Optional reason for skipping
     * @return bool Whether skip was successful
     */
    public function skip(int $subscription_id, int $cycles, int $user_id = 0, string $reason = ''): bool
    {
        $reason = sanitize_textarea_field($reason);

        // Validate can skip
        $can_skip = $this->canSkip($subscription_id);
        if (!$can_skip['can_skip']) {
            return false;
        }

        // Validate cycle count
        $settings = $this->getSkipSettings();
        $cycles = min(max(1, $cycles), $settings['max_cycles']);

        $subscription = get_post($subscription_id);
        if (!$subscription instanceof \WP_Post) {
            return false;
        }

        // Store original next payment date
        $original_next_payment = get_post_meta($subscription_id, '_next_payment_date', true);

        // Calculate new next payment date
        $billing_interval = (int) get_post_meta($subscription_id, '_billing_interval', true) ?: 1;
        $billing_period = get_post_meta($subscription_id, '_billing_period', true) ?: 'month';

        $new_next_payment = $this->calculateSkippedDate(
            $original_next_payment,
            $cycles,
            $billing_interval,
            $billing_period
        );

        // Update meta fields
        update_post_meta($subscription_id, '_skip_cycles_count', $cycles);
        update_post_meta($subscription_id, '_skip_cycles_remaining', $cycles);
        update_post_meta($subscription_id, '_original_next_payment_date', $original_next_payment);
        update_post_meta($subscription_id, '_skip_requested_at', current_time('mysql', true));
        update_post_meta($subscription_id, '_skip_requested_by', $user_id);
        update_post_meta($subscription_id, '_skip_reason', $reason);
        update_post_meta($subscription_id, '_next_payment_date', $new_next_payment);
        $this->rescheduleRenewal($subscription_id, $new_next_payment);

        // Add to skip history
        $this->addToSkipHistory($subscription_id, [
            'action'         => 'skip',
            'cycles'         => $cycles,
            'original_date'  => $original_next_payment,
            'new_date'       => $new_next_payment,
            'requested_by'   => $user_id,
            'requested_at'   => current_time('mysql', true),
            'reason'         => $reason,
        ]);

        // Fire action for integrations (notes, emails, etc.)
        do_action('arraysubs_cycles_skipped', $subscription_id, $cycles, $user_id, [
            'original_date' => $original_next_payment,
            'new_date'      => $new_next_payment,
            'reason'        => $reason,
        ]);

        return true;
    }

    /**
     * Undo a skip for a subscription
     *
     * Restores the original next payment date and clears skip meta.
     *
     * @param int $subscription_id Subscription post ID
     * @param int $user_id        User ID who is undoing (0 = system)
     * @return bool Whether undo was successful
     */
    public function undoSkip(int $subscription_id, int $user_id = 0): bool
    {
        $subscription = get_post($subscription_id);
        if (!$subscription instanceof \WP_Post) {
            return false;
        }

        // Check if currently skipping
        $skip_remaining = (int) get_post_meta($subscription_id, '_skip_cycles_remaining', true);
        if ($skip_remaining <= 0) {
            return false; // Nothing to undo
        }

        // Get original date
        $original_date = get_post_meta($subscription_id, '_original_next_payment_date', true);
        $current_date = get_post_meta($subscription_id, '_next_payment_date', true);

        // Restore original next payment date
        if (!empty($original_date)) {
            update_post_meta($subscription_id, '_next_payment_date', $original_date);
            $this->rescheduleRenewal($subscription_id, $original_date);
        }

        // Clear skip meta
        $this->clearSkipMeta($subscription_id);

        // Add to history
        $this->addToSkipHistory($subscription_id, [
            'action'         => 'undo',
            'cycles_undone'  => $skip_remaining,
            'restored_date'  => $original_date,
            'previous_date'  => $current_date,
            'requested_by'   => $user_id,
            'requested_at'   => current_time('mysql', true),
        ]);

        // Fire action for integrations
        do_action('arraysubs_skip_undone', $subscription_id, $user_id, [
            'cycles_undone' => $skip_remaining,
            'restored_date' => $original_date,
        ]);
        return true;
    }

    /**
     * Modify an existing skip (change cycle count)
     *
     * @param int $subscription_id Subscription post ID
     * @param int $new_cycles     New number of cycles to skip
     * @param int $user_id        User ID who is modifying (0 = system)
     * @return bool Whether modification was successful
     */
    public function modifySkip(int $subscription_id, int $new_cycles, int $user_id = 0): bool
    {
        $subscription = get_post($subscription_id);
        if (!$subscription instanceof \WP_Post) {
            return false;
        }

        // Check if currently skipping
        $old_cycles = (int) get_post_meta($subscription_id, '_skip_cycles_count', true);
        if ($old_cycles <= 0) {
            return false; // Nothing to modify
        }

        // Validate new cycle count
        $settings = $this->getSkipSettings();
        $new_cycles = min(max(1, $new_cycles), $settings['max_cycles']);

        if ($new_cycles === $old_cycles) {
            return true; // No change needed
        }

        // Get original date
        $original_date = get_post_meta($subscription_id, '_original_next_payment_date', true);
        $billing_interval = (int) get_post_meta($subscription_id, '_billing_interval', true) ?: 1;
        $billing_period = get_post_meta($subscription_id, '_billing_period', true) ?: 'month';

        // Calculate new next payment date from original
        $new_next_payment = $this->calculateSkippedDate(
            $original_date,
            $new_cycles,
            $billing_interval,
            $billing_period
        );

        // Calculate remaining based on change
        $cycles_consumed = $old_cycles - (int) get_post_meta($subscription_id, '_skip_cycles_remaining', true);
        $new_remaining = max(0, $new_cycles - $cycles_consumed);

        // Update meta
        update_post_meta($subscription_id, '_skip_cycles_count', $new_cycles);
        update_post_meta($subscription_id, '_skip_cycles_remaining', $new_remaining);
        update_post_meta($subscription_id, '_next_payment_date', $new_next_payment);
        $this->rescheduleRenewal($subscription_id, $new_next_payment);

        // Add to history
        $this->addToSkipHistory($subscription_id, [
            'action'       => 'modify',
            'old_cycles'   => $old_cycles,
            'new_cycles'   => $new_cycles,
            'new_date'     => $new_next_payment,
            'requested_by' => $user_id,
            'requested_at' => current_time('mysql', true),
        ]);

        // Fire action for integrations
        do_action('arraysubs_skip_modified', $subscription_id, $old_cycles, $new_cycles, $user_id);

        return true;
    }

    /**
     * Get the current skip status for a subscription
     *
     * @param int $subscription_id Subscription post ID
     * @return array Skip status information
     */
    public function getSkipStatus(int $subscription_id): array
    {
        $cycles_count = (int) get_post_meta($subscription_id, '_skip_cycles_count', true);
        $cycles_remaining = (int) get_post_meta($subscription_id, '_skip_cycles_remaining', true);

        return [
            'is_skipping'       => $cycles_remaining > 0,
            'cycles_total'      => $cycles_count,
            'cycles_remaining'  => $cycles_remaining,
            'cycles_completed'  => max(0, $cycles_count - $cycles_remaining),
            'original_date'     => get_post_meta($subscription_id, '_original_next_payment_date', true),
            'new_date'          => get_post_meta($subscription_id, '_next_payment_date', true),
            'requested_at'      => get_post_meta($subscription_id, '_skip_requested_at', true),
            'requested_by'      => (int) get_post_meta($subscription_id, '_skip_requested_by', true),
            'reason'            => get_post_meta($subscription_id, '_skip_reason', true),
        ];
    }

    /**
     * Get remaining skip allowance for a subscription
     *
     * @param int $subscription_id Subscription post ID
     * @return int Number of additional cycles that can be skipped
     */
    public function getRemainingSkipAllowance(int $subscription_id): int
    {
        $can_skip = $this->canSkip($subscription_id);
        return $can_skip['can_skip'] ? $can_skip['max_cycles'] : 0;
    }

    /**
     * Process a skipped cycle (called when billing would occur)
     *
     * Decrements the skip counter and advances dates if needed.
     *
     * @param int $subscription_id Subscription post ID
     * @return void
     */
    public function processSkippedCycle(int $subscription_id): void
    {
        $remaining = (int) get_post_meta($subscription_id, '_skip_cycles_remaining', true);

        if ($remaining <= 0) {
            return; // No skips remaining
        }

        $new_remaining = $remaining - 1;
        update_post_meta($subscription_id, '_skip_cycles_remaining', $new_remaining);

        // Fire action for tracking
        do_action('arraysubs_cycle_skipped_processed', $subscription_id, $remaining, $new_remaining);
        // If no more skips remaining, clear the skip meta
        if ($new_remaining <= 0) {
            $this->clearSkipMeta($subscription_id, true); // Keep history
            do_action('arraysubs_skip_cycles_completed', $subscription_id);
            return;
        }

        $this->restoreSkippedRenewalTargetDate($subscription_id);
    }

    /**
     * Complete all remaining skipped cycles.
     *
     * Used when the shifted renewal date has arrived and billing should continue
     * in the same scheduler pass instead of requiring one extra generate run.
     *
     * @param int $subscription_id Subscription post ID
     * @return void
     */
    public function completeSkippedCycles(int $subscription_id): void
    {
        $remaining = (int) get_post_meta($subscription_id, '_skip_cycles_remaining', true);

        if ($remaining <= 0) {
            return;
        }

        update_post_meta($subscription_id, '_skip_cycles_remaining', 0);

        do_action('arraysubs_cycle_skipped_processed', $subscription_id, $remaining, 0);

        $this->clearSkipMeta($subscription_id, true);
        do_action('arraysubs_skip_cycles_completed', $subscription_id);
    }

    /**
     * Get the renewal target date created by the current skip request.
     *
     * @param int $subscription_id Subscription post ID
     * @return string UTC MySQL date, or empty string when unavailable.
     */
    public function getSkippedRenewalTargetDate(int $subscription_id): string
    {
        $history = $this->getSkipHistory($subscription_id);

        for ($index = count($history) - 1; $index >= 0; $index--) {
            $entry = $history[$index];

            if (!is_array($entry)) {
                continue;
            }

            if (!in_array((string) ($entry['action'] ?? ''), ['skip', 'modify'], true)) {
                continue;
            }

            $new_date = sanitize_text_field((string) ($entry['new_date'] ?? ''));

            if ($new_date !== '') {
                return $new_date;
            }
        }

        $original_date = (string) get_post_meta($subscription_id, '_original_next_payment_date', true);

        if ($original_date === '') {
            return '';
        }

        $cycles = (int) get_post_meta($subscription_id, '_skip_cycles_count', true);

        if ($cycles <= 0) {
            return '';
        }

        $billing_interval = (int) get_post_meta($subscription_id, '_billing_interval', true) ?: 1;
        $billing_period   = get_post_meta($subscription_id, '_billing_period', true) ?: 'month';

        return $this->calculateSkippedDate(
            $original_date,
            $cycles,
            $billing_interval,
            $billing_period
        );
    }

    /**
     * Get skip settings
     *
     * @return array Settings with defaults
     */
    private function getSkipSettings(): array
    {
        $settings = Config::get('settings.skip_renewal', []);

        return wp_parse_args($settings, [
            'enabled'           => true,
            'max_cycles'        => 3,
            'cutoff_days'       => 2,
            'customer_can_skip' => true,
        ]);
    }

    /**
     * Calculate the new next payment date after skipping cycles
     *
     * @param string $current_date    Current next payment date
     * @param int    $cycles          Number of cycles to skip
     * @param int    $billing_interval Billing interval
     * @param string $billing_period   Billing period
     * @return string New next payment date
     */
    private function calculateSkippedDate(
        string $current_date,
        int $cycles,
        int $billing_interval,
        string $billing_period
    ): string {
        if (empty($current_date)) {
            return '';
        }

        $timestamp = strtotime($current_date);
        $total_interval = $billing_interval * $cycles;

        switch ($billing_period) {
            case 'day':
                $new_timestamp = strtotime("+{$total_interval} days", $timestamp);
                break;
            case 'week':
                $new_timestamp = strtotime("+{$total_interval} weeks", $timestamp);
                break;
            case 'month':
                $new_timestamp = strtotime("+{$total_interval} months", $timestamp);
                break;
            case 'year':
                $new_timestamp = strtotime("+{$total_interval} years", $timestamp);
                break;
            default:
                $new_timestamp = strtotime("+{$total_interval} months", $timestamp);
        }

        return gmdate('Y-m-d H:i:s', $new_timestamp);
    }

    /**
     * Get days until a given date
     *
     * @param string $date Date in MySQL format
     * @return int|false Days until date or false if invalid
     */
    private function getDaysUntilDate(string $date)
    {
        $timestamp = strtotime($date);
        if (!$timestamp) {
            return false;
        }

        $now = time();
        $diff = $timestamp - $now;

        return max(0, (int) floor($diff / DAY_IN_SECONDS));
    }

    /**
     * Add entry to skip history
     *
     * @param int   $subscription_id Subscription post ID
     * @param array $entry           History entry data
     * @return void
     */
    private function addToSkipHistory(int $subscription_id, array $entry): void
    {
        $history = $this->getSkipHistory($subscription_id);

        $history[] = $entry;

        // Keep only last 50 entries
        if (count($history) > 50) {
            $history = array_slice($history, -50);
        }

        update_post_meta($subscription_id, '_skip_history', wp_json_encode($history));
    }

    /**
     * Read skip history.
     *
     * @param int $subscription_id Subscription post ID
     * @return array<int, array<string, mixed>>
     */
    private function getSkipHistory(int $subscription_id): array
    {
        $history = get_post_meta($subscription_id, '_skip_history', true);
        $history = !empty($history) ? json_decode($history, true) : [];

        return is_array($history) ? $history : [];
    }

    /**
     * Reschedule renewal jobs after changing the next payment date.
     *
     * @param int    $subscription_id Subscription post ID
     * @param string $next_payment    Next payment date in UTC MySQL format
     * @return void
     */
    private function rescheduleRenewal(int $subscription_id, string $next_payment): void
    {
        RenewalScheduler::unschedule($subscription_id);

        $timestamp = strtotime($next_payment);

        if ($timestamp && $timestamp > time()) {
            RenewalScheduler::schedule($subscription_id, $timestamp);
        }
    }

    /**
     * Restore the renewal date to the shifted target while skipped cycles remain.
     *
     * @param int $subscription_id Subscription post ID
     * @return void
     */
    private function restoreSkippedRenewalTargetDate(int $subscription_id): void
    {
        $target_date = $this->getSkippedRenewalTargetDate($subscription_id);

        if ($target_date === '') {
            return;
        }

        update_post_meta($subscription_id, '_next_payment_date', $target_date);
        $this->rescheduleRenewal($subscription_id, $target_date);
    }

    /**
     * Clear skip meta fields
     *
     * @param int  $subscription_id Subscription post ID
     * @param bool $keep_history    Whether to preserve skip history
     * @return void
     */
    private function clearSkipMeta(int $subscription_id, bool $keep_history = false): void
    {
        update_post_meta($subscription_id, '_skip_cycles_count', 0);
        update_post_meta($subscription_id, '_skip_cycles_remaining', 0);
        delete_post_meta($subscription_id, '_original_next_payment_date');
        delete_post_meta($subscription_id, '_skip_requested_at');
        delete_post_meta($subscription_id, '_skip_requested_by');
        delete_post_meta($subscription_id, '_skip_reason');

        if (!$keep_history) {
            delete_post_meta($subscription_id, '_skip_history');
        }
    }
}
