<?php

namespace App\Http\Controllers\Tenant;

use App\Http\Controllers\Controller;
use App\Models\Expense;
use App\Models\ExpenseCategory;
use App\Services\AccountingService;
use App\Services\ActivityLogger;
use App\Services\BranchService;
use App\Services\NumberGenerator;
use App\Services\TransactionCancellationService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Schema;
use Illuminate\Validation\Rule;
use Illuminate\View\View;
use InvalidArgumentException;

class ExpenseController extends Controller
{
    private const DEFAULT_CATEGORIES = [
        'Salary' => 'Staff salaries, allowances, and payroll-related expenses.',
        'Fuel/Diesel' => 'Fuel, diesel, generator, and vehicle fueling costs.',
        'Transportation' => 'Transport fares, movement costs, and field operations travel.',
        'Maintenance' => 'Routine maintenance for office, vehicles, equipment, and facilities.',
        'Office Supplies' => 'Stationery, printing items, forms, and daily office consumables.',
        'Utility Bills' => 'Electricity, water, waste, and other utility payments.',
        'Internet' => 'Internet subscriptions, data plans, and connectivity costs.',
        'Rent' => 'Office rent, shop rent, and related property charges.',
        'Security' => 'Security services, guards, safety, and monitoring expenses.',
        'Marketing' => 'Promotions, adverts, branding, outreach, and sales campaigns.',
        'Repairs' => 'Repairs for assets, devices, office equipment, and facilities.',
        'Bank Charges' => 'Bank fees, transfer charges, POS charges, and account maintenance.',
        'Tax' => 'Taxes, levies, dues, and statutory payments.',
        'Logistics' => 'Courier, dispatch, delivery, and operational logistics.',
    ];

