<?php

namespace Licenzo\Services;

if (!defined('ABSPATH')) {
    exit;
}

use Licenzo\Helper\Action;
use Licenzo\Helper\Settings;
use Licenzo\Model\License;
use Licenzo\Model\Subscription;
use Licenzo\Framework\Support\Facades\DB;

class DunningService
{
    private const MAX_RETRY_ATTEMPTS = 5;
    private const BASE_RETRY_DELAY_HOURS = 24;
    private const DEFAULT_GRACE_PERIOD_DAYS = 7;
    private const MAX_GRACE_PERIOD_DAYS = 30;
    private const MAX_RETRY_CAP_DAYS = 30;
    private const HOURS_PER_DAY = 24;

    public static function processFailedPayment(Subscription $subscription): void
    {
        DB::beginTransaction();

        try {
            $failedCount = (int) $subscription->failed_payment_count + 1;
            $maxRetries = static::resolveMaxRetryAttempts();
            $nextRetry = static::getNextRetryTime($failedCount);

            $subscription->update([
                'failed_payment_count' => $failedCount,
                'last_payment_attempt' => gmdate('Y-m-d H:i:s'),
                'next_payment_attempt' => $nextRetry,
                'dunning_status'       => Subscription::DUNNING_ACTIVE,
            ]);

            if ($failedCount >= $maxRetries) {
                DB::commit();
                static::enterGracePeriod($subscription);
                return;
            }

            Action::do('notification_payment_failed', [
                'subscription_id'      => (int) $subscription->id,
                'customer_id'          => (int) $subscription->customer_id,
                'failed_payment_count' => $failedCount,
                'next_payment_attempt' => $nextRetry,
            ]);

            DB::commit();
        } catch (\Throwable $e) {
            DB::rollback();
            throw $e;
        }
    }

    public static function enterGracePeriod(Subscription $subscription): void
    {
        $graceDays = static::resolveGracePeriodDays();
        $graceEnd = gmdate('Y-m-d H:i:s', strtotime("+{$graceDays} days"));

        $subscription->update([
            'dunning_status'       => Subscription::DUNNING_PAUSED,
            'next_payment_attempt' => $graceEnd,
        ]);

        Action::do('notification_grace_period_started', [
            'subscription_id' => (int) $subscription->id,
            'customer_id'     => (int) $subscription->customer_id,
            'grace_end_date'  => $graceEnd,
            'grace_days'      => $graceDays,
        ]);
    }

    public static function checkGracePeriodExpiry(): void
    {
        $now = gmdate('Y-m-d H:i:s');

        $expired = Subscription::where('dunning_status', Subscription::DUNNING_PAUSED)
            ->whereNotNull('next_payment_attempt')
            ->where('next_payment_attempt', '<=', $now)
            ->get();

        foreach ($expired as $subscription) {
            static::suspendSubscription($subscription);
        }
    }

    public static function resetDunningState(Subscription $subscription): void
    {
        $subscription->update([
            'failed_payment_count' => 0,
            'dunning_status'       => null,
            'next_payment_attempt' => null,
            'last_payment_attempt' => null,
        ]);
    }

    public static function getNextRetryTime(int $attemptNumber): string
    {
        $settings = Settings::getSubscriptionSettings();
        $baseDays = max(1, (int) ($settings['retry_interval_days'] ?? 3));
        $baseHours = $baseDays * static::HOURS_PER_DAY;

        $delayHours = $baseHours * pow(2, max(0, $attemptNumber - 1));
        $capHours = static::MAX_RETRY_CAP_DAYS * static::HOURS_PER_DAY;
        $delayHours = (int) min($delayHours, $capHours);

        return gmdate('Y-m-d H:i:s', strtotime("+{$delayHours} hours"));
    }

    public static function shouldRetry(Subscription $subscription): bool
    {
        if ((string) $subscription->dunning_status === Subscription::DUNNING_EXHAUSTED) {
            return false;
        }

        return (int) $subscription->failed_payment_count < static::resolveMaxRetryAttempts();
    }

    /**
     * @return \Licenzo\Framework\Database\Eloquent\Collection
     */
    public static function getDueRetries()
    {
        $now = gmdate('Y-m-d H:i:s');
        $maxRetries = static::resolveMaxRetryAttempts();

        return Subscription::where('next_payment_attempt', '<=', $now)
            ->where('dunning_status', Subscription::DUNNING_ACTIVE)
            ->where('failed_payment_count', '>', 0)
            ->where('failed_payment_count', '<', $maxRetries)
            ->get();
    }

    private static function resolveMaxRetryAttempts(): int
    {
        $settings = Settings::getSubscriptionSettings();
        $retries = (int) ($settings['failed_payment_retries'] ?? static::MAX_RETRY_ATTEMPTS);

        return max(1, min(10, $retries));
    }

    private static function suspendSubscription(Subscription $subscription): void
    {
        $subscription->update([
            'status'         => Subscription::STATUS_CANCELED,
            'dunning_status' => Subscription::DUNNING_EXHAUSTED,
            'canceled_at'    => gmdate('Y-m-d H:i:s'),
        ]);

        $licenses = License::where('subscription_id', $subscription->id)->get();
        foreach ($licenses as $license) {
            $license->update(['status' => License::STATUS_INACTIVE]);
        }

        Action::do('notification_subscription_suspended', [
            'subscription_id' => (int) $subscription->id,
            'customer_id'     => (int) $subscription->customer_id,
        ]);
    }

    private static function resolveGracePeriodDays(): int
    {
        $settings = Settings::getSubscriptionSettings();
        $days = (int) ($settings['grace_period_days'] ?? static::DEFAULT_GRACE_PERIOD_DAYS);

        if ($days < 1) {
            return static::DEFAULT_GRACE_PERIOD_DAYS;
        }

        return min($days, static::MAX_GRACE_PERIOD_DAYS);
    }
}
