<?php

namespace App\Services\Meet;

use App\Models\Organization;
use App\Models\Recording;
use App\Models\Room;
use App\Models\Session;
use App\Models\SessionFile;
use App\Models\UsageRecord;
use App\Services\Billing\BillingClient;
use Carbon\CarbonInterface;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Log;
use RuntimeException;

/** Pay-as-you-go Meet billing: per-participant sessions + monthly storage per GB. */
class MeetBillingService
{
    private const BYTES_PER_GB = 1073741824;

    public function __construct(
        protected BillingClient $billing,
    ) {}

    public function pricePerParticipantGhs(): float
    {
        return (float) config('meet.billing.price_per_participant_ghs', 0.30);
    }

    public function pricePerGbMonthGhs(): float
    {
        return (float) config('meet.billing.price_per_gb_month_ghs', 0.30);
    }

    public function gracePeriodDays(): int
    {
        return max(1, (int) config('meet.billing.grace_period_days', 15));
    }

    public function storageGb(int $bytes): float
    {
        if ($bytes <= 0) {
            return 0;
        }

        return round($bytes / self::BYTES_PER_GB, 4);
    }

    public function participantsBillableFor(Session $session): int
    {
        return max(1, (int) ($session->peak_participants ?? 0));
    }

    public function amountMinorForParticipants(int $count): int
    {
        $ghs = round($count * $this->pricePerParticipantGhs(), 2);

        return max((int) round($ghs * 100), 1);
    }

    public function amountMinorForStorageBytes(int $bytes): int
    {
        if ($bytes <= 0) {
            return 0;
        }

        $ghs = round($this->storageGb($bytes) * $this->pricePerGbMonthGhs(), 2);

        return max((int) round($ghs * 100), 1);
    }

    public function assertCanStartSession(Organization $organization, ?Room $room = null): void
    {
        $amountMinor = $this->amountMinorForParticipants(1);
        $message = sprintf(
            'Insufficient wallet balance. Sessions are billed at GHS %.2f per participant — top up your Ladill wallet to start.',
            $this->pricePerParticipantGhs(),
        );

        $affordable = $this->canAffordSafely($organization->owner_ref, $amountMinor);

        if ($affordable === null) {
            abort(503, 'Billing is temporarily unavailable. Please try again shortly.');
        }

        abort_unless($affordable, 402, $message);
    }

    public function chargeSession(Session $session): void
    {
        $this->chargeParticipants($session);
    }

    public function chargeParticipants(Session $session): void
    {
        $session->loadMissing('room.organization');
        $organization = $session->room->organization;
        $count = $this->participantsBillableFor($session);
        $amountMinor = $this->amountMinorForParticipants($count);

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

        $this->recordUsage($organization, 'participant_count', $count, [
            'session_uuid' => $session->uuid,
            'room_uuid' => $session->room->uuid,
            'participants' => $count,
        ], $session->room->branch_id);

        $this->debit(
            $organization->owner_ref,
            $amountMinor,
            'meet_participants',
            'meet-participants-'.$session->uuid,
            sprintf('Meet session: %s (%d participants)', $session->room->title, $count),
        );
    }

    public function chargeRecordingStorageInitial(Recording $recording): void
    {
        $recording->loadMissing('session.room.organization');
        $bytes = (int) ($recording->file_size ?? 0);

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

        if (! $this->attemptChargeStorageInitial(
            $recording,
            $bytes,
            $recording->session->room->organization,
            'recording',
            $recording->uuid,
            sprintf('Meet recording storage: %s (%s GB)', $recording->session->room->title, $this->formatGb($bytes)),
        )) {
            Log::warning('Meet recording storage billing failed', ['recording_uuid' => $recording->uuid]);
        }
    }

    public function chargeFileStorageInitial(SessionFile $file): void
    {
        $file->loadMissing('room.organization');
        $bytes = (int) ($file->file_size ?? 0);

        if ($bytes <= 0 || $file->drive_file_id) {
            return;
        }

        abort_unless(
            $this->attemptChargeStorageInitial(
                $file,
                $bytes,
                $file->room->organization,
                'shared_file',
                $file->uuid,
                sprintf('Meet file storage: %s (%s GB)', $file->original_name, $this->formatGb($bytes)),
            ),
            402,
            sprintf(
                'Insufficient wallet balance. Storage is billed at GHS %.2f per GB per month — top up your Ladill wallet.',
                $this->pricePerGbMonthGhs(),
            ),
        );
    }

    public function chargeStorageRenewal(Recording|SessionFile $asset): bool
    {
        $bytes = (int) ($asset->file_size ?? 0);
        if ($bytes <= 0) {
            return true;
        }

        if ($asset instanceof Recording) {
            $asset->loadMissing('session.room.organization');
            $organization = $asset->session->room->organization;
            $label = $asset->session->room->title;
            $type = 'recording';
            $uuid = $asset->uuid;
        } else {
            $asset->loadMissing('room.organization');
            $organization = $asset->room->organization;
            $label = $asset->original_name;
            $type = 'shared_file';
            $uuid = $asset->uuid;
        }

        $amountMinor = $this->amountMinorForStorageBytes($bytes);
        if ($amountMinor <= 0) {
            return true;
        }

        $base = $asset->storage_paid_until instanceof CarbonInterface
            ? $asset->storage_paid_until->copy()
            : now();
        $periodEnd = $this->nextPeriodEnd($base);
        $reference = sprintf('meet-%s-renewal:%s:through:%s', $type, $uuid, $periodEnd->format('Y-m-d'));

        if (! ($this->canAffordSafely($organization->owner_ref, $amountMinor) ?? false)) {
            return false;
        }

        if (! $this->debit(
            $organization->owner_ref,
            $amountMinor,
            'meet_storage_renewal',
            $reference,
            sprintf('Meet storage renewal: %s', $label),
        )) {
            return false;
        }

        $this->markStoragePaidThrough($asset, $periodEnd);
        $metric = $type === 'recording' ? 'recording_storage_bytes' : 'file_storage_bytes';
        $this->recordUsage($organization, $metric, $bytes, [
            'asset_uuid' => $uuid,
            'renewal' => true,
        ]);

        return true;
    }

