<?php

/**
 * Cancellation Helper Functions
 *
 * Global helper functions for subscription cancellation operations.
 *
 * @package ArraySubs
 * @since 1.0.0
 */

defined('ABSPATH') || exit;

use ArraySubs\Supports\ActionScheduler;

// =====================================================
// Cancellation Status Functions
// =====================================================

/**
 * Check if subscription is waiting for cancellation (scheduled for end-of-period)
 *
 * @param int $subscription_id Subscription ID
 * @return bool True if waiting for cancellation
 */
function arraysubs_is_waiting_cancellation(int $subscription_id): bool
{
    return (bool) get_post_meta($subscription_id, '_waiting_cancellation', true);
}

/**
 * Get scheduled cancellation date for a subscription
 *
 * @param int $subscription_id Subscription ID
 * @return string|null Scheduled cancellation date or null if not scheduled
 */
function arraysubs_get_scheduled_cancel_date(int $subscription_id): ?string
{
    $date = get_post_meta($subscription_id, '_cancellation_scheduled_date', true);
    return !empty($date) ? $date : null;
}

/**
 * Get cancellation reason for a subscription
 *
 * @param int $subscription_id Subscription ID
 * @return string|null Cancellation reason or null
 */
function arraysubs_get_cancellation_reason(int $subscription_id): ?string
{
    $reason = get_post_meta($subscription_id, '_cancellation_reason', true);
    return !empty($reason) ? $reason : null;
}

/**
 * Get detailed cancellation reason data (all form responses)
 *
 * @param int $subscription_id Subscription ID
 * @return array Cancellation reason details
 */
function arraysubs_get_cancellation_reason_details(int $subscription_id): array
{
    $details = get_post_meta($subscription_id, '_cancellation_reason_details', true);
    if (is_string($details)) {
        $details = json_decode($details, true);
    }
    return is_array($details) ? $details : [];
}

/**
 * Get who cancelled the subscription
 *
 * @param int $subscription_id Subscription ID
 * @return string|null 'customer', 'admin:{user_id}', 'gateway', 'system', or null
 */
function arraysubs_get_cancelled_by(int $subscription_id): ?string
{
    $cancelled_by = get_post_meta($subscription_id, '_cancelled_by', true);
    return !empty($cancelled_by) ? $cancelled_by : null;
}

/**
 * Get cancellation type
 *
 * @param int $subscription_id Subscription ID
 * @return string|null 'customer', 'admin', 'system', or null
 */
function arraysubs_get_cancellation_type(int $subscription_id): ?string
{
    $type = get_post_meta($subscription_id, '_cancellation_type', true);
    return !empty($type) ? $type : null;
}

// =====================================================
// Cancellation Action Functions
// =====================================================

/**
 * Schedule a subscription for cancellation at end of billing period
 *
 * @param int    $subscription_id Subscription ID
 * @param string $reason          Optional cancellation reason
 * @param string $cancelled_by    Who initiated: 'customer', 'admin:{user_id}', 'gateway', 'system'
 * @param array  $reason_details  Optional additional form responses
 * @return bool True on success
 */
