<?php

namespace App\Http\Controllers\Tenant;

use App\Http\Controllers\Controller;
use App\Models\Customer;
use App\Models\SavingsAccount;
use App\Models\SavingsProduct;
use App\Models\SavingsTransaction;
use App\Services\ActivityLogger;
use App\Services\MemberNotificationAutomationService;
use App\Services\NumberGenerator;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Schema;
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException;
use Illuminate\View\View;

class SavingsAccountController extends Controller
{
    public function index(Request $request): View
    {
        $user = auth()->user();
        $tenant = $user->tenant;

        if ($tenant && ! SavingsProduct::where('tenant_id', $tenant->id)->exists()) {
            SavingsProduct::createDefaultsForTenant($tenant);
        }

        $validated = $request->validate([
            'q' => ['nullable', 'string', 'max:255'],
            'product_id' => ['nullable', 'integer'],
            'type' => ['nullable', Rule::in(array_keys(SavingsProduct::TYPES))],
            'status' => ['nullable', 'string', 'max:50'],
        ]);

        $search = trim((string) ($validated['q'] ?? ''));
        $productId = $validated['product_id'] ?? null;
        $type = $validated['type'] ?? null;
        $status = $validated['status'] ?? null;

        $accountsQuery = SavingsAccount::with(['customer.accountOfficer', 'product'])
            ->whereHas('customer', fn ($query) => $query->visibleTo($user))
            ->when($productId, fn ($query) => $query->where('savings_product_id', $productId))
            ->when($type, fn ($query) => $query->where('product_type', $type))
            ->when($status, fn ($query) => $query->where('status', $status))
            ->when($search !== '', function ($query) use ($search): void {
                $like = '%'.str_replace(['\\', '%', '_'], ['\\\\', '\\%', '\\_'], $search).'%';

                $query->where(function ($inner) use ($like): void {
                    $inner->where('account_number', 'like', $like)
                        ->orWhere('product_name', 'like', $like)
                        ->orWhereHas('customer', function ($customer) use ($like): void {
                            $customer->where('first_name', 'like', $like)
                                ->orWhere('middle_name', 'like', $like)
                                ->orWhere('last_name', 'like', $like)
                                ->orWhere('account_number', 'like', $like)
                                ->orWhere('phone', 'like', $like);
                    });
                });
            });

        $scheduledSavingsAccounts = $this->scheduledSavingsAccounts($user);
        $dueSavingsStats = $this->dueSavingsStats($scheduledSavingsAccounts);
        $missedSavingsAccounts = $this->missedSavingsAccounts($scheduledSavingsAccounts);
        $missedSavingsAmount = round((float) $missedSavingsAccounts->sum(fn ($account) => (float) $account->missed_savings_amount), 2);
        $missedSavingsPeriods = (int) $missedSavingsAccounts->sum(fn ($account) => (int) $account->missed_savings_periods);

        return view('tenant.savings.index', [
            'accounts' => (clone $accountsQuery)->latest()->paginate(15)->withQueryString(),
            'savingsStats' => [
                'accounts' => SavingsAccount::whereHas('customer', fn ($query) => $query->visibleTo($user))->count(),
                'active_accounts' => SavingsAccount::whereHas('customer', fn ($query) => $query->visibleTo($user))->where('status', 'active')->count(),
                'pending_deposits' => Schema::hasColumn('savings_transactions', 'status')
                    ? SavingsTransaction::whereHas('customer', fn ($query) => $query->visibleTo($user))->whereIn('type', ['deposit', 'withdrawal'])->where('status', 'pending')->count()
                    : 0,
                'today_deposits' => SavingsTransaction::whereHas('customer', fn ($query) => $query->visibleTo($user))
                    ->where('type', 'deposit')
                    ->when(Schema::hasColumn('savings_transactions', 'status'), fn ($query) => $query->where('status', 'posted'))
                    ->whereDate('transacted_at', today())
                    ->sum('amount'),
                'due_savings_amount' => $dueSavingsStats['amount'],
                'due_savings_accounts' => $dueSavingsStats['accounts'],
                'missed_savings_amount' => $missedSavingsAmount,
                'missed_savings_accounts' => $missedSavingsAccounts->count(),
                'missed_savings_periods' => $missedSavingsPeriods,
                'balance' => SavingsAccount::whereHas('customer', fn ($query) => $query->visibleTo($user))->sum('balance'),
                'maturing_soon' => SavingsAccount::whereHas('customer', fn ($query) => $query->visibleTo($user))
                    ->where('status', 'active')
                    ->whereBetween('maturity_date', [today(), today()->addDays(30)])
                    ->count(),
            ],
            'customers' => Customer::visibleTo($user)->where('status', 'active')->orderBy('first_name')->get(),
            'products' => SavingsProduct::orderBy('name')->get(),
            'pendingSavingsDeposits' => Schema::hasColumn('savings_transactions', 'status')
                ? SavingsTransaction::with(['customer', 'account', 'creator', 'approver'])
                    ->whereHas('customer', fn ($query) => $query->visibleTo($user))
                    ->whereIn('type', ['deposit', 'withdrawal'])
                    ->where('status', 'pending')
                    ->latest('transacted_at')
                    ->get()
                : collect(),
            'missedSavingsAccounts' => $missedSavingsAccounts,
            'maturingSavingsAccounts' => SavingsAccount::with(['customer.accountOfficer', 'product'])
                ->whereHas('customer', fn ($query) => $query->visibleTo($user))
                ->where('status', 'active')
                ->whereBetween('maturity_date', [today(), today()->addDays(30)])
                ->orderBy('maturity_date')
                ->get(),
            'types' => SavingsProduct::TYPES,
            'contributionFrequencies' => SavingsProduct::CONTRIBUTION_FREQUENCIES,
            'withdrawalPolicies' => SavingsProduct::WITHDRAWAL_POLICIES,
            'processingFeeTypes' => SavingsProduct::PROCESSING_FEE_TYPES,
            'processingFeeTimings' => SavingsProduct::PROCESSING_FEE_TIMINGS,
            'autoDebitModes' => SavingsAccount::autoDebitModes(),
            'autoDebitAccounts' => SavingsAccount::with('customer')
                ->whereHas('customer', fn ($query) => $query->visibleTo($user))
                ->where('status', 'active')
                ->orderBy('account_number')
                ->get(),
            'search' => $search,
            'productId' => $productId,
            'typeFilter' => $type,
            'statusFilter' => $status,
        ]);
    }

