<?php

namespace App\Services;

use App\Models\Budget;
use App\Models\Debt;
use App\Models\Goal;
use App\Models\ResourceShare;
use App\Models\SubscriptionItem;
use App\Models\Transaction;
use App\Models\User;
use App\Support\BudgetSpentSync;
use App\Support\DebtDueDate;
use App\Support\FinanceListOrdering;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;

class FinanceDemoRepository
{
    private const CACHE_TTL_SECONDS = 12;

    private const CACHE_KEYS = [
        'transactions',
        'budgets',
        'goals',
        'debts',
        'subscriptions',
        'users',
    ];

    private function cacheKey(string $segment, int $userId): string
    {
        return "finance_repo:v1:user:{$userId}:{$segment}";
    }

    /**
     * @template T
     *
     * @param  callable():T  $resolver
     * @return T
     */
    private function rememberForViewer(string $segment, callable $resolver)
    {
        $viewer = $this->viewer();
        if ($viewer === null) {
            return $resolver();
        }

        try {
            return Cache::remember(
                $this->cacheKey($segment, (int) $viewer->id),
                now()->addSeconds(self::CACHE_TTL_SECONDS),
                $resolver
            );
        } catch (\Throwable) {
            return $resolver();
        }
    }

    private function forgetViewerCaches(?int $userId = null): void
    {
        $id = $userId ?? (int) ($this->viewer()?->id ?? 0);
        if ($id <= 0) {
            return;
        }

        foreach (self::CACHE_KEYS as $segment) {
            try {
                Cache::forget($this->cacheKey($segment, $id));
            } catch (\Throwable) {
                // ignore cache backend issues
            }
        }
    }

    /** @return list<array{id:string,name:string}> */
    public function quickSearchTransactions(int $limit = 5): array
    {
        return Transaction::query()
            ->ownedOrSharedWith($this->viewer())
            ->orderByDesc('date')
            ->orderByDesc('created_at')
            ->orderByDesc('id')
            ->limit(max(1, $limit))
            ->get(['id', 'external_id', 'name'])
            ->map(fn (Transaction $row): array => [
                'id' => $row->external_id ?: (string) $row->id,
                'name' => (string) $row->name,
            ])->all();
    }

    public function transactions(): array
    {
        return $this->rememberForViewer('transactions', function (): array {
            $rows = Transaction::query()
                ->ownedOrSharedWith($this->viewer())
                ->orderByDesc('date')
                ->orderByDesc('created_at')
                ->orderByDesc('id')
                ->get()
                ->map(fn (Transaction $row): array => [
                    'id' => $row->external_id ?: (string) $row->id,
                    'name' => $row->name,
                    'categoryId' => $row->category_id,
                    'amount' => $row->amount,
                    'date' => $row->date?->toDateString(),
                    'type' => $row->type,
                    'isRecurring' => $row->is_recurring,
                    '_createdTs' => $row->created_at?->getTimestamp() ?? 0,
                    '_internalId' => $row->id,
                ])->all();

            return FinanceListOrdering::transactionsNewestFirst($rows);
        });
    }

    public function setTransactions(array $rows): void
    {
        $viewer = $this->viewer();
        if ($viewer === null) {
            return;
        }

        $rows = FinanceListOrdering::transactionsNewestFirst($rows);

        $current = Transaction::query()->where('user_id', $viewer->id)->pluck('id', 'external_id');
        $persistedIds = [];

        foreach ($rows as $row) {
            $externalId = (string) ($row['id'] ?? Str::uuid()->toString());
            $model = Transaction::query()->find($current[$externalId] ?? null);
            if ($model !== null && $model->user_id !== $viewer->id) {
                continue;
            }

            $saved = Transaction::query()->updateOrCreate(
                ['user_id' => $viewer->id, 'external_id' => $externalId],
                [
                    'name' => (string) ($row['name'] ?? 'Untitled'),
                    'category_id' => (string) ($row['categoryId'] ?? 'other'),
                    'amount' => (float) ($row['amount'] ?? 0),
                    'date' => (string) ($row['date'] ?? now()->toDateString()),
                    'type' => (string) ($row['type'] ?? 'expense'),
                    'is_recurring' => (bool) ($row['isRecurring'] ?? false),
                ]
            );

            $persistedIds[] = $saved->id;
        }

        Transaction::query()
            ->where('user_id', $viewer->id)
            ->whereNotIn('id', $persistedIds)
            ->delete();

        BudgetSpentSync::syncSpentForUser((int) $viewer->id);
        $this->forgetViewerCaches((int) $viewer->id);
    }