function arraysubs_schedule_cancellation(
    int $subscription_id,
    string $reason = '',
    string $cancelled_by = 'system',
    array $reason_details = []
): bool {
    $subscription = arraysubs_get_subscription($subscription_id);
    if (!$subscription instanceof \WP_Post) {
        return false;
    }

    // Terminal status guard: scheduling a cancellation on an already-
    // cancelled or expired subscription is a no-op. Otherwise the lifecycle
    // listeners (notes, emails, AS jobs) re-fire on a dead record.
    if (in_array($subscription->post_status, ['arraysubs-cancelled', 'arraysubs-expired'], true)) {
        return false;
    }

    // Get the scheduled cancel date (next payment date or end date)
    $next_payment = get_post_meta($subscription_id, '_next_payment_date', true);
    $end_date = get_post_meta($subscription_id, '_end_date', true);

    // Use end_date if it exists and is before next_payment, otherwise use next_payment
    $scheduled_date = $next_payment;
    if (!empty($end_date)) {
        if (empty($next_payment) || strtotime($end_date) < strtotime($next_payment)) {
            $scheduled_date = $end_date;
        }
    }

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

    $scheduled_ts = strtotime($scheduled_date);
    if ($scheduled_ts === false || $scheduled_ts <= time()) {
        return false;
    }

    $already_waiting = (bool) get_post_meta($subscription_id, '_waiting_cancellation', true);
    $existing_date = (string) get_post_meta($subscription_id, '_cancellation_scheduled_date', true);
    $is_resave = $already_waiting && $existing_date === $scheduled_date;

    if ($is_resave) {
        update_post_meta($subscription_id, '_cancelled_by', sanitize_text_field($cancelled_by));

        if (!empty($reason)) {
            update_post_meta($subscription_id, '_cancellation_reason', sanitize_text_field($reason));
        }

        if (!empty($reason_details)) {
            update_post_meta($subscription_id, '_cancellation_reason_details', wp_json_encode($reason_details));
        }

        return true;
    }

    ActionScheduler::unscheduleAll(
        ActionScheduler::HOOK_CANCEL_SUBSCRIPTION,
        [$subscription_id],
        ActionScheduler::GROUP_STATUS
    );

    // Set waiting cancellation meta
    update_post_meta($subscription_id, '_waiting_cancellation', true);
    update_post_meta($subscription_id, '_cancellation_scheduled_date', $scheduled_date);
    update_post_meta($subscription_id, '_cancelled_by', sanitize_text_field($cancelled_by));
    update_post_meta($subscription_id, '_cancellation_type', 'end_of_period');

    if (!empty($reason)) {
        update_post_meta($subscription_id, '_cancellation_reason', sanitize_text_field($reason));
    }

    if (!empty($reason_details)) {
        update_post_meta($subscription_id, '_cancellation_reason_details', wp_json_encode($reason_details));
    }

    if (function_exists('arraysubs_clear_pending_switch')) {
        arraysubs_clear_pending_switch($subscription_id);
    }

    // Schedule the cancellation action
    ActionScheduler::scheduleSingle(
        ActionScheduler::HOOK_CANCEL_SUBSCRIPTION,
        $scheduled_ts,
        [$subscription_id],
        ActionScheduler::GROUP_STATUS
    );

    do_action('arraysubs_data_waiting_cancellation', $subscription_id, $reason);

    return true;
}

/**
 * Cancel a subscription immediately
 *
 * @param int    $subscription_id Subscription ID
 * @param string $reason          Optional cancellation reason
 * @param string $cancelled_by    Who initiated: 'customer', 'admin:{user_id}', 'gateway', 'system'
 * @param array  $reason_details  Optional additional form responses
 * @return bool True on success
 */
