<?php

namespace App\Services\Finance;

use App\Enums\CashTransactionType;
use App\Models\CashAccount;
use App\Models\CashTransaction;
use App\Models\User;
use App\Services\Media\MediaService;
use App\Services\System\PushNotificationService;
use App\Support\Media\MediaPresenter;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\ValidationException;

class CashService
{
    private const MAX_PHOTOS = 1;

    public function __construct(
        private readonly MediaService $mediaService,
        private readonly PushNotificationService $pushNotificationService,
    ) {}

    public function getDefaultAccount(): CashAccount
    {
        return CashAccount::query()->firstOrFail();
    }

    public function paginateForIndex(CashAccount $cashAccount, array $tableQuery, string $referenceType = ''): LengthAwarePaginator
    {
        $query = CashTransaction::query()
            ->with(['createdBy.profile', 'reference', 'media'])
            ->where('cash_account_id', $cashAccount->id)
            ->when($tableQuery['search'] !== '', function (Builder $query) use ($tableQuery): void {
                $search = $tableQuery['search'];
                $query->where(function (Builder $query) use ($search): void {
                    $query->where('description', 'like', "%{$search}%");
                });
            })
            ->when($referenceType !== '', function (Builder $query) use ($referenceType): void {
                if ($referenceType === 'manual') {
                    $query->whereNull('reference_type');
                } else {
                    $query->where('reference_type', $referenceType);
                }
            });

        $this->applySorting($query, $tableQuery['sort'], $tableQuery['direction']);

        return $query
            ->paginate(25)
            ->withQueryString()
            ->through(function (CashTransaction $transaction) {
                $transaction->setAttribute(
                    'photos',
                    MediaPresenter::collection($transaction, 'photos'),
                );

                return $transaction;
            });
    }

    public function deposit(CashAccount $cashAccount, array $validated, User $user): CashTransaction
    {
        try {
            $transaction = DB::transaction(function () use ($cashAccount, $validated, $user): CashTransaction {
                $account = CashAccount::query()->lockForUpdate()->findOrFail($cashAccount->id);
                $amount = (int) $validated['amount'];
                $newBalance = $account->balance + $amount;

                $account->update(['balance' => $newBalance]);

                $transaction = CashTransaction::create([
                    'cash_account_id' => $account->id,
                    'type' => CashTransactionType::DEPOSIT,
                    'amount' => $amount,
                    'balance_after' => $newBalance,
                    'description' => $validated['description'],
                    'created_by_id' => $user->id,
                ]);

                $this->syncPhotos($transaction, $validated);

                return $transaction;
            });
        } catch (ValidationException $e) {
            throw $e;
        } catch (\Throwable $e) {
            Log::error('Gagal melakukan setoran kas: '.$e->getMessage(), [
                'trace' => $e->getTraceAsString(),
            ]);

            throw ValidationException::withMessages([
                'system' => 'Terjadi kesalahan pada server. Silakan laporkan masalah ini ke pihak terkait.',
            ]);
        }

        $this->pushNotificationService->sendToRoles(
            '💰 Setoran Kas',
            "Setoran kas baru {$transaction->amount_formatted} dengan keterangan: {$transaction->description} oleh {$user->profile?->full_name}.",
            ['owner', 'developer', 'admin-toko'],
            route('admin.finance.cash.index'),
        );

        return $transaction;
    }

    public function withdraw(CashAccount $cashAccount, array $validated, User $user): CashTransaction
    {
        try {
            $transaction = DB::transaction(function () use ($cashAccount, $validated, $user): CashTransaction {
                $account = CashAccount::query()->lockForUpdate()->findOrFail($cashAccount->id);
                $amount = (int) $validated['amount'];

                if ($account->balance < $amount) {
                    throw ValidationException::withMessages([
                        'amount' => 'Saldo kas tidak mencukupi.',
                    ]);
                }

                $newBalance = $account->balance - $amount;

                $account->update(['balance' => $newBalance]);

                $transaction = CashTransaction::create([
                    'cash_account_id' => $account->id,
                    'type' => CashTransactionType::WITHDRAWAL,
                    'amount' => $amount,
                    'balance_after' => $newBalance,
                    'description' => $validated['description'],
                    'created_by_id' => $user->id,
                ]);

                $this->syncPhotos($transaction, $validated);

                return $transaction;
            });
        } catch (ValidationException $e) {
            throw $e;
        } catch (\Throwable $e) {
            Log::error('Gagal melakukan tarik kas: '.$e->getMessage(), [
                'trace' => $e->getTraceAsString(),
            ]);

            throw ValidationException::withMessages([
                'system' => 'Terjadi kesalahan pada server. Silakan laporkan masalah ini ke pihak terkait.',
            ]);
        }

        $this->pushNotificationService->sendToRoles(
            '🏦 Tarik Kas',
            "Tarik kas {$transaction->amount_formatted} dengan keterangan: {$transaction->description} oleh {$user->profile?->full_name}.",
            ['owner', 'developer', 'admin-toko'],
            route('admin.finance.cash.index'),
        );

        return $transaction;
    }