    public function resetTransactions(): void
    {
        $viewer = $this->viewer();
        if ($viewer !== null) {
            Transaction::query()->where('user_id', $viewer->id)->delete();
            BudgetSpentSync::syncSpentForUser((int) $viewer->id);
            $this->forgetViewerCaches((int) $viewer->id);
        }
    }

    public function deleteTransaction(string $externalId): bool
    {
        return $this->deleteOwnedOrUnshareModel(Transaction::class, $externalId);
    }

    public function budgets(): array
    {
        return $this->rememberForViewer('budgets', function (): array {
            $viewer = $this->viewer();
            if ($viewer !== null) {
                once(fn () => BudgetSpentSync::syncSpentForUser((int) $viewer->id));
            }

            $rows = Budget::query()
                ->ownedOrSharedWith($this->viewer())
                ->orderBy('name')
                ->get()
                ->map(fn (Budget $row): array => [
                    'id' => $row->external_id ?: (string) $row->id,
                    'name' => $row->name,
                    'categoryId' => $row->category_id,
                    'limit' => $row->limit_amount,
                    'spent' => $row->spent_amount,
                    'period' => $row->period,
                    'icon' => $row->icon,
                ])->all();

            return FinanceListOrdering::budgetsByUrgency($rows);
        });
    }

    /** @return list<array{id:string,name:string}> */
    public function quickSearchBudgets(int $limit = 3): array
    {
        return Budget::query()
            ->ownedOrSharedWith($this->viewer())
            ->orderBy('name')
            ->limit(max(1, $limit))
            ->get(['id', 'external_id', 'name'])
            ->map(fn (Budget $row): array => [
                'id' => $row->external_id ?: (string) $row->id,
                'name' => (string) $row->name,
            ])->all();
    }

    public function setBudgets(array $rows): void
    {
        $viewer = $this->viewer();
        if ($viewer === null) {
            return;
        }

        $persistedIds = [];
        foreach ($rows as $row) {
            $externalId = (string) ($row['id'] ?? Str::uuid()->toString());
            $saved = Budget::query()->updateOrCreate(
                ['user_id' => $viewer->id, 'external_id' => $externalId],
                [
                    'name' => (string) ($row['name'] ?? 'Untitled'),
                    'category_id' => (string) ($row['categoryId'] ?? 'other'),
                    'limit_amount' => (float) ($row['limit'] ?? 0),
                    'spent_amount' => (float) ($row['spent'] ?? 0),
                    'period' => (string) ($row['period'] ?? 'monthly'),
                    'icon' => isset($row['icon']) ? (string) $row['icon'] : null,
                ]
            );
            $persistedIds[] = $saved->id;
        }

        Budget::query()->where('user_id', $viewer->id)->whereNotIn('id', $persistedIds)->delete();

        BudgetSpentSync::syncSpentForUser((int) $viewer->id);
        $this->forgetViewerCaches((int) $viewer->id);
    }

    public function resetBudgets(): void
    {
        if ($this->viewer() !== null) {
            Budget::query()->where('user_id', $this->viewer()->id)->delete();
            $this->forgetViewerCaches((int) $this->viewer()->id);
        }
    }

    public function deleteBudget(string $externalId): bool
    {
        return $this->deleteOwnedOrUnshareModel(Budget::class, $externalId);
    }

    public function goals(): array
    {
        return $this->rememberForViewer('goals', fn (): array => Goal::query()
            ->ownedOrSharedWith($this->viewer())
            ->orderByRaw('case when deadline is null then 1 else 0 end')
            ->orderBy('deadline')
            ->orderBy('name')
            ->get()
            ->map(fn (Goal $row): array => [
                'id' => $row->external_id ?: (string) $row->id,
                'name' => $row->name,
                'target' => $row->target_amount,
                'saved' => $row->saved_amount,
                'icon' => $row->icon,
                'deadline' => $row->deadline?->toDateString(),
                'monthlyContribution' => $row->monthly_contribution,
                'priority' => $row->priority,
            ])->all());
    }