    public function enterStorageGracePeriod(Recording|SessionFile $asset): void
    {
        $graceEndsAt = now()->addDays($this->gracePeriodDays());

        $asset->update([
            'storage_grace_ends_at' => $graceEndsAt,
        ]);
    }

    public function processDueStorageRenewals(): array
    {
        $renewed = 0;
        $graced = 0;
        $deleted = 0;

        foreach ([Recording::class, SessionFile::class] as $modelClass) {
            $due = $modelClass::query()
                ->whereNotNull('storage_paid_until')
                ->where('storage_paid_until', '<=', now())
                ->where(function ($query) {
                    $query->whereNull('storage_grace_ends_at')
                        ->orWhere('storage_grace_ends_at', '>', now());
                })
                ->get();

            foreach ($due as $asset) {
                if ($this->chargeStorageRenewal($asset)) {
                    $renewed++;
                    continue;
                }

                if (! $asset->storage_grace_ends_at) {
                    $this->enterStorageGracePeriod($asset);
                    $graced++;
                }
            }

            $expired = $modelClass::query()
                ->whereNotNull('storage_grace_ends_at')
                ->where('storage_grace_ends_at', '<=', now())
                ->get();

            foreach ($expired as $asset) {
                $this->deleteUnpaidAsset($asset);
                $deleted++;
            }
        }

        return compact('renewed', 'graced', 'deleted');
    }

    public function storageIsAccessible(Recording|SessionFile $asset): bool
    {
        if (! $asset->storage_paid_until) {
            return true;
        }

        if ($asset->storage_paid_until->isFuture()) {
            return true;
        }

        return $asset->storage_grace_ends_at instanceof CarbonInterface
            && $asset->storage_grace_ends_at->isFuture();
    }

    protected function attemptChargeStorageInitial(
        Recording|SessionFile $asset,
        int $bytes,
        Organization $organization,
        string $type,
        string $uuid,
        string $description,
    ): bool {
        $amountMinor = $this->amountMinorForStorageBytes($bytes);

        if ($amountMinor <= 0) {
            return true;
        }

        if (! ($this->canAffordSafely($organization->owner_ref, $amountMinor) ?? false)) {
            return false;
        }

        $periodEnd = $this->nextPeriodEnd(now());
        $reference = sprintf('meet-%s-storage:%s:through:%s', $type, $uuid, $periodEnd->format('Y-m-d'));

        if (! $this->debit($organization->owner_ref, $amountMinor, 'meet_storage', $reference, $description)) {
            return false;
        }

        $this->markStoragePaidThrough($asset, $periodEnd);

        $metric = $type === 'recording' ? 'recording_storage_bytes' : 'file_storage_bytes';
        $this->recordUsage($organization, $metric, $bytes, [
            'asset_uuid' => $uuid,
            'type' => $type,
        ]);

        return true;
    }

    protected function markStoragePaidThrough(Recording|SessionFile $asset, CarbonInterface $periodEnd): void
    {
        $asset->update([
            'storage_paid_until' => $periodEnd,
            'storage_grace_ends_at' => null,
            'storage_last_billed_at' => now(),
        ]);
    }

    protected function nextPeriodEnd(CarbonInterface $from): CarbonInterface
    {
        $days = max(1, (int) config('meet.billing.billing_period_days', 30));

        return Carbon::instance($from)->addDays($days);
    }

    protected function recordUsage(
        Organization $organization,
        string $metric,
        int $quantity,
        array $metadata = [],
        ?int $branchId = null,
    ): void {
        UsageRecord::create([
            'owner_ref' => $organization->owner_ref,
            'organization_id' => $organization->id,
            'branch_id' => $branchId,
            'metric' => $metric,
            'quantity' => $quantity,
            'metadata' => $metadata,
            'recorded_at' => now(),
        ]);
    }

    /** @return bool|null null when the billing API is unreachable */
    protected function canAffordSafely(string $ownerRef, int $amountMinor): ?bool
    {
        try {
            return $this->billing->canAfford($ownerRef, $amountMinor);
        } catch (\Throwable $e) {
            Log::warning('Meet billing can-afford check failed', [
                'owner_ref' => $ownerRef,
                'amount_minor' => $amountMinor,
                'error' => $e->getMessage(),
            ]);

            return null;
        }
    }

    protected function debit(
        string $ownerRef,
        int $amountMinor,
        string $source,
        string $reference,
        string $description,
    ): bool {
        try {
            return $this->billing->debit(
                $ownerRef,
                $amountMinor,
                $source,
                $reference,
                $description,
            );
        } catch (\Throwable $e) {
            Log::warning('Meet billing debit failed', [
                'owner_ref' => $ownerRef,
                'source' => $source,
                'reference' => $reference,
                'error' => $e->getMessage(),
            ]);

            return false;
        }
    }

    protected function deleteUnpaidAsset(Recording|SessionFile $asset): void
    {
        if ($asset instanceof Recording) {
            app(RecordingService::class)->delete($asset, $asset->owner_ref);
        } else {
            app(FileSharingService::class)->delete($asset);
        }
    }

    protected function formatGb(int $bytes): string
    {
        return number_format($this->storageGb($bytes), 2);
    }
}
