<?php

namespace App\Actions;

use App\Enums\BillingCycle;
use App\Enums\InvoiceStatus;
use App\Enums\SubscriptionStatus;
use App\Enums\TenantStatus;
use App\Models\Plan;
use App\Models\Subscription;
use App\Models\SubscriptionInvoice;
use App\Models\Tenant;
use App\Services\ActivityLogger;
use App\Services\SettingsRepository;
use Illuminate\Support\Facades\DB;
use RuntimeException;

/**
 * The manual overrides.
 *
 * Real subscription businesses need them constantly: a customer paid cash to a
 * salesman, a demo shop needs two more weeks, a reseller negotiated a price.
 * Doing these by hand in the database is how billing data rots, so every
 * override lives here, is validated, and is logged.
 */
class AdjustSubscription
{
    public function __construct(
        protected ActivityLogger $activity,
        protected SettingsRepository $settings,
    ) {}

    /**
     * Push the end date out without taking payment.
     *
     * Extends from whichever is later — today or the current end date — so
     * granting a week to a live subscription adds a week rather than
     * truncating it.
     */
    public function extend(Subscription $subscription, int $days, string $reason): Subscription
    {
        if ($days === 0) {
            throw new RuntimeException('Enter the number of days to add or remove.');
        }

        $this->requireReason($reason);

        return DB::transaction(function () use ($subscription, $days, $reason) {
            $base = $subscription->ends_at && $subscription->ends_at->isFuture()
                ? $subscription->ends_at->copy()
                : now();

            $endsAt = $base->addDays($days);

            if ($endsAt->isPast() && $days > 0) {
                throw new RuntimeException('That extension still leaves the subscription in the past.');
            }

            $subscription->forceFill([
                'ends_at' => $endsAt,
                'grace_ends_at' => $endsAt->copy()->addDays((int) config('thinkrax.grace_days', 3)),
                'status' => $endsAt->isFuture() && $subscription->status !== SubscriptionStatus::Cancelled
                    ? ($subscription->is_trial ? SubscriptionStatus::Trial : SubscriptionStatus::Active)
                    : $subscription->status,
                'admin_notes' => $this->appendNote($subscription->admin_notes, $days.' day(s): '.$reason),
            ])->save();

            $this->syncTenantStatus($subscription);

            $this->activity->log(
                'subscription.extended',
                sprintf('%s %d day(s) on %s — %s',
                    $days > 0 ? 'Added' : 'Removed',
                    abs($days),
                    $subscription->tenant?->business_name ?? 'a tenant',
                    $reason,
                ),
                $subscription,
                ['days' => $days, 'ends_at' => $endsAt->toDateTimeString(), 'reason' => $reason],
                (int) $subscription->tenant_id,
            );

            return $subscription->refresh();
        });
    }

    /**
     * Move a tenant onto a different plan for the remainder of the term.
     *
     * The entitlement snapshot is rewritten from the new plan, which is the
     * whole point: the tenant's access changes immediately, but nothing about
     * their dates or invoices does.
     */
    public function changePlan(Subscription $subscription, Plan $plan, string $reason): Subscription
    {
        $this->requireReason($reason);

        if ((int) $plan->saas_product_id !== (int) $subscription->saas_product_id) {
            throw new RuntimeException('That plan belongs to a different product.');
        }

        return DB::transaction(function () use ($subscription, $plan, $reason) {
            $previous = $subscription->plan?->name ?? 'none';

            $subscription->forceFill([
                'plan_id' => $plan->id,
                'features_snapshot' => $plan->features ?? [],
                'limits_snapshot' => $plan->limits ?? [],
                'admin_notes' => $this->appendNote($subscription->admin_notes, "Plan {$previous} → {$plan->name}: {$reason}"),
            ])->save();

            $this->activity->log(
                'subscription.plan_changed',
                sprintf('Moved %s from %s to %s — %s',
                    $subscription->tenant?->business_name ?? 'a tenant',
                    $previous,
                    $plan->name,
                    $reason,
                ),
                $subscription,
                ['from' => $previous, 'to' => $plan->name, 'reason' => $reason],
                (int) $subscription->tenant_id,
            );

            return $subscription->refresh();
        });
    }