    /** @return list<array{id:string,name:string}> */
    public function quickSearchGoals(int $limit = 3): array
    {
        return Goal::query()
            ->ownedOrSharedWith($this->viewer())
            ->orderByRaw('case when deadline is null then 1 else 0 end')
            ->orderBy('deadline')
            ->orderBy('name')
            ->limit(max(1, $limit))
            ->get(['id', 'external_id', 'name'])
            ->map(fn (Goal $row): array => [
                'id' => $row->external_id ?: (string) $row->id,
                'name' => (string) $row->name,
            ])->all();
    }

    public function setGoals(array $rows): void
    {
        $viewer = $this->viewer();
        if ($viewer === null) {
            return;
        }

        $persistedIds = [];
        foreach ($rows as $row) {
            $externalId = (string) ($row['id'] ?? Str::uuid()->toString());
            $saved = Goal::query()->updateOrCreate(
                ['user_id' => $viewer->id, 'external_id' => $externalId],
                [
                    'name' => (string) ($row['name'] ?? 'Untitled'),
                    'target_amount' => (float) ($row['target'] ?? 0),
                    'saved_amount' => (float) ($row['saved'] ?? 0),
                    'icon' => isset($row['icon']) ? (string) $row['icon'] : null,
                    'deadline' => ! empty($row['deadline']) ? (string) $row['deadline'] : null,
                    'monthly_contribution' => (float) ($row['monthlyContribution'] ?? 0),
                    'priority' => (string) ($row['priority'] ?? 'medium'),
                ]
            );
            $persistedIds[] = $saved->id;
        }

        Goal::query()->where('user_id', $viewer->id)->whereNotIn('id', $persistedIds)->delete();
        $this->forgetViewerCaches((int) $viewer->id);
    }

    public function resetGoals(): void
    {
        if ($this->viewer() !== null) {
            Goal::query()->where('user_id', $this->viewer()->id)->delete();
            $this->forgetViewerCaches((int) $this->viewer()->id);
        }
    }

    public function deleteGoal(string $externalId): bool
    {
        return $this->deleteOwnedOrUnshareModel(Goal::class, $externalId);
    }

    public function debts(): array
    {
        return $this->rememberForViewer('debts', function (): array {
            $rows = Debt::query()
                ->ownedOrSharedWith($this->viewer())
                ->orderBy('name')
                ->get()
                ->map(fn (Debt $row): array => [
                    'id' => $row->external_id ?: (string) $row->id,
                    'name' => $row->name,
                    'balance' => $row->balance,
                    'originalBalance' => $row->original_balance,
                    'interestRate' => $row->interest_rate,
                    'minimumPayment' => $row->minimum_payment,
                    'dueDay' => $row->due_day,
                    'icon' => $row->icon,
                    'type' => $row->type,
                ])->all();

            usort($rows, static function (array $a, array $b): int {
                return DebtDueDate::nextOccurrenceTimestamp($a) <=> DebtDueDate::nextOccurrenceTimestamp($b);
            });

            return $rows;
        });
    }

    /** @return list<array{id:string,name:string}> */
    public function quickSearchDebts(int $limit = 3): array
    {
        return Debt::query()
            ->ownedOrSharedWith($this->viewer())
            ->orderBy('name')
            ->limit(max(1, $limit))
            ->get(['id', 'external_id', 'name'])
            ->map(fn (Debt $row): array => [
                'id' => $row->external_id ?: (string) $row->id,
                'name' => (string) $row->name,
            ])->all();
    }

    public function setDebts(array $rows): void
    {
        $viewer = $this->viewer();
        if ($viewer === null) {
            return;
        }

        $persistedIds = [];
        foreach ($rows as $row) {
            $externalId = (string) ($row['id'] ?? Str::uuid()->toString());
            $saved = Debt::query()->updateOrCreate(
                ['user_id' => $viewer->id, 'external_id' => $externalId],
                [
                    'name' => (string) ($row['name'] ?? 'Untitled'),
                    'balance' => (float) ($row['balance'] ?? 0),
                    'original_balance' => (float) ($row['originalBalance'] ?? 0),
                    'interest_rate' => (float) ($row['interestRate'] ?? 0),
                    'minimum_payment' => (float) ($row['minimumPayment'] ?? 0),
                    'due_day' => isset($row['dueDay']) ? (int) $row['dueDay'] : null,
                    'icon' => isset($row['icon']) ? (string) $row['icon'] : null,
                    'type' => (string) ($row['type'] ?? 'loan'),
                ]
            );
            $persistedIds[] = $saved->id;
        }

        Debt::query()->where('user_id', $viewer->id)->whereNotIn('id', $persistedIds)->delete();
        $this->forgetViewerCaches((int) $viewer->id);
    }