function arraysubs_cancel_subscription_immediately(
    int $subscription_id,
    string $reason = '',
    string $cancelled_by = 'system',
    array $reason_details = []
): bool {
    $subscription = arraysubs_get_subscription($subscription_id);
    if (!$subscription instanceof \WP_Post) {
        return false;
    }

    // Terminal status guard: cancelling an already-cancelled or expired
    // subscription is a no-op (do not re-fire lifecycle actions).
    if (in_array($subscription->post_status, ['arraysubs-cancelled', 'arraysubs-expired'], true)) {
        return true;
    }

    // Detect whether this immediate cancellation is overriding a
    // previously scheduled end-of-period cancellation. Captured before
    // we clear the meta so downstream listeners (notes, emails, audit
    // logs) can present the correct narrative — "cancellation took
    // effect immediately, overriding the previously scheduled
    // cancellation" rather than the misleading default.
    $override_scheduled = (bool) get_post_meta($subscription_id, '_waiting_cancellation', true);

    // IMPORTANT: order matters. All meta must be in its final state
    // before wp_update_post() flips the post status, because the
    // transition_post_status hook fires synchronously inside that call
    // and the AutoNotes status-change listener reads these metas to
    // build its message. Updating meta after wp_update_post() leaves
    // the listener seeing stale data.
    update_post_meta($subscription_id, '_cancelled_date', current_time('mysql', true));
    update_post_meta($subscription_id, '_cancelled_by', sanitize_text_field($cancelled_by));
    update_post_meta($subscription_id, '_cancellation_type', 'immediate');

    if ($override_scheduled) {
        update_post_meta($subscription_id, '_cancellation_override_scheduled', '1');
    } else {
        delete_post_meta($subscription_id, '_cancellation_override_scheduled');
    }

    if (!empty($reason)) {
        update_post_meta($subscription_id, '_cancellation_reason', sanitize_text_field($reason));
    }

    if (!empty($reason_details)) {
        update_post_meta($subscription_id, '_cancellation_reason_details', wp_json_encode($reason_details));
    }

    // Clear any waiting cancellation meta. Both UI badges (admin React
    // detail card and customer portal banner) gate on
    // _waiting_cancellation, so this single delete eliminates the
    // "Cancelled + Cancels at end of period" double-badge state.
    delete_post_meta($subscription_id, '_waiting_cancellation');
    delete_post_meta($subscription_id, '_cancellation_scheduled_date');

    // Clear any payment-retry pipeline state so the retry scheduler does
    // not chase a cancelled subscription.
    delete_post_meta($subscription_id, '_last_payment_failure');
    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');

    if (function_exists('arraysubs_clear_pending_switch')) {
        arraysubs_clear_pending_switch($subscription_id);
    }

    // Update status to cancelled
    $result = wp_update_post([
        'ID' => $subscription_id,
        'post_status' => 'arraysubs-cancelled',
    ]);

    if (is_wp_error($result)) {
        return false;
    }

    // Cancel any scheduled actions for this subscription
    ActionScheduler::unscheduleAllForSubscription($subscription_id);

    // Fire actions
    do_action('arraysubs_data_cancelled', $subscription_id, $subscription, 'immediate');

    return true;
}

/**
 * Cancel a subscription using the default behavior from settings
 *
 * @param int    $subscription_id Subscription ID
 * @param string $reason          Optional cancellation reason
 * @param string $cancelled_by    Who initiated
 * @param array  $reason_details  Optional additional form responses
 * @return bool True on success
 */
function arraysubs_cancel_subscription(
    int $subscription_id,
    string $reason = '',
    string $cancelled_by = 'system',
    array $reason_details = []
): bool {
    $cancel_immediately = arraysubs_cancel_immediately();

    if ($cancel_immediately) {
        return arraysubs_cancel_subscription_immediately($subscription_id, $reason, $cancelled_by, $reason_details);
    }

    return arraysubs_schedule_cancellation($subscription_id, $reason, $cancelled_by, $reason_details);
}

/**
 * Undo a scheduled cancellation
 *
 * @param int    $subscription_id Subscription ID
 * @param string $undone_by       Who undid: 'customer', 'admin:{user_id}'
 * @return bool True on success
 */
function arraysubs_undo_scheduled_cancellation(int $subscription_id, string $undone_by = 'system'): bool
{
    $subscription = arraysubs_get_subscription($subscription_id);
    if (!$subscription instanceof \WP_Post) {
        return false;
    }

    // Check if subscription is actually waiting for cancellation
    $waiting_cancellation = get_post_meta($subscription_id, '_waiting_cancellation', true);
    if (!$waiting_cancellation) {
        return false;
    }

    // Remove the waiting cancellation flags
    delete_post_meta($subscription_id, '_waiting_cancellation');
    delete_post_meta($subscription_id, '_cancellation_scheduled_date');
    delete_post_meta($subscription_id, '_cancellation_reason');
    delete_post_meta($subscription_id, '_cancellation_reason_details');
    delete_post_meta($subscription_id, '_cancelled_by');
    delete_post_meta($subscription_id, '_cancellation_type');

    // Unschedule the cancellation action
    ActionScheduler::unscheduleAll(
        ActionScheduler::HOOK_CANCEL_SUBSCRIPTION,
        [$subscription_id],
        ActionScheduler::GROUP_STATUS
    );

    // Add subscription note
    if (function_exists('arraysubs_add_subscription_note')) {
        $note = __('Scheduled cancellation removed. Subscription will continue renewing normally.', 'arraysubs');
        $is_customer = strpos($undone_by, 'customer') !== false;
        arraysubs_add_subscription_note(
            $subscription_id,
            $note,
            $is_customer ? 'customer' : 'private',
            get_current_user_id(),
            [],
            'subscription'
        );
    }

    // Fire action
    do_action('arraysubs_data_cancellation_undone', $subscription_id);

    return true;
}