    public function recordOutgoing(
        Model $reference,
        int $amount,
        string $description,
        User $user,
        ?CashAccount $cashAccount = null,
    ): CashTransaction {
        try {
            return DB::transaction(function () use ($reference, $amount, $description, $user, $cashAccount): CashTransaction {
                $account = CashAccount::query()->lockForUpdate()->findOrFail(
                    ($cashAccount ?? $this->getDefaultAccount())->id,
                );

                if ($account->balance < $amount) {
                    throw ValidationException::withMessages([
                        'amount' => 'Saldo kas tidak mencukupi.',
                    ]);
                }

                $newBalance = $account->balance - $amount;

                $account->update(['balance' => $newBalance]);

                $transaction = new CashTransaction([
                    'cash_account_id' => $account->id,
                    'type' => CashTransactionType::WITHDRAWAL,
                    'amount' => $amount,
                    'balance_after' => $newBalance,
                    'description' => $description,
                    'created_by_id' => $user->id,
                ]);

                $transaction->reference()->associate($reference);
                $transaction->save();

                return $transaction;
            });
        } catch (ValidationException $e) {
            throw $e;
        } catch (\Throwable $e) {
            Log::error('Gagal mencatat transaksi keluar kas: '.$e->getMessage(), [
                'trace' => $e->getTraceAsString(),
            ]);

            throw ValidationException::withMessages([
                'system' => 'Terjadi kesalahan pada server. Silakan laporkan masalah ini ke pihak terkait.',
            ]);
        }
    }

    public function recordIncoming(
        Model $reference,
        int $amount,
        string $description,
        User $user,
        ?CashAccount $cashAccount = null,
    ): CashTransaction {
        try {
            return DB::transaction(function () use ($reference, $amount, $description, $user, $cashAccount): CashTransaction {
                $account = CashAccount::query()->lockForUpdate()->findOrFail(
                    ($cashAccount ?? $this->getDefaultAccount())->id,
                );

                $newBalance = $account->balance + $amount;

                $account->update(['balance' => $newBalance]);

                $transaction = new CashTransaction([
                    'cash_account_id' => $account->id,
                    'type' => CashTransactionType::DEPOSIT,
                    'amount' => $amount,
                    'balance_after' => $newBalance,
                    'description' => $description,
                    'created_by_id' => $user->id,
                ]);

                $transaction->reference()->associate($reference);
                $transaction->save();

                return $transaction;
            });
        } catch (ValidationException $e) {
            throw $e;
        } catch (\Throwable $e) {
            Log::error('Gagal mencatat transaksi masuk kas: '.$e->getMessage(), [
                'trace' => $e->getTraceAsString(),
            ]);

            throw ValidationException::withMessages([
                'system' => 'Terjadi kesalahan pada server. Silakan laporkan masalah ini ke pihak terkait.',
            ]);
        }
    }

    public function updateDeposit(CashTransaction $transaction, array $validated): void
    {
        $this->ensureEditable($transaction);

        try {
            DB::transaction(function () use ($transaction, $validated): void {
                CashAccount::query()->lockForUpdate()->findOrFail($transaction->cash_account_id);

                $transaction->update([
                    'amount' => (int) $validated['amount'],
                    'description' => $validated['description'],
                ]);

                $this->syncPhotos($transaction, $validated);

                $this->recalculateBalances($transaction->cashAccount);

                $account = $transaction->cashAccount->fresh();

                if ($account->balance < 0) {
                    throw ValidationException::withMessages([
                        'amount' => 'Saldo kas tidak mencukupi.',
                    ]);
                }
            });
        } catch (ValidationException $e) {
            throw $e;
        } catch (\Throwable $e) {
            Log::error('Gagal memperbarui setoran kas: '.$e->getMessage(), [
                'trace' => $e->getTraceAsString(),
            ]);

            throw ValidationException::withMessages([
                'system' => 'Terjadi kesalahan pada server. Silakan laporkan masalah ini ke pihak terkait.',
            ]);
        }

        $this->pushNotificationService->sendToRoles(
            '✏️ Transaksi Kas Diperbarui',
            "Transaksi kas dengan keterangan {$transaction->description} diperbarui menjadi senilai {$transaction->amount_formatted}.",
            ['owner', 'developer', 'admin-toko'],
            route('admin.finance.cash.index'),
        );
    }