    public function index(Request $request, BranchService $branches): View
    {
        $tenantId = $request->user()->tenant_id;
        $supportsExpenseBranches = Schema::hasColumn('expenses', 'branch_id');
        $supportsRecurringExpenses = Schema::hasColumn('expenses', 'is_recurring');
        $branchState = $supportsExpenseBranches
            ? $branches->formState($request->user())
            : ['branches' => collect(), 'showBranchField' => false, 'defaultBranchId' => null];

        $this->ensureDefaultCategories($tenantId);

        $validated = $request->validate([
            'search' => ['nullable', 'string', 'max:255'],
            'category_id' => ['nullable', 'integer'],
            'branch_id' => ['nullable', 'string', 'max:50'],
            'status' => ['nullable', Rule::in([Expense::STATUS_PENDING, Expense::STATUS_APPROVED, Expense::STATUS_REJECTED, Expense::STATUS_CANCELED])],
            'from' => ['nullable', 'date'],
            'to' => ['nullable', 'date'],
        ]);

        $from = filled($validated['from'] ?? null) ? Carbon::parse($validated['from'])->startOfDay() : now()->startOfMonth();
        $to = filled($validated['to'] ?? null) ? Carbon::parse($validated['to'])->endOfDay() : now()->endOfMonth();
        $search = trim((string) ($validated['search'] ?? ''));
        $categoryId = $validated['category_id'] ?? null;
        $branchId = $validated['branch_id'] ?? null;
        $status = $validated['status'] ?? null;
        $relations = ['categoryModel', 'recordedBy', 'approvedBy', 'rejectedBy', 'canceledBy'];

        if ($supportsExpenseBranches) {
            $relations[] = 'branch';
        }

        $baseQuery = Expense::query()
            ->with($relations)
            ->whereBetween('spent_at', [$from->toDateString(), $to->toDateString()])
            ->when($categoryId, fn ($query) => $query->where('expense_category_id', $categoryId))
            ->when($supportsExpenseBranches && filled($branchId), function ($query) use ($branchId): void {
                $branchId === 'unassigned'
                    ? $query->whereNull('branch_id')
                    : $query->where('branch_id', (int) $branchId);
            })
            ->when($status, fn ($query) => $query->where('status', $status))
            ->when($search !== '', function ($query) use ($search): void {
                $query->where(function ($nested) use ($search): void {
                    $like = '%'.str_replace(['\\', '%', '_'], ['\\\\', '\\%', '\\_'], $search).'%';

                    $nested->where('category', 'like', $like)
                        ->orWhere('payee', 'like', $like)
                        ->orWhere('method', 'like', $like)
                        ->orWhere('reference', 'like', $like);
                });
            });
        $approvedQuery = (clone $baseQuery)->where('status', Expense::STATUS_APPROVED);
        $approvedChartExpenses = (clone $approvedQuery)->get();
        $monthlyComparison = $this->monthlyComparison();
        $expenseTrend = $this->expenseTrend();
        $currentMonthTotal = Expense::where('status', Expense::STATUS_APPROVED)
            ->whereBetween('spent_at', [now()->startOfMonth()->toDateString(), now()->endOfMonth()->toDateString()])
            ->sum('amount');
        $recurringThisMonthQuery = Expense::where('status', Expense::STATUS_APPROVED)
            ->whereBetween('spent_at', [now()->startOfMonth()->toDateString(), now()->endOfMonth()->toDateString()]);

        if ($supportsRecurringExpenses) {
            $recurringThisMonthQuery->where('is_recurring', true);
        } else {
            $recurringThisMonthQuery->whereRaw('1 = 0');
        }

        $recurringThisMonthAmount = (clone $recurringThisMonthQuery)->sum('amount');
        $recurringThisMonthCount = (clone $recurringThisMonthQuery)->count();
        $previousMonthTotal = Expense::where('status', Expense::STATUS_APPROVED)
            ->whereBetween('spent_at', [now()->subMonthNoOverflow()->startOfMonth()->toDateString(), now()->subMonthNoOverflow()->endOfMonth()->toDateString()])
            ->sum('amount');
        $monthlyTrendPercent = (float) $previousMonthTotal > 0
            ? round((((float) $currentMonthTotal - (float) $previousMonthTotal) / (float) $previousMonthTotal) * 100, 1)
            : ((float) $currentMonthTotal > 0 ? 100.0 : 0.0);
        $topCategory = $this->categoryChart($approvedChartExpenses)->first();

        return view('tenant.expenses.index', [
            'expenses' => (clone $baseQuery)->latest('spent_at')->paginate(15)->withQueryString(),
            'totalExpenses' => (clone $approvedQuery)->sum('amount'),
            'categoryTotals' => (clone $approvedQuery)
                ->select('category', DB::raw('SUM(amount) as total'))
                ->groupBy('category')
                ->orderByDesc('total')
                ->limit(8)
                ->get(),
            'categories' => ExpenseCategory::orderBy('name')->get(),
            'activeCategories' => ExpenseCategory::where('is_active', true)->orderBy('name')->get(),
            'branches' => $branchState['branches'],
            'showBranchField' => $branchState['showBranchField'],
            'defaultBranchId' => $branchState['defaultBranchId'],
            'supportsExpenseBranches' => $supportsExpenseBranches,
            'supportsRecurringExpenses' => $supportsRecurringExpenses,
            'pendingExpenses' => Expense::with(['categoryModel', 'recordedBy'])
                ->where('status', Expense::STATUS_PENDING)
                ->latest('submitted_at')
                ->latest()
                ->limit(10)
                ->get(),
            'expenseStats' => [
                'records' => (clone $baseQuery)->count(),
                'categories' => (clone $baseQuery)->distinct('expense_category_id')->count('expense_category_id'),
                'average' => (float) (clone $approvedQuery)->avg('amount'),
                'approved' => (clone $baseQuery)->where('status', Expense::STATUS_APPROVED)->count(),
                'pending' => (clone $baseQuery)->where('status', Expense::STATUS_PENDING)->count(),
                'pending_amount' => Expense::where('status', Expense::STATUS_PENDING)->sum('amount'),
                'rejected' => (clone $baseQuery)->where('status', Expense::STATUS_REJECTED)->count(),
                'today_total' => Expense::where('status', Expense::STATUS_APPROVED)->whereDate('spent_at', today())->sum('amount'),
                'today_count' => Expense::where('status', Expense::STATUS_APPROVED)->whereDate('spent_at', today())->count(),
                'month_total' => Expense::where('status', Expense::STATUS_APPROVED)->whereBetween('spent_at', [now()->startOfMonth()->toDateString(), now()->endOfMonth()->toDateString()])->sum('amount'),
                'recurring_month_total' => $recurringThisMonthAmount,
                'recurring_month_count' => $recurringThisMonthCount,
                'previous_month_total' => $previousMonthTotal,
                'monthly_trend_percent' => $monthlyTrendPercent,
                'top_category_name' => $topCategory['label'] ?? 'No category yet',
                'top_category_amount' => $topCategory['total'] ?? 0,
            ],
            'expenseCharts' => [
                'category' => $this->categoryChart($approvedChartExpenses)->values(),
                'monthly' => $monthlyComparison,
                'trend' => $expenseTrend,
                'cash_bank' => $this->cashBankChart($approvedChartExpenses)->values(),
                'branch' => $this->branchChart($approvedChartExpenses, $supportsExpenseBranches)->values(),
            ],
            'from' => $from,
            'to' => $to,
            'search' => $search,
            'status' => $status,
            'categoryId' => $categoryId,
            'branchId' => $branchId,
        ]);
    }