    public function resetDebts(): void
    {
        if ($this->viewer() !== null) {
            Debt::query()->where('user_id', $this->viewer()->id)->delete();
            $this->forgetViewerCaches((int) $this->viewer()->id);
        }
    }

    public function deleteDebt(string $externalId): bool
    {
        return $this->deleteOwnedOrUnshareModel(Debt::class, $externalId);
    }

    public function subscriptions(): array
    {
        return $this->rememberForViewer('subscriptions', function (): array {
            $viewer = $this->viewer();
            if ($viewer === null) {
                return [];
            }

            $rows = SubscriptionItem::query()
                ->ownedOrSharedWith($viewer)
                ->with(['shares.sharedWith:id,name'])
                ->orderBy('name')
                ->get()
                ->map(function (SubscriptionItem $row) use ($viewer): array {
                    $sharedRows = $row->shares
                        ->filter(fn (ResourceShare $share) => $share->owner_user_id === $row->user_id)
                        ->values();

                    return [
                        'id' => $row->external_id ?: (string) $row->id,
                        'name' => $row->name,
                        'categoryId' => $row->category_id,
                        'cost' => $row->cost,
                        'billingCycle' => $row->billing_cycle,
                        'nextRenewal' => $row->next_renewal?->toDateString(),
                        'lastCharge' => $row->last_charge?->toDateString(),
                        'status' => $row->status,
                        'confidence' => $row->confidence,
                        'shared' => $sharedRows->isNotEmpty(),
                        'sharedWith' => $sharedRows->pluck('sharedWith.name')->filter()->values()->all(),
                        'usageScore' => $row->usage_score,
                        'notes' => $row->notes,
                        'isOwner' => $row->user_id === $viewer->id,
                    ];
                })->all();

            return FinanceListOrdering::subscriptionsByNextRenewal($rows);
        });
    }

    /** @return list<array{id:string,name:string}> */
    public function quickSearchSubscriptions(int $limit = 3): array
    {
        $viewer = $this->viewer();
        if ($viewer === null) {
            return [];
        }

        return SubscriptionItem::query()
            ->ownedOrSharedWith($viewer)
            ->orderBy('name')
            ->limit(max(1, $limit))
            ->get(['id', 'external_id', 'name'])
            ->map(fn (SubscriptionItem $row): array => [
                'id' => $row->external_id ?: (string) $row->id,
                'name' => (string) $row->name,
            ])->all();
    }

    public function setSubscriptions(array $rows): void
    {
        $viewer = $this->viewer();
        if ($viewer === null) {
            return;
        }

        $persistedIds = [];
        foreach ($rows as $row) {
            $externalId = (string) ($row['id'] ?? Str::uuid()->toString());
            $saved = SubscriptionItem::query()->updateOrCreate(
                ['user_id' => $viewer->id, 'external_id' => $externalId],
                [
                    'name' => (string) ($row['name'] ?? 'Untitled'),
                    'category_id' => (string) ($row['categoryId'] ?? 'subscriptions'),
                    'cost' => (float) ($row['cost'] ?? 0),
                    'billing_cycle' => (string) ($row['billingCycle'] ?? 'monthly'),
                    'next_renewal' => ! empty($row['nextRenewal']) ? (string) $row['nextRenewal'] : null,
                    'last_charge' => ! empty($row['lastCharge']) ? (string) $row['lastCharge'] : null,
                    'status' => (string) ($row['status'] ?? 'active'),
                    'confidence' => isset($row['confidence']) ? (int) $row['confidence'] : null,
                    'usage_score' => isset($row['usageScore']) ? (int) $row['usageScore'] : null,
                    'notes' => isset($row['notes']) ? (string) $row['notes'] : null,
                ]
            );

            $persistedIds[] = $saved->id;
            $this->syncSubscriptionShares($saved, $row);
        }

        $toDelete = SubscriptionItem::query()
            ->where('user_id', $viewer->id)
            ->whereNotIn('id', $persistedIds)
            ->get();

        foreach ($toDelete as $item) {
            $item->shares()->delete();
            $item->delete();
        }

        BudgetSpentSync::syncSpentForUser((int) $viewer->id);
        $this->forgetViewerCaches((int) $viewer->id);
    }