    public function store(Request $request, MemberNotificationAutomationService $notifications): RedirectResponse
    {
        $validated = $request->validate([
            'customer_id' => ['required', 'exists:customers,id'],
            'savings_product_id' => ['required', 'exists:savings_products,id'],
            'start_date' => ['nullable', 'date'],
            'target_amount' => ['nullable', 'numeric', 'min:0.01'],
            'target_date' => ['nullable', 'date', 'after_or_equal:today'],
            'contribution_amount' => ['nullable', 'numeric', 'min:0.01'],
            'contribution_frequency' => ['nullable', Rule::in(array_keys(SavingsProduct::CONTRIBUTION_FREQUENCIES))],
            'contribution_periods' => ['nullable', 'integer', 'min:1', 'max:10000'],
            'auto_debit_mode' => ['nullable', Rule::in(array_keys(SavingsAccount::autoDebitModes()))],
            'auto_debit_savings_account_id' => ['nullable', 'integer', 'exists:savings_accounts,id'],
            'cooperative_group_name' => ['nullable', 'string', 'max:255'],
        ]);

        $customer = Customer::visibleTo($request->user())
            ->where('status', 'active')
            ->findOrFail($validated['customer_id']);
        $tenant = $request->user()->tenant;

        $product = SavingsProduct::where('is_active', true)->findOrFail($validated['savings_product_id']);
        $this->guardProductRequirements($product, $validated);

        $startDate = Carbon::parse($validated['start_date'] ?? today())->toDateString();
        $contributionAmount = $validated['contribution_amount'] ?? null;
        $contributionFrequency = $validated['contribution_frequency'] ?? $product->contribution_frequency;
        $contributionPeriods = isset($validated['contribution_periods'])
            ? (int) $validated['contribution_periods']
            : $this->periodsFromDuration($product, $contributionFrequency);
        $maturityDate = $this->maturityDate($startDate, $contributionFrequency, $contributionPeriods, $product);
        $plannedTotalAmount = $this->plannedTotalAmount($product, $contributionAmount ? (float) $contributionAmount : null, $contributionPeriods, isset($validated['target_amount']) ? (float) $validated['target_amount'] : null);
        $autoDebitMode = $validated['auto_debit_mode'] ?? SavingsAccount::AUTO_DEBIT_MANUAL_ONLY;
        $autoDebitAccountId = $validated['auto_debit_savings_account_id'] ?? null;

        if ($autoDebitMode !== SavingsAccount::AUTO_DEBIT_MANUAL_ONLY) {
            if (! $autoDebitAccountId || ! $customer->savingsAccounts()->whereKey($autoDebitAccountId)->where('status', 'active')->exists()) {
                return back()->with('warning', 'Select an active source savings account for this member before enabling automatic debit.')->withInput();
            }
        } else {
            $autoDebitAccountId = null;
        }

        $lockedUntil = $product->withdrawal_policy === 'locked_until_maturity'
            ? ($validated['target_date'] ?? $maturityDate)
            : null;
        $processingFeeAmount = $this->processingFeeAmount($product, $contributionAmount ? (float) $contributionAmount : null, isset($validated['target_amount']) ? (float) $validated['target_amount'] : null);

        $account = SavingsAccount::create([
            'tenant_id' => $tenant->id,
            'customer_id' => $customer->id,
            'savings_product_id' => $product->id,
            'account_number' => NumberGenerator::make($tenant, $this->accountPrefix($product), SavingsAccount::class, 'account_number'),
            'product_name' => $product->name,
            'product_type' => $product->type,
            'start_date' => $startDate,
            'balance' => 0,
            'processing_fee_type' => $product->processing_fee_type ?? 'fixed',
            'processing_fee_amount' => $processingFeeAmount,
            'processing_fee_timing' => $product->processing_fee_timing ?? 'manual',
            'auto_debit_mode' => $autoDebitMode,
            'auto_debit_savings_account_id' => $autoDebitAccountId,
            'auto_debit_authorized_at' => $autoDebitAccountId ? now() : null,
            'target_amount' => $validated['target_amount'] ?? null,
            'target_date' => $validated['target_date'] ?? null,
            'contribution_amount' => $contributionAmount,
            'contribution_frequency' => $contributionFrequency,
            'contribution_periods' => $contributionPeriods,
            'planned_total_amount' => $plannedTotalAmount,
            'maturity_date' => $maturityDate,
            'cooperative_group_name' => $product->type === 'cooperative' ? ($validated['cooperative_group_name'] ?? null) : null,
            'locked_until' => $lockedUntil,
            'status' => 'active',
            'approved_by' => $request->user()->id,
            'approved_at' => now(),
            'opened_at' => now(),
        ]);

        ActivityLogger::log('savings_account_opened', $account->product_name.' opened for '.$customer->fullName().'.', $account);
        $notifications->send($customer, 'savings_account_opened', [
            'product_name' => $account->product_name,
            'account_number' => $account->account_number,
        ]);

        return redirect()->route('tenant.savings.show', $account)->with('status', 'Savings account opened.');
    }