    public function store(Request $request, BranchService $branches): RedirectResponse
    {
        $tenantId = $request->user()->tenant_id;
        $supportsExpenseBranches = Schema::hasColumn('expenses', 'branch_id');
        $supportsRecurringExpenses = Schema::hasColumn('expenses', 'is_recurring');

        $validated = $request->validate([
            'expense_category_id' => [
                'required',
                Rule::exists('expense_categories', 'id')
                    ->where('tenant_id', $tenantId)
                    ->where('is_active', true),
            ],
            'branch_id' => [
                'nullable',
                Rule::exists('branches', 'id')
                    ->where('tenant_id', $tenantId)
                    ->where('is_active', true),
            ],
            'amount' => ['required', 'numeric', 'min:0.01'],
            'spent_at' => ['required', 'date'],
            'payee' => ['nullable', 'string', 'max:255'],
            'method' => ['required', 'string', 'max:50'],
            'is_recurring' => ['nullable', 'boolean'],
            'recurrence_frequency' => ['nullable', Rule::in(['daily', 'weekly', 'monthly', 'quarterly', 'yearly'])],
            'approval_level_required' => ['required', 'integer', 'min:1', 'max:3'],
            'notes' => ['nullable', 'string', 'max:1000'],
        ]);
        if ($supportsExpenseBranches) {
            $validated['branch_id'] = $branches->resolveSubmittedBranchId($request);
        }

        $expense = DB::transaction(function () use ($request, $validated, $supportsExpenseBranches, $supportsRecurringExpenses): Expense {
            $category = ExpenseCategory::findOrFail($validated['expense_category_id']);

            $payload = $validated + [
                'tenant_id' => $request->user()->tenant_id,
                'recorded_by' => $request->user()->id,
                'category' => $category->name,
                'reference' => NumberGenerator::reference('EXP'),
                'status' => Expense::STATUS_PENDING,
                'current_approval_level' => 0,
                'submitted_at' => now(),
            ];

            if ($supportsExpenseBranches) {
                $payload['branch_id'] = $validated['branch_id'] ?? null;
            } else {
                unset($payload['branch_id']);
            }

            if ($supportsRecurringExpenses) {
                $payload['is_recurring'] = $request->boolean('is_recurring');
                $payload['recurrence_frequency'] = $request->boolean('is_recurring')
                    ? ($validated['recurrence_frequency'] ?? 'monthly')
                    : null;
            } else {
                unset($payload['is_recurring'], $payload['recurrence_frequency']);
            }

            return Expense::create($payload);
        });

        ActivityLogger::log('expense_submitted', 'Expense '.$expense->reference.' submitted for approval.', $expense);

        return redirect()->route('tenant.expenses.index')->with('status', 'Expense submitted for approval.');
    }