// =====================================================
// Cancellation Validation Functions
// =====================================================

/**
 * Check if a subscription can be cancelled by customer
 *
 * @param int $subscription_id Subscription ID
 * @return bool True if subscription can be cancelled
 */
function arraysubs_can_customer_cancel_by_subs_id(int $subscription_id): bool
{
    $subscription = arraysubs_get_subscription($subscription_id);
    if (!$subscription instanceof \WP_Post) {
        return false;
    }

    // Customers can cancel any non-terminal subscription state that can still
    // represent an active customer relationship.
    $cancellable_statuses = apply_filters(
        'arraysubs_customer_cancellable_statuses',
        ['arraysubs-active', 'arraysubs-on-hold', 'arraysubs-trial', 'arraysubs-pending'],
        $subscription_id,
        $subscription
    );
    $cancellable_statuses = is_array($cancellable_statuses) ? $cancellable_statuses : [];

    if (!in_array($subscription->post_status, $cancellable_statuses, true)) {
        return false;
    }

    // Already waiting for cancellation - can only undo, not re-cancel
    if (arraysubs_is_waiting_cancellation($subscription_id)) {
        return false;
    }

    // Allow filtering
    return apply_filters('arraysubs_can_customer_cancel', true, $subscription_id, $subscription);
}

/**
 * Check if a subscription can be cancelled by admin
 *
 * @param int $subscription_id Subscription ID
 * @return bool True if subscription can be cancelled
 */
function arraysubs_can_admin_cancel(int $subscription_id): bool
{
    $subscription = arraysubs_get_subscription($subscription_id);
    if (!$subscription instanceof \WP_Post) {
        return false;
    }

    // Admin can cancel any subscription that's not already cancelled
    $non_cancellable_statuses = ['arraysubs-cancelled', 'arraysubs-expired'];
    if (in_array($subscription->post_status, $non_cancellable_statuses, true)) {
        return false;
    }

    // Allow filtering
    return apply_filters('arraysubs_can_admin_cancel', true, $subscription_id, $subscription);
}

/**
 * Check if a scheduled cancellation can be undone
 *
 * @param int $subscription_id Subscription ID
 * @return bool True if cancellation can be undone
 */
function arraysubs_can_undo_cancellation(int $subscription_id): bool
{
    // Must be waiting for cancellation
    if (!arraysubs_is_waiting_cancellation($subscription_id)) {
        return false;
    }

    // Check scheduled date hasn't passed
    $scheduled_date = arraysubs_get_scheduled_cancel_date($subscription_id);
    if ($scheduled_date && strtotime($scheduled_date) <= time()) {
        return false;
    }

    return true;
}

// =====================================================
// Retention Offer Functions
// =====================================================

/**
 * Get retention offers for a subscription based on reason
 *
 * @param int         $subscription_id Subscription ID
 * @param string|null $reason          Selected cancellation reason (optional)
 * @return array Array of applicable retention offers
 */