    public function show(SavingsAccount $saving): View
    {
        $saving->load(['customer', 'product', 'approver', 'transactions' => fn ($query) => $query->with(['creator', 'approver', 'canceler'])->latest()]);
        abort_unless($saving->customer?->isVisibleTo(auth()->user()), 403, 'This member is assigned to another account officer.');

        return view('tenant.savings.show', [
            'account' => $saving,
            'progress' => $this->savingsProgress($saving),
            'savingsSchedule' => $this->savingsSchedule($saving),
        ]);
    }

    public function approve(SavingsAccount $saving, MemberNotificationAutomationService $notifications): RedirectResponse
    {
        $saving->load('customer');
        abort_unless($saving->customer?->isVisibleTo(auth()->user()), 403, 'This member is assigned to another account officer.');
        abort_unless($saving->status === 'pending', 422);

        $saving->update([
            'status' => 'active',
            'approved_by' => auth()->id(),
            'approved_at' => now(),
            'rejected_at' => null,
            'rejected_reason' => null,
            'opened_at' => $saving->opened_at ?: now(),
        ]);

        ActivityLogger::log('savings_account_approved', 'Savings account '.$saving->account_number.' approved.', $saving);
        $saving->refresh()->load('customer');
        $notifications->send($saving->customer, 'savings_account_opened', [
            'product_name' => $saving->product_name,
            'account_number' => $saving->account_number,
        ]);

        return back()->with('status', 'Savings account approved.');
    }