    public function resetSubscriptions(): void
    {
        $viewer = $this->viewer();
        if ($viewer !== null) {
            $subs = SubscriptionItem::query()->where('user_id', $viewer->id)->get();
            foreach ($subs as $sub) {
                $sub->shares()->delete();
                $sub->delete();
            }
            BudgetSpentSync::syncSpentForUser((int) $viewer->id);
            $this->forgetViewerCaches((int) $viewer->id);
        }
    }

    public function deleteSubscription(string $externalId): bool
    {
        return $this->deleteOwnedOrUnshareModel(SubscriptionItem::class, $externalId);
    }

    public function users(): array
    {
        return $this->rememberForViewer('users', function (): array {
            $viewer = $this->viewer();
            if ($viewer === null) {
                return [];
            }

            if (! $viewer->isSuperAdmin()) {
                return [[
                    'id' => 'u-'.$viewer->id,
                    'name' => $viewer->name,
                    'email' => $viewer->email,
                    'role' => $viewer->role,
                    'status' => $viewer->suspended_at === null ? 'active' : 'suspended',
                    'joined' => $viewer->created_at?->toDateString(),
                    'plan' => 'free',
                    'lastActive' => now()->toDateString(),
                ]];
            }

            return User::query()
                ->orderBy('name')
                ->get()
                ->map(fn (User $user): array => [
                    'id' => 'u-'.$user->id,
                    'name' => $user->name,
                    'email' => $user->email,
                    'role' => $user->role,
                    'status' => $user->suspended_at === null ? 'active' : 'suspended',
                    'joined' => $user->created_at?->toDateString(),
                    'plan' => $user->activeSaasSubscription()->exists() ? 'premium' : 'free',
                    'lastActive' => now()->toDateString(),
                ])
                ->all();
        });
    }

    private function viewer(): ?User
    {
        return Auth::user();
    }

    /**
     * Delete a row owned by the viewer, or remove the viewer's share of someone else's row.
     *
     * @param  class-string<Budget|Debt|Goal|SubscriptionItem|Transaction>  $modelClass
     */
    private function deleteOwnedOrUnshareModel(string $modelClass, string $externalId): bool
    {
        $viewer = $this->viewer();
        if ($viewer === null) {
            return false;
        }

        $numericId = ctype_digit($externalId) ? (int) $externalId : null;

        $row = $modelClass::query()
            ->ownedOrSharedWith($viewer)
            ->where(function ($query) use ($externalId, $numericId): void {
                $query->where('external_id', $externalId);
                if ($numericId !== null) {
                    $query->orWhere('id', $numericId);
                }
            })
            ->first();

        if ($row === null) {
            return false;
        }

        if ((int) $row->user_id === (int) $viewer->id) {
            $row->shares()->delete();
            $row->delete();

            if ($modelClass === Transaction::class || $modelClass === SubscriptionItem::class) {
                BudgetSpentSync::syncSpentForUser((int) $viewer->id);
            }
            $this->forgetViewerCaches((int) $viewer->id);

            return true;
        }

        $deleted = ResourceShare::query()
            ->where('shared_with_user_id', $viewer->id)
            ->where('shareable_type', $modelClass)
            ->where('shareable_id', $row->id)
            ->delete() > 0;

        if ($deleted) {
            $this->forgetViewerCaches((int) $viewer->id);
        }

        return $deleted;
    }

    /** @param array<string,mixed> $row */
    private function syncSubscriptionShares(SubscriptionItem $subscription, array $row): void
    {
        $sharedNames = array_values(array_filter((array) ($row['sharedWith'] ?? [])));
        if (! ((bool) ($row['shared'] ?? false)) || count($sharedNames) === 0) {
            $subscription->shares()->delete();

            return;
        }

        $users = User::query()
            ->where('id', '!=', $subscription->user_id)
            ->whereIn('name', $sharedNames)
            ->pluck('id');

        $subscription->shares()->whereNotIn('shared_with_user_id', $users)->delete();
        foreach ($users as $sharedUserId) {
            ResourceShare::query()->updateOrCreate(
                [
                    'owner_user_id' => $subscription->user_id,
                    'shared_with_user_id' => $sharedUserId,
                    'shareable_type' => $subscription::class,
                    'shareable_id' => $subscription->id,
                ],
                ['permission' => 'view']
            );
        }
    }
}