    /**
     * Record a subscription that was paid for outside the app — cash to a
     * salesman, a cheque, a reseller's bulk order.
     *
     * A paid invoice is written alongside it so the revenue reports stay
     * complete and the tenant can still download something.
     */
    public function grantTerm(
        Tenant $tenant,
        Plan $plan,
        BillingCycle $cycle,
        string $reason,
        ?float $amount = null,
    ): Subscription {
        $this->requireReason($reason);

        return DB::transaction(function () use ($tenant, $plan, $cycle, $reason, $amount) {
            $current = $tenant->currentSubscription();

            // An early manual renewal queues behind the term already paid for.
            $startsAt = $current && $current->ends_at && $current->ends_at->isFuture()
                ? $current->ends_at->copy()
                : now();

            $endsAt = $startsAt->copy()->addMonths($cycle->months());
            $price = $amount !== null ? round($amount, 2) : $plan->priceFor($cycle);

            $subscription = Subscription::create([
                'tenant_id' => $tenant->id,
                'saas_product_id' => $tenant->saas_product_id,
                'plan_id' => $plan->id,
                'billing_cycle' => $cycle,
                'status' => SubscriptionStatus::Active,
                'amount' => $price,
                'currency' => $plan->currency ?: 'PKR',
                'starts_at' => $startsAt,
                'ends_at' => $endsAt,
                'grace_ends_at' => $endsAt->copy()->addDays((int) config('thinkrax.grace_days', 3)),
                'is_trial' => false,
                'auto_renew' => true,
                'features_snapshot' => $plan->features ?? [],
                'limits_snapshot' => $plan->limits ?? [],
                'previous_subscription_id' => $current?->id,
                'admin_notes' => $this->appendNote(null, 'Granted manually: '.$reason),
            ]);

            SubscriptionInvoice::create([
                'tenant_id' => $tenant->id,
                'subscription_id' => $subscription->id,
                'invoice_number' => $this->nextInvoiceNumber(),
                'status' => $price > 0 ? InvoiceStatus::Paid : InvoiceStatus::Cancelled,
                'subtotal' => $price,
                'discount' => 0,
                'tax' => 0,
                'total' => $price,
                'paid_amount' => $price,
                'currency' => $subscription->currency,
                'issued_on' => now()->toDateString(),
                'due_on' => now()->toDateString(),
                'period_start' => $startsAt->toDateString(),
                'period_end' => $endsAt->toDateString(),
                'paid_at' => now(),
                'notes' => 'Recorded manually by '.(auth()->user()?->name ?? 'system').' — '.$reason,
            ]);

            // Supersede whatever came before, exactly as a verified payment does.
            Subscription::query()
                ->where('tenant_id', $tenant->id)
                ->whereKeyNot($subscription->id)
                ->whereIn('status', [
                    SubscriptionStatus::Trial->value,
                    SubscriptionStatus::Grace->value,
                    SubscriptionStatus::Expired->value,
                    SubscriptionStatus::PendingPayment->value,
                    SubscriptionStatus::PendingVerification->value,
                    SubscriptionStatus::Rejected->value,
                ])
                ->update(['status' => SubscriptionStatus::Cancelled->value, 'cancelled_at' => now()]);

            $tenant->forceFill([
                'status' => TenantStatus::Active,
                'approved_at' => $tenant->approved_at ?? now(),
            ])->save();

            $this->activity->log(
                'subscription.granted',
                sprintf('Granted %s a %s %s subscription — %s',
                    $tenant->business_name,
                    $cycle->value,
                    $plan->name,
                    $reason,
                ),
                $subscription,
                ['plan' => $plan->name, 'cycle' => $cycle->value, 'amount' => $price, 'reason' => $reason],
                (int) $tenant->id,
            );

            return $subscription;
        });
    }

    /** End a subscription early. */
    public function cancel(Subscription $subscription, string $reason, bool $immediately = false): Subscription
    {
        $this->requireReason($reason);

        return DB::transaction(function () use ($subscription, $reason, $immediately) {
            $subscription->forceFill([
                'status' => SubscriptionStatus::Cancelled,
                'cancelled_at' => now(),
                'cancellation_reason' => $reason,
                'auto_renew' => false,
                // Not immediate? The tenant keeps the days they paid for.
                'ends_at' => $immediately ? now() : $subscription->ends_at,
                'grace_ends_at' => $immediately ? now() : $subscription->grace_ends_at,
            ])->save();

            $this->syncTenantStatus($subscription);

            $this->activity->log(
                'subscription.cancelled',
                sprintf('Cancelled %s%s — %s',
                    $subscription->tenant?->business_name ?? 'a subscription',
                    $immediately ? ' immediately' : ' at term end',
                    $reason,
                ),
                $subscription,
                ['immediate' => $immediately, 'reason' => $reason],
                (int) $subscription->tenant_id,
            );

            return $subscription->refresh();
        });
    }

    /*
    |--------------------------------------------------------------------------
    | Internals
    |--------------------------------------------------------------------------
    */

    /** Keep the tenant's headline status honest after a date change. */
    protected function syncTenantStatus(Subscription $subscription): void
    {
        $tenant = $subscription->tenant;

        if (! $tenant || $tenant->status === TenantStatus::Suspended) {
            return;
        }

        $tenant->forceFill([
            'status' => match ($subscription->refresh()->effectiveStatus()) {
                SubscriptionStatus::Trial => TenantStatus::Trial,
                SubscriptionStatus::Active => TenantStatus::Active,
                SubscriptionStatus::Grace, SubscriptionStatus::Expired => TenantStatus::Expired,
                SubscriptionStatus::Cancelled, SubscriptionStatus::Rejected => TenantStatus::Expired,
                default => $tenant->status,
            },
        ])->save();
    }

    protected function nextInvoiceNumber(): string
    {
        $prefix = $this->settings->string('invoice_prefix', 'TX-').now()->year.'-';

        $last = SubscriptionInvoice::query()
            ->where('invoice_number', 'like', $prefix.'%')
            ->orderByDesc('id')
            ->value('invoice_number');

        $next = $last ? ((int) substr($last, strlen($prefix))) + 1 : 1;

        return $prefix.str_pad((string) $next, 6, '0', STR_PAD_LEFT);
    }

    protected function requireReason(string $reason): void
    {
        if (trim($reason) === '') {
            throw new RuntimeException('Please write a reason — every manual override is auditable.');
        }
    }

    protected function appendNote(?string $existing, ?string $note): ?string
    {
        if ($note === null || trim($note) === '') {
            return $existing;
        }

        $stamp = now()->format('d-m-Y H:i').' · '.(auth()->user()?->name ?? 'system');

        return trim(($existing ? $existing."\n\n" : '')."[{$stamp}] ".trim($note));
    }
}