function arraysubs_get_retention_offers_for_subscription(int $subscription_id, ?string $reason = null): array
{
    if (!arraysubs_is_retention_offers_enabled()) {
        return [];
    }

    // Get all default offers (filtered by settings)
    $all_offers = apply_filters('arraysubs_default_retention_offers', []);

    // Filter to only enabled offers
    $enabled_offers = [];
    foreach ($all_offers as $offer_id => $offer) {
        if (empty($offer['enabled'])) {
            continue;
        }

        // Check if offer should be shown for this reason
        $trigger_reasons = $offer['trigger_reasons'] ?? [];
        if (!empty($trigger_reasons) && !empty($reason)) {
            if (!in_array($reason, $trigger_reasons, true)) {
                continue;
            }
        }

        // For downgrade offer, check if there are downgrade options
        if ($offer['type'] === 'downgrade') {
            $downgrade_options = arraysubs_get_subscription_switch_options($subscription_id, 'downgrade');
            if (empty($downgrade_options)) {
                continue;
            }
        }

        $enabled_offers[$offer_id] = $offer;
    }

    return apply_filters('arraysubs_retention_offers_for_subscription', $enabled_offers, $subscription_id, $reason);
}

/**
 * Track that retention offers were shown to customer
 *
 * @param int   $subscription_id Subscription ID
 * @param array $offers_shown    Array of offer IDs that were shown
 */
function arraysubs_track_retention_offers_shown(int $subscription_id, array $offers_shown): void
{
    update_post_meta($subscription_id, '_retention_offers_shown', $offers_shown);
    update_post_meta($subscription_id, '_retention_offers_shown_date', current_time('mysql', true));
}

/**
 * Get which retention offers were shown to customer
 *
 * @param int $subscription_id Subscription ID
 * @return array Array of offer IDs that were shown
 */
function arraysubs_get_retention_offers_shown(int $subscription_id): array
{
    $shown = get_post_meta($subscription_id, '_retention_offers_shown', true);
    return is_array($shown) ? $shown : [];
}

/**
 * Get which retention offer was accepted (if any)
 *
 * @param int $subscription_id Subscription ID
 * @return string|null Accepted offer type or null
 */
function arraysubs_get_accepted_retention_offer(int $subscription_id): ?string
{
    $accepted = get_post_meta($subscription_id, '_accepted_retention_offer', true);

    if (empty($accepted)) {
        $accepted = get_post_meta($subscription_id, '_retention_offer_type', true);
    }

    return !empty($accepted) ? $accepted : null;
}

/**
 * Get the effective recurring amount for a subscription.
 *
 * This reflects the currently active retention discount when present.
 *
 * @param int $subscription_id Subscription ID.
 * @return float
 */
function arraysubs_get_subscription_effective_recurring_amount(int $subscription_id): float
{
    $processor_class = '\\ArraySubs\\Features\\CancellationFlow\\Services\\RetentionOfferProcessor';

    if (class_exists($processor_class) && method_exists($processor_class, 'getEffectiveRecurringAmount')) {
        return (float) $processor_class::getEffectiveRecurringAmount($subscription_id);
    }

    return (float) get_post_meta($subscription_id, '_recurring_amount', true);
}

/**
 * Get the active retention discount summary for a subscription.
 *
 * @param int $subscription_id Subscription ID.
 * @return array|null
 */
function arraysubs_get_subscription_active_retention_discount(int $subscription_id): ?array
{
    $processor_class = '\\ArraySubs\\Features\\CancellationFlow\\Services\\RetentionOfferProcessor';

    if (class_exists($processor_class) && method_exists($processor_class, 'getActiveDiscount')) {
        $discount = $processor_class::getActiveDiscount($subscription_id);
        return is_array($discount) ? $discount : null;
    }

    return null;
}

// =====================================================
// Cancellation Reporting Functions
// =====================================================

/**
 * Get cancellation summary for a subscription
 *
 * @param int $subscription_id Subscription ID
 * @return array|null Cancellation summary or null if not cancelled
 */