    public function approve(Expense $expense, AccountingService $accounting): RedirectResponse
    {
        abort_unless($expense->status === Expense::STATUS_PENDING, 422, 'Only pending expenses can be approved.');

        $expense = DB::transaction(function () use ($expense, $accounting): Expense {
            $lockedExpense = Expense::whereKey($expense->id)->lockForUpdate()->firstOrFail();
            abort_unless($lockedExpense->status === Expense::STATUS_PENDING, 422, 'Only pending expenses can be approved.');

            $requiredLevel = max(1, min(3, (int) $lockedExpense->approval_level_required));
            $nextLevel = min($requiredLevel, (int) $lockedExpense->current_approval_level + 1);
            $history = $lockedExpense->approval_history ?: [];
            $history[] = [
                'action' => 'approved',
                'level' => $nextLevel,
                'user_id' => auth()->id(),
                'user_name' => auth()->user()?->name,
                'approved_at' => now()->toDateTimeString(),
            ];

            $updates = [
                'current_approval_level' => $nextLevel,
                'approval_history' => $history,
            ];

            if ($nextLevel >= $requiredLevel) {
                $updates += [
                    'status' => Expense::STATUS_APPROVED,
                    'approved_by' => auth()->id(),
                    'approved_at' => now(),
                ];
            }

            $lockedExpense->update($updates);
            $lockedExpense = $lockedExpense->fresh(['tenant']);

            if ($lockedExpense->status === Expense::STATUS_APPROVED && ! $lockedExpense->accounting_posted_at) {
                $accounting->recordExpense($lockedExpense);
                $lockedExpense->update(['accounting_posted_at' => now()]);
            }

            return $lockedExpense->fresh(['recordedBy', 'approvedBy']);
        });

        ActivityLogger::log('expense_approved', 'Expense '.$expense->reference.' approved at level '.$expense->current_approval_level.'.', $expense);

        return back()->with('status', $expense->status === Expense::STATUS_APPROVED
            ? 'Expense fully approved and posted to accounting.'
            : 'Expense approval moved to level '.$expense->current_approval_level.' of '.$expense->approval_level_required.'.'
        );
    }