    public function reject(Request $request, SavingsAccount $saving): RedirectResponse
    {
        $saving->load('customer');
        abort_unless($saving->customer?->isVisibleTo(auth()->user()), 403, 'This member is assigned to another account officer.');
        abort_unless($saving->status === 'pending', 422);

        $validated = $request->validate([
            'rejected_reason' => ['required', 'string', 'max:1000'],
        ]);

        $saving->update([
            'status' => 'rejected',
            'rejected_at' => now(),
            'rejected_reason' => $validated['rejected_reason'],
        ]);

        ActivityLogger::log('savings_account_rejected', 'Savings account '.$saving->account_number.' rejected.', $saving);

        return back()->with('status', 'Savings account rejected.');
    }

    private function scheduledSavingsAccounts($user)
    {
        $usesTransactionStatus = Schema::hasColumn('savings_transactions', 'status');

        return SavingsAccount::with([
            'customer.accountOfficer',
            'product',
            'transactions' => function ($query) use ($usesTransactionStatus): void {
                $query->whereIn('type', ['deposit', 'transfer_in'])
                    ->when($usesTransactionStatus, fn ($query) => $query->where('status', 'posted'));
            },
        ])
            ->whereHas('customer', fn ($query) => $query->visibleTo($user))
            ->where('status', 'active')
            ->whereNotNull('start_date')
            ->whereNotNull('contribution_frequency')
            ->whereNotNull('contribution_periods')
            ->where('contribution_amount', '>', 0)
            ->get();
    }

    private function dueSavingsStats($accounts): array
    {
        $amount = 0.0;
        $dueAccounts = 0;

        foreach ($accounts as $account) {
            $dueAmount = $this->dueSavingsAmount($account);

            if ($dueAmount <= 0) {
                continue;
            }

            $amount += $dueAmount;
            $dueAccounts++;
        }

        return [
            'amount' => round($amount, 2),
            'accounts' => $dueAccounts,
        ];
    }

    private function missedSavingsAccounts($accounts)
    {
        return $accounts
            ->map(function (SavingsAccount $account) {
                $summary = $this->missedSavingsSummary($account);

                if ($summary['amount'] <= 0) {
                    return null;
                }

                $account->setAttribute('missed_savings_amount', $summary['amount']);
                $account->setAttribute('missed_savings_periods', $summary['periods']);
                $account->setAttribute('oldest_missed_due_date', $summary['oldest_due_date']);
                $account->setAttribute('latest_missed_due_date', $summary['latest_due_date']);

                return $account;
            })
            ->filter()
            ->sortByDesc(fn (SavingsAccount $account) => (float) $account->missed_savings_amount)
            ->values();
    }

    private function missedSavingsSummary(SavingsAccount $account): array
    {
        if (! $account->start_date || ! $account->contribution_frequency || ! $account->contribution_periods || (float) $account->contribution_amount <= 0) {
            return [
                'amount' => 0.0,
                'periods' => 0,
                'oldest_due_date' => null,
                'latest_due_date' => null,
            ];
        }

        $expected = (float) $account->contribution_amount;
        $unallocatedDeposits = $this->postedDepositNetTotal($account->transactions);
        $missedAmount = 0.0;
        $missedPeriods = 0;
        $oldestDueDate = null;
        $latestDueDate = null;

        for ($period = 1; $period <= (int) $account->contribution_periods; $period++) {
            $paid = min($expected, max(0, $unallocatedDeposits));
            $unallocatedDeposits -= $paid;
            $dueDate = $this->addSavingsPeriods($account->start_date->copy(), (string) $account->contribution_frequency, $period);

            if (! $dueDate->lt(today()) || $paid >= $expected) {
                continue;
            }

            $missedAmount += max(0, $expected - $paid);
            $missedPeriods++;
            $oldestDueDate ??= $dueDate->copy();
            $latestDueDate = $dueDate->copy();
        }

        return [
            'amount' => round($missedAmount, 2),
            'periods' => $missedPeriods,
            'oldest_due_date' => $oldestDueDate,
            'latest_due_date' => $latestDueDate,
        ];
    }