    public function deleteTransaction(CashTransaction $transaction): void
    {
        $this->ensureEditable($transaction);

        try {
            DB::transaction(function () use ($transaction): void {
                CashAccount::query()->lockForUpdate()->findOrFail($transaction->cash_account_id);

                $account = $transaction->cashAccount;

                $transaction->clearMediaCollection('photos');
                $transaction->delete();

                $this->recalculateBalances($account);

                if ($account->fresh()->balance < 0) {
                    throw ValidationException::withMessages([
                        'transaction' => 'Saldo kas tidak mencukupi jika transaksi ini dihapus.',
                    ]);
                }
            });
        } catch (ValidationException $e) {
            throw $e;
        } catch (\Throwable $e) {
            Log::error('Gagal menghapus transaksi kas: '.$e->getMessage(), [
                'trace' => $e->getTraceAsString(),
            ]);

            throw ValidationException::withMessages([
                'system' => 'Terjadi kesalahan pada server. Silakan laporkan masalah ini ke pihak terkait.',
            ]);
        }

        $this->pushNotificationService->sendToRoles(
            '🗑️ Transaksi Kas Dihapus',
            "Transaksi kas senilai {$transaction->amount_formatted} dengan keterangan {$transaction->description} telah dihapus.",
            ['owner', 'developer', 'admin-toko'],
            route('admin.finance.cash.index'),
        );
    }

    public function updateReferencedTransaction(
        CashTransaction $transaction,
        int $amount,
        string $description,
    ): void {
        try {
            DB::transaction(function () use ($transaction, $amount, $description): void {
                CashAccount::query()->lockForUpdate()->findOrFail($transaction->cash_account_id);

                $transaction->update([
                    'amount' => $amount,
                    'description' => $description,
                ]);

                $this->recalculateBalances($transaction->cashAccount);

                $account = $transaction->cashAccount->fresh();

                if ($account->balance < 0) {
                    throw ValidationException::withMessages([
                        'amount' => 'Saldo kas tidak mencukupi.',
                    ]);
                }
            });
        } catch (ValidationException $e) {
            throw $e;
        } catch (\Throwable $e) {
            Log::error('Gagal memperbarui transaksi kas referensi: '.$e->getMessage(), [
                'trace' => $e->getTraceAsString(),
            ]);

            throw ValidationException::withMessages([
                'system' => 'Terjadi kesalahan pada server. Silakan laporkan masalah ini ke pihak terkait.',
            ]);
        }
    }

    public function deleteReferencedTransaction(CashTransaction $transaction): void
    {
        try {
            DB::transaction(function () use ($transaction): void {
                CashAccount::query()->lockForUpdate()->findOrFail($transaction->cash_account_id);
                $account = $transaction->cashAccount;

                $transaction->delete();

                $this->recalculateBalances($account);

                if ($account->fresh()->balance < 0) {
                    throw ValidationException::withMessages([
                        'transaction' => 'Saldo kas tidak mencukupi jika transaksi ini dihapus.',
                    ]);
                }
            });
        } catch (ValidationException $e) {
            throw $e;
        } catch (\Throwable $e) {
            Log::error('Gagal menghapus transaksi kas referensi: '.$e->getMessage(), [
                'trace' => $e->getTraceAsString(),
            ]);

            throw ValidationException::withMessages([
                'system' => 'Terjadi kesalahan pada server. Silakan laporkan masalah ini ke pihak terkait.',
            ]);
        }
    }

    private function syncPhotos(CashTransaction $transaction, array $validated): void
    {
        $this->mediaService->syncCollection(
            $transaction,
            'photos',
            $validated['photos'] ?? null,
            $validated['remove_media_ids'] ?? null,
            self::MAX_PHOTOS,
            required: true,
            errorKey: 'photos',
        );
    }

    private function ensureEditable(CashTransaction $transaction): void
    {
        if ($transaction->reference_type !== null) {
            throw ValidationException::withMessages([
                'transaction' => 'Transaksi ini tidak dapat diubah dari halaman kas.',
            ]);
        }
    }

    private function recalculateBalances(CashAccount $cashAccount): void
    {
        $account = CashAccount::query()->lockForUpdate()->findOrFail($cashAccount->id);
        $runningBalance = 0;

        $transactions = CashTransaction::query()
            ->with('reference')
            ->where('cash_account_id', $account->id)
            ->orderBy('created_at')
            ->orderBy('id')
            ->get();

        foreach ($transactions as $transaction) {
            if ($transaction->type === CashTransactionType::DEPOSIT) {
                $runningBalance += $transaction->amount;
            } else {
                $runningBalance -= $transaction->amount;
            }

            $transaction->updateQuietly(['balance_after' => $runningBalance]);
        }

        $account->update(['balance' => $runningBalance]);
    }

    private function applySorting(Builder $query, string $sort, string $direction): void
    {
        if (in_array($sort, ['created_at', 'amount', 'reference_type'], true)) {
            $query->orderBy($sort, $direction);

            return;
        }

        $query->latest();
    }
}