function arraysubs_get_cancellation_summary(int $subscription_id): ?array
{
    $subscription = arraysubs_get_subscription($subscription_id);
    if (!$subscription instanceof \WP_Post) {
        return null;
    }

    // Check if subscription is cancelled or waiting cancellation
    $is_cancelled = $subscription->post_status === 'arraysubs-cancelled';
    $is_waiting = arraysubs_is_waiting_cancellation($subscription_id);

    if (!$is_cancelled && !$is_waiting) {
        return null;
    }

    $summary = [
        'subscription_id' => $subscription_id,
        'is_cancelled' => $is_cancelled,
        'is_waiting' => $is_waiting,
        'cancelled_date' => get_post_meta($subscription_id, '_cancelled_date', true) ?: null,
        'scheduled_date' => arraysubs_get_scheduled_cancel_date($subscription_id),
        'cancellation_type' => arraysubs_get_cancellation_type($subscription_id),
        'cancelled_by' => arraysubs_get_cancelled_by($subscription_id),
        'reason' => arraysubs_get_cancellation_reason($subscription_id),
        'reason_details' => arraysubs_get_cancellation_reason_details($subscription_id),
        'retention_offers_shown' => arraysubs_get_retention_offers_shown($subscription_id),
        'retention_offer_accepted' => arraysubs_get_accepted_retention_offer($subscription_id),
        'subscription_age_at_cancel' => get_post_meta($subscription_id, '_subscription_age_at_cancel', true) ?: null,
        'payments_at_cancel' => get_post_meta($subscription_id, '_payments_at_cancel', true) ?: null,
    ];

    return $summary;
}

/**
 * Get all cancellation reasons (key => label).
 *
 * Reads custom reasons from settings first, falls back to hard-coded
 * defaults, then applies the `arraysubs_default_cancellation_reasons`
 * filter so third-party code can still customise the list.
 *
 * This function is the single source of truth for cancellation reasons
 * and must be used everywhere instead of calling the filter directly
 * with an empty default.
 *
 * @return array<string, string> Key => Label map of cancellation reasons.
 */
function arraysubs_get_cancellation_reasons(): array
{
    $settings_reasons = arraysubs_get_setting('cancellation.reasons', []);

    if (! empty($settings_reasons) && is_array($settings_reasons)) {
        $formatted = [];
        foreach ($settings_reasons as $reason) {
            if (is_array($reason) && ! empty($reason['key']) && ! empty($reason['label'])) {
                $formatted[$reason['key']] = $reason['label'];
            }
        }

        if (! empty($formatted)) {
            return apply_filters('arraysubs_default_cancellation_reasons', $formatted);
        }
    }

    $defaults = [
        'too_expensive'    => __('Too expensive', 'arraysubs'),
        'not_using'        => __('Not using it enough', 'arraysubs'),
        'found_alternative' => __('Found a better alternative', 'arraysubs'),
        'missing_features' => __('Missing features I need', 'arraysubs'),
        'technical_issues' => __('Technical issues', 'arraysubs'),
        'temporary_pause'  => __('Just need a temporary break', 'arraysubs'),
        'other'            => __('Other', 'arraysubs'),
    ];

    return apply_filters('arraysubs_default_cancellation_reasons', $defaults);
}

/**
 * Get human-readable cancellation reason label
 *
 * @param string $reason_key Reason key
 * @return string Human-readable label
 */
function arraysubs_get_cancellation_reason_label(string $reason_key): string
{
    $reasons = arraysubs_get_cancellation_reasons();

    if (isset($reasons[$reason_key])) {
        return $reasons[$reason_key];
    }

    // Fallback: convert key to label
    return ucfirst(str_replace(['_', '-'], ' ', $reason_key));
}

/**
 * Check if subscription switch options exist
 *
 * Helper function used by retention offers.
 *
 * @param int    $subscription_id Subscription ID
 * @param string $type            'upgrade', 'downgrade', or 'crossgrade'
 * @return array Array of switch options
 */
function arraysubs_get_subscription_switch_options(int $subscription_id, string $type = 'downgrade'): array
{
    // Use PlanSwitching feature if available
    if (class_exists('\ArraySubs\Features\PlanSwitching\Services\SwitchManager')) {
        try {
            return \ArraySubs\Features\PlanSwitching\Services\SwitchManager::getSwitchOptions($subscription_id, $type);
        } catch (\Exception $e) {
            return [];
        }
    }

    return [];
}