    private function dueSavingsAmount(SavingsAccount $account): float
    {
        if (! $account->start_date || ! $account->contribution_frequency || ! $account->contribution_periods || (float) $account->contribution_amount <= 0) {
            return 0.0;
        }

        $periodsDue = 0;
        $frequency = (string) $account->contribution_frequency;

        for ($period = 1; $period <= (int) $account->contribution_periods; $period++) {
            $dueDate = $this->addSavingsPeriods($account->start_date->copy(), $frequency, $period);

            if ($dueDate->lte(today())) {
                $periodsDue = $period;
                continue;
            }

            break;
        }

        if ($periodsDue === 0) {
            return 0.0;
        }

        $expectedByNow = (float) $account->contribution_amount * $periodsDue;
        $postedDeposits = $this->postedDepositNetTotal($account->transactions);

        return max(0, round($expectedByNow - $postedDeposits, 2));
    }

    private function guardProductRequirements(SavingsProduct $product, array $validated): void
    {
        if ($product->type === 'target' && blank($validated['target_amount'] ?? null)) {
            throw ValidationException::withMessages(['target_amount' => 'Target amount is required for target savings.']);
        }

        if (in_array($product->type, ['target', 'thrift_isusu', 'cooperative'], true) && blank($validated['contribution_amount'] ?? null)) {
            throw ValidationException::withMessages(['contribution_amount' => 'Contribution amount is required for this savings plan.']);
        }

        if (in_array($product->type, ['target', 'thrift_isusu', 'cooperative'], true) && blank($validated['contribution_periods'] ?? null)) {
            throw ValidationException::withMessages(['contribution_periods' => 'Number of contributions is required so maturity date and total savings can be calculated.']);
        }

        if ($product->type === 'cooperative' && blank($validated['cooperative_group_name'] ?? null)) {
            throw ValidationException::withMessages(['cooperative_group_name' => 'Cooperative group name is required for cooperative savings.']);
        }

        if (($product->processing_fee_type ?? 'fixed') === 'contribution_amount'
            && blank($validated['contribution_amount'] ?? null)
        ) {
            throw ValidationException::withMessages(['contribution_amount' => 'Contribution amount is required because this product uses contribution amount as the processing fee.']);
        }

        if (($product->processing_fee_type ?? 'fixed') === 'contribution_amount'
            && blank($validated['contribution_periods'] ?? null)
        ) {
            throw ValidationException::withMessages(['contribution_periods' => 'Number of contributions is required so maturity date and total savings can be calculated.']);
        }
    }

    private function accountPrefix(SavingsProduct $product): string
    {
        if (filled($product->account_number_prefix)) {
            return strtoupper($product->account_number_prefix);
        }

        return match ($product->type) {
            'target' => 'TSA',
            'thrift_isusu' => 'ISU',
            'cooperative' => 'COP',
            default => 'SAV',
        };
    }

    private function processingFeeAmount(SavingsProduct $product, ?float $contributionAmount, ?float $targetAmount): float
    {
        return $product->processingFeeFor($contributionAmount, $targetAmount);
    }

    private function periodsFromDuration(SavingsProduct $product, ?string $frequency): ?int
    {
        if (! $product->duration_days || ! $frequency) {
            return null;
        }

        $days = match ($frequency) {
            'daily' => 1,
            'weekly' => 7,
            'biweekly' => 14,
            'quarterly' => 90,
            'yearly' => 365,
            default => 30,
        };

        return max(1, (int) ceil((int) $product->duration_days / $days));
    }