    public function reject(Request $request, Expense $expense): RedirectResponse
    {
        abort_unless($expense->status === Expense::STATUS_PENDING, 422, 'Only pending expenses can be rejected.');

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

        $expense = DB::transaction(function () use ($expense, $validated): Expense {
            $lockedExpense = Expense::whereKey($expense->id)->lockForUpdate()->firstOrFail();
            abort_unless($lockedExpense->status === Expense::STATUS_PENDING, 422, 'Only pending expenses can be rejected.');

            $history = $lockedExpense->approval_history ?: [];
            $history[] = [
                'action' => 'rejected',
                'level' => (int) $lockedExpense->current_approval_level + 1,
                'user_id' => auth()->id(),
                'user_name' => auth()->user()?->name,
                'rejected_at' => now()->toDateTimeString(),
                'reason' => $validated['rejected_reason'],
            ];

            $lockedExpense->update([
                'status' => Expense::STATUS_REJECTED,
                'approval_history' => $history,
                'rejected_by' => auth()->id(),
                'rejected_at' => now(),
                'rejected_reason' => $validated['rejected_reason'],
            ]);

            return $lockedExpense->fresh(['recordedBy', 'rejectedBy']);
        });

        ActivityLogger::log('expense_rejected', 'Expense '.$expense->reference.' rejected.', $expense);

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

    public function cancel(Request $request, Expense $expense, TransactionCancellationService $cancellations): RedirectResponse
    {
        $validated = $request->validate([
            'canceled_reason' => ['required', 'string', 'max:1000'],
        ]);

        try {
            $cancellations->cancelExpense($expense, $validated['canceled_reason'], $request->user()->id);
        } catch (InvalidArgumentException $exception) {
            return back()->with('warning', $exception->getMessage());
        }

        return back()->with('status', 'Expense canceled and accounting entries were reversed.');
    }

    public function storeCategory(Request $request): RedirectResponse
    {
        $tenantId = $request->user()->tenant_id;
        $validated = $request->validate([
            'name' => [
                'required',
                'string',
                'max:255',
                Rule::unique('expense_categories', 'name')->where('tenant_id', $tenantId),
            ],
            'description' => ['nullable', 'string', 'max:1000'],
            'is_active' => ['nullable', 'boolean'],
        ]);

        ExpenseCategory::create([
            'tenant_id' => $tenantId,
            'name' => trim($validated['name']),
            'description' => $validated['description'] ?? null,
            'is_active' => $request->boolean('is_active', true),
        ]);

        return redirect()->route('tenant.expenses.index')->with('status', 'Expense category created.');
    }

    public function updateCategory(Request $request, ExpenseCategory $category): RedirectResponse
    {
        $tenantId = $request->user()->tenant_id;
        $validated = $request->validate([
            'name' => [
                'required',
                'string',
                'max:255',
                Rule::unique('expense_categories', 'name')
                    ->where('tenant_id', $tenantId)
                    ->ignore($category->id),
            ],
            'description' => ['nullable', 'string', 'max:1000'],
            'is_active' => ['nullable', 'boolean'],
        ]);

        DB::transaction(function () use ($request, $category, $validated): void {
            $name = trim($validated['name']);

            $category->update([
                'name' => $name,
                'description' => $validated['description'] ?? null,
                'is_active' => $request->boolean('is_active'),
            ]);

            Expense::where('expense_category_id', $category->id)->update([
                'category' => $name,
            ]);
        });

        return redirect()->route('tenant.expenses.index')->with('status', 'Expense category updated.');
    }

    private function ensureDefaultCategories(int $tenantId): void
    {
        if (ExpenseCategory::where('tenant_id', $tenantId)->exists()) {
            return;
        }

        foreach (self::DEFAULT_CATEGORIES as $name => $description) {
            ExpenseCategory::create([
                'tenant_id' => $tenantId,
                'name' => $name,
                'description' => $description,
                'is_active' => true,
            ]);
        }
    }

    private function categoryChart($expenses)
    {
        return $expenses
            ->groupBy(fn (Expense $expense) => $expense->category ?: 'Uncategorized')
            ->map(fn ($items, string $label): array => [
                'label' => $label,
                'total' => round((float) $items->sum('amount'), 2),
                'count' => $items->count(),
            ])
            ->sortByDesc('total')
            ->take(10)
            ->values();
    }

    private function cashBankChart($expenses)
    {
        $bankMethods = ['transfer', 'bank_transfer', 'pos', 'cheque', 'bank', 'mobile_money'];

        return $expenses
            ->groupBy(fn (Expense $expense) => in_array(strtolower((string) $expense->method), $bankMethods, true) ? 'Bank' : 'Cash')
            ->map(fn ($items, string $label): array => [
                'label' => $label,
                'total' => round((float) $items->sum('amount'), 2),
                'count' => $items->count(),
            ])
            ->sortByDesc('total')
            ->values();
    }

    private function branchChart($expenses, bool $supportsExpenseBranches)
    {
        if (! $supportsExpenseBranches) {
            return collect([[
                'label' => 'Head Office',
                'total' => round((float) $expenses->sum('amount'), 2),
                'count' => $expenses->count(),
            ]]);
        }

        return $expenses
            ->groupBy(fn (Expense $expense) => $expense->branch?->name ?: 'Head Office')
            ->map(fn ($items, string $label): array => [
                'label' => $label,
                'total' => round((float) $items->sum('amount'), 2),
                'count' => $items->count(),
            ])
            ->sortByDesc('total')
            ->take(8)
            ->values();
    }

    private function monthlyComparison()
    {
        $months = collect(range(5, 0))->map(fn (int $offset) => now()->subMonthsNoOverflow($offset)->startOfMonth());
        $start = $months->first()->toDateString();
        $monthly = Expense::where('status', Expense::STATUS_APPROVED)
            ->whereDate('spent_at', '>=', $start)
            ->get()
            ->groupBy(fn (Expense $expense) => $expense->spent_at->format('Y-m'));

        return $months->map(function (Carbon $month) use ($monthly): array {
            $key = $month->format('Y-m');
            $items = $monthly->get($key, collect());

            return [
                'label' => $month->format('M'),
                'key' => $key,
                'total' => round((float) $items->sum('amount'), 2),
                'count' => $items->count(),
            ];
        })->values();
    }

    private function expenseTrend()
    {
        $days = collect(range(13, 0))->map(fn (int $offset) => today()->subDays($offset));
        $daily = Expense::where('status', Expense::STATUS_APPROVED)
            ->whereDate('spent_at', '>=', $days->first()->toDateString())
            ->get()
            ->groupBy(fn (Expense $expense) => $expense->spent_at->format('Y-m-d'));

        return $days->map(function (Carbon $day) use ($daily): array {
            $key = $day->format('Y-m-d');
            $items = $daily->get($key, collect());

            return [
                'label' => app_day_month($day),
                'key' => $key,
                'total' => round((float) $items->sum('amount'), 2),
                'count' => $items->count(),
            ];
        })->values();
    }
}