    private function maturityDate(string $startDate, ?string $frequency, ?int $periods, SavingsProduct $product): ?string
    {
        $date = Carbon::parse($startDate);

        if ($periods && $frequency) {
            match ($frequency) {
                'daily' => $date->addDays($periods),
                'weekly' => $date->addWeeks($periods),
                'biweekly' => $date->addWeeks($periods * 2),
                'quarterly' => $date->addMonthsNoOverflow($periods * 3),
                'yearly' => $date->addYears($periods),
                default => $date->addMonthsNoOverflow($periods),
            };

            return $date->toDateString();
        }

        if ($product->duration_days) {
            return $date->addDays((int) $product->duration_days)->toDateString();
        }

        return null;
    }

    private function plannedTotalAmount(SavingsProduct $product, ?float $contributionAmount, ?int $periods, ?float $targetAmount): ?float
    {
        if ($contributionAmount && $periods) {
            return round($contributionAmount * $periods, 2);
        }

        if ($product->type === 'target' && $targetAmount) {
            return round($targetAmount, 2);
        }

        return null;
    }

    private function savingsProgress(SavingsAccount $account): array
    {
        $goal = (float) ($account->target_amount ?: $account->planned_total_amount ?: 0);
        $postedTransactions = $account->transactions->where('status', 'posted');
        $postedDeposits = $postedTransactions->whereIn('type', ['deposit', 'transfer_in']);
        $deposited = $this->postedDepositNetTotal($postedDeposits);
        $saved = $deposited;
        $withdrawn = (float) $postedTransactions->where('type', 'withdrawal')->sum('amount');
        $remaining = $goal > 0 ? max(0, $goal - $saved) : 0;
        $percent = $goal > 0 ? min(100, round(($saved / $goal) * 100, 2)) : 0;

        return compact('goal', 'saved', 'deposited', 'withdrawn', 'remaining', 'percent');
    }

    private function savingsSchedule(SavingsAccount $account): array
    {
        if (! $account->start_date || ! $account->contribution_frequency || ! $account->contribution_periods || (float) $account->contribution_amount <= 0) {
            return [];
        }

        $expected = (float) $account->contribution_amount;
        $periods = (int) $account->contribution_periods;
        $plannedTotal = (float) ($account->planned_total_amount ?: ($expected * $periods));
        $unallocatedDeposits = $this->postedDepositNetTotal($account->transactions
            ->whereIn('type', ['deposit', 'transfer_in'])
            ->where('status', 'posted'));
        $cumulativeSaved = 0.0;
        $rows = [];

        for ($period = 1; $period <= $periods; $period++) {
            $paid = min($expected, max(0, $unallocatedDeposits));
            $unallocatedDeposits -= $paid;
            $cumulativeSaved += $paid;
            $dueDate = $this->addSavingsPeriods($account->start_date->copy(), (string) $account->contribution_frequency, $period);
            $remaining = max(0, $plannedTotal - $cumulativeSaved);

            $status = match (true) {
                $paid >= $expected => 'saved',
                $paid > 0 => 'partial',
                $dueDate->isPast() && ! $dueDate->isToday() => 'missed',
                $dueDate->isToday() => 'due_today',
                default => 'upcoming',
            };

            $rows[] = [
                'period' => $period,
                'due_date' => $dueDate,
                'expected' => $expected,
                'paid' => $paid,
                'remaining' => $remaining,
                'status' => $status,
            ];
        }

        return $rows;
    }

    private function postedDepositNetTotal($transactions): float
    {
        return round((float) $transactions->sum(function (SavingsTransaction $transaction): float {
            $netAmount = (float) ($transaction->net_amount ?? 0);

            return $netAmount > 0 ? $netAmount : (float) $transaction->amount;
        }), 2);
    }

    private function addSavingsPeriods(Carbon $date, string $frequency, int $periods): Carbon
    {
        match ($frequency) {
            'daily' => $date->addDays($periods),
            'weekly' => $date->addWeeks($periods),
            'biweekly' => $date->addWeeks($periods * 2),
            'quarterly' => $date->addMonthsNoOverflow($periods * 3),
            'yearly' => $date->addYears($periods),
            default => $date->addMonthsNoOverflow($periods),
        };

        return $date;
    }
}
