<?php

namespace App\Http\Controllers\Admin;

use App\Http\Controllers\Admin\Concerns\CalculatesGstDocumentTotals;
use App\Helpers\CompanySettings;
use App\Helpers\ExpenseDocumentSettings;
use App\Helpers\GstHelper;
use App\Helpers\UnitHelper;
use App\Models\Company;
use App\Models\Expense;
use App\Models\ExpenseItem;
use App\Models\Customer;
use App\Models\ExpenseCategory;
use App\Models\TaxRate;
use App\Models\Vendor;
use Illuminate\Http\JsonResponse;
use App\Services\ExpenseDocumentPresenter;
use App\Services\ExpensePdfGenerator;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Symfony\Component\HttpFoundation\StreamedResponse;

class ExpenseController extends AdminController
{
    use CalculatesGstDocumentTotals;

    public function __construct(
        private readonly ExpenseDocumentPresenter $documentPresenter,
    ) {}

    public function index(Request $request)
    {
        $companyId = $this->companyId();
        $filters = $request->only([
            'q', 'vendor_name', 'expense_number', 'category', 'status', 'city',
            'issued_from', 'issued_to', 'amount_min', 'amount_max', 'item_name',
        ]);

        $query = Expense::with(['vendor'])
            ->forCompany($companyId)
            ->when($filters['vendor_name'] ?? null, fn ($q, $v) => $q->whereHas('vendor', fn ($c) => $c->where('name', 'like', "%{$v}%")))
            ->when($filters['expense_number'] ?? null, fn ($q, $v) => $q->where('expense_number', 'like', "%{$v}%"))
            ->when($filters['status'] ?? null, fn ($q, $v) => $v !== 'all' ? $q->where('status', $v) : $q)
            ->when($filters['city'] ?? null, fn ($q, $v) => $q->whereHas('vendor', fn ($c) => $c->where('city', 'like', "%{$v}%")))
            ->when($filters['issued_from'] ?? null, fn ($q, $v) => $q->whereDate('expense_date', '>=', $v))
            ->when($filters['issued_to'] ?? null, fn ($q, $v) => $q->whereDate('expense_date', '<=', $v))
            ->when($filters['category'] ?? null, fn ($q, $v) => $q->where('category', 'like', "%{$v}%"))
            ->when($filters['amount_min'] ?? null, fn ($q, $v) => $q->where('grand_total', '>=', $v))
            ->when($filters['amount_max'] ?? null, fn ($q, $v) => $q->where('grand_total', '<=', $v))
            ->when($filters['item_name'] ?? null, fn ($q, $v) => $q->whereHas('items', fn ($i) => $i->where('item_name', 'like', "%{$v}%")))
            ->when($filters['q'] ?? null, function ($q, $v) {
                $q->where(function ($sub) use ($v) {
                    $sub->where('expense_number', 'like', "%{$v}%")
                        ->orWhereHas('vendor', fn ($c) => $c->where('name', 'like', "%{$v}%"));
                });
            });

        $expenses = $query->latest('expense_date')->latest('id')->get();
        $hasFilters = collect($filters)->filter(fn ($v) => $v && $v !== 'all')->isNotEmpty();

        $noteId = $request->query('expense');
        $selected = $noteId ? $expenses->firstWhere('id', (int) $noteId) : null;

        if ($noteId && ! $selected) {
            $selected = Expense::with(['vendor'])->forCompany($companyId)->find($noteId);
            if ($selected) {
                $expenses = $expenses->push($selected)
                    ->sortByDesc(fn ($n) => $n->expense_date->format('Y-m-d').'-'.$n->id)
                    ->values();
            }
        }

        $panelData = $selected ? $this->documentPresenter->displayData($selected) : [];

        return view('admin.expenses.index', array_merge([
            'expenses' => $expenses,
            'filters' => $filters,
            'hasFilters' => $hasFilters,
            'statuses' => config('expense.statuses'),
            'features' => config('expense.features'),
            'selected' => $selected,
            'panelOpen' => (bool) ($noteId && $selected),
        ], $panelData));
    }

    public function create(Request $request)
    {
        $data = $this->formData(null, $request);

        if ($data['vendors']->isEmpty()) {
            return view('admin.expenses.create', array_merge($data, ['emptyVendors' => true]));
        }

        return view('admin.expenses.create', $data);
    }

    public function store(Request $request)
    {
        $data = $this->validateExpense($request);
        $items = $this->validateLineItems($request);
        $calc = $this->calculateTotals($data, $items);
        $processed = $calc['items'];
        unset($calc['items']);

        $status = $request->input('save_action') === 'draft' ? 'draft' : 'recorded';
        $calc['category'] = $processed[0]['item_name'] ?? '';
        $calc['amount'] = $calc['grand_total'];

        $expense = null;
        DB::transaction(function () use ($data, $processed, $calc, $status, &$expense) {
            $expense = Expense::create(array_merge($data, $calc, ['status' => $status]));
            $this->syncLineItems($expense, $processed);
        });

        if (in_array($request->input('save_action'), ['save', 'preview'], true)) {
            return redirect()->route('admin.expenses.index', ['expense' => $expense->id])
                ->with('success', 'Expense created successfully.');
        }

        return redirect()->route('admin.expenses.index')
            ->with('success', 'Expense saved as draft.');
    }

    public function edit(Expense $expense)
    {
        $this->authorizeOrder($expense);
        $expense->load(['items']);

        return view('admin.expenses.edit', $this->formData($expense, request()));
    }

    public function update(Request $request, Expense $expense)
    {
        $this->authorizeOrder($expense);

        $data = $this->validateExpense($request, $expense);
        $items = $this->validateLineItems($request);
        $calc = $this->calculateTotals($data, $items);
        $processed = $calc['items'];
        unset($calc['items']);

        $status = $request->input('save_action') === 'draft'
            ? 'draft'
            : ($expense->status === 'draft' ? 'recorded' : $expense->status);

        $calc['category'] = $processed[0]['item_name'] ?? '';
        $calc['amount'] = $calc['grand_total'];

        DB::transaction(function () use ($expense, $data, $processed, $calc, $status) {
            $expense->update(array_merge($data, $calc, ['status' => $status]));
            $expense->items()->delete();
            $this->syncLineItems($expense, $processed);
        });

        if (in_array($request->input('save_action'), ['save', 'preview'], true)) {
            return redirect()->route('admin.expenses.index', ['expense' => $expense->id])
                ->with('success', 'Expense updated successfully.');
        }

        return redirect()->route('admin.expenses.index')
            ->with('success', 'Expense saved as draft.');
    }

    public function show(Expense $expense)
    {
        return redirect()->route('admin.expenses.index', ['expense' => $expense->id]);
    }

    public function print(Expense $expense, Request $request)
    {
        $this->authorizeOrder($expense);
        $copyKey = $request->query('copy', 'original');

        return view('admin.expenses.print-pdf', [
            'expense' => $expense,
            'copyKey' => $copyKey,
            'autoprint' => $request->boolean('autoprint', true),
            'backUrl' => route('admin.expenses.index', ['expense' => $expense->id]),
            'pdfUrl' => route('admin.expenses.pdf', [
                'expense' => $expense,
                'copy' => $copyKey,
                'inline' => 1,
            ]),
        ]);
    }

    public function downloadPdf(Expense $expense, Request $request, ExpensePdfGenerator $pdfGenerator)
    {
        $this->authorizeOrder($expense);
        $data = $this->documentPresenter->displayData($expense);
        $copyKey = $request->query('copy', 'original');
        $copies = $data['documentCopies'];
        $copyLabel = $copies[$copyKey] ?? $copies['original'] ?? 'Original Copy';

        if ($request->boolean('inline')) {
            return $pdfGenerator->stream($expense, $data, $copyLabel);
        }

        return $pdfGenerator->download($expense, $data, $copyLabel)
            ->withHeaders([
                'Cache-Control' => 'no-store, no-cache, must-revalidate, max-age=0',
                'Pragma' => 'no-cache',
            ]);
    }

    public function shareLink(Expense $expense)
    {
        $this->authorizeOrder($expense);

        if ($expense->status === 'draft') {
            return response()->json([
                'message' => 'Issue this expense before sharing a public link.',
            ], 422);
        }

        return response()->json([
            'url' => $expense->publicShareUrl(),
            'visibility' => 'PUBLIC',
        ]);
    }

    public function export(Request $request): StreamedResponse
    {
        $companyId = $this->companyId();
        $filters = $request->only([
            'q', 'vendor_name', 'expense_number', 'status', 'city',
            'issued_from', 'issued_to', 'amount_min', 'amount_max', 'item_name',
        ]);

        $orders = Expense::with('vendor')
            ->forCompany($companyId)
            ->when($filters['vendor_name'] ?? null, fn ($q, $v) => $q->whereHas('vendor', fn ($c) => $c->where('name', 'like', "%{$v}%")))
            ->when($filters['expense_number'] ?? null, fn ($q, $v) => $q->where('expense_number', 'like', "%{$v}%"))
            ->when($filters['status'] ?? null, fn ($q, $v) => $v !== 'all' ? $q->where('status', $v) : $q)
            ->when($filters['issued_from'] ?? null, fn ($q, $v) => $q->whereDate('expense_date', '>=', $v))
            ->when($filters['issued_to'] ?? null, fn ($q, $v) => $q->whereDate('expense_date', '<=', $v))
            ->when($filters['q'] ?? null, function ($q, $v) {
                $q->where(function ($sub) use ($v) {
                    $sub->where('expense_number', 'like', "%{$v}%")
                        ->orWhereHas('vendor', fn ($c) => $c->where('name', 'like', "%{$v}%"));
                });
            })
            ->latest('expense_date')
            ->latest('id')
            ->get();

        $filename = 'expenses-'.date('Y-m-d').'.csv';

        return response()->streamDownload(function () use ($orders) {
            $out = fopen('php://output', 'w');
            fputcsv($out, ['Date', 'Doc No', 'Status', 'Vendor', 'Category', 'Amount']);
            foreach ($orders as $order) {
                fputcsv($out, [
                    $order->expense_date->format('Y-m-d'),
                    $order->expense_number,
                    $order->statusMeta()['label'],
                    $order->partyName(),
                    $order->displayCategory(),
                    $order->grand_total,
                ]);
            }
            fclose($out);
        }, $filename, ['Content-Type' => 'text/csv']);
    }

    public function destroy(Expense $expense)
    {
        $this->authorizeOrder($expense);
        $expense->delete();

        return redirect()->route('admin.expenses.index')->with('success', 'Expense deleted successfully.');
    }

    public function storeCategory(Request $request): JsonResponse
    {
        $data = $request->validate([
            'name' => ['required', 'string', 'max:150'],
        ]);

        $category = ExpenseCategory::firstOrCreate(
            ['company_id' => $this->companyId(), 'name' => trim($data['name'])],
            ['is_active' => true]
        );

        return response()->json([
            'id' => $category->id,
            'name' => $category->name,
        ]);
    }

    private function authorizeOrder(Expense $expense): void
    {
        if ($expense->company_id !== $this->companyId()) {
            abort(403);
        }
    }

    private function formData(?Expense $expense, Request $request): array
    {
        $companyId = $this->companyId();
        $count = Expense::where('company_id', $companyId)->count();
        $defaultSeq = $count + ($expense ? 0 : 1);
        $defaultPrefix = config('expense.prefixes.default', 'EXP');

        $lineItems = [['item_name' => '', 'description' => '', 'unit' => 'NOS', 'quantity' => 1, 'rate' => 0, 'discount_percent' => 0, 'tax_rate' => 0, 'hsn_sac' => '']];

        if ($request->query('copy_from') && ! $expense) {
            $source = Expense::with('items')->forCompany($companyId)->find($request->query('copy_from'));
            if ($source) {
                $lineItems = $source->items->map(fn ($li) => [
                    'expense_category_id' => $li->expense_category_id,
                    'item_name' => $li->item_name,
                    'description' => $li->description,
                    'unit' => $li->unit,
                    'quantity' => (float) $li->quantity,
                    'rate' => (float) $li->rate,
                    'discount_percent' => (float) $li->discount_percent,
                    'tax_rate' => (float) $li->tax_rate,
                    'hsn_sac' => $li->hsn_sac,
                ])->values()->all();
                $expense = new Expense([
                    'vendor_id' => $source->vendor_id,
                    'customer_id' => $source->customer_id,
                    'payment_method' => $source->payment_method,
                    'place_of_supply' => $source->place_of_supply,
                    'tax_on' => $source->tax_on,
                    'terms' => $source->terms,
                    'private_notes' => $source->private_notes,
                    'has_shipping' => $source->has_shipping,
                    'has_discount_total' => $source->has_discount_total,
                    'has_discount_all' => $source->has_discount_all,
                    'has_custom_amount' => $source->has_custom_amount,
                    'has_total_quantity' => $source->has_total_quantity,
                    'show_cess' => $source->show_cess,
                    'reverse_charge' => $source->reverse_charge,
                ]);
            }
        }

        $expenseCategories = ExpenseCategory::pickerListForCompany($companyId);
        $taxRates = TaxRate::where('company_id', $companyId)->active()->selectable()->orderBy('rate')->get();
        $company = Company::find($companyId);
        $vendors = Vendor::where('company_id', $companyId)->where('is_active', true)->orderBy('name')->get();
        $customers = Customer::where('company_id', $companyId)->where('is_active', true)->orderBy('name')->get();

        return [
            'expense' => $expense,
            'company' => $company,
            'gstEnabled' => GstHelper::usesRegularGst($company),
            'indianStates' => config('company.indian_states', []),
            'paymentMethods' => config('expense.payment_methods'),
            'typeLabel' => 'Expense',
            'docLabels' => [
                'title' => ExpenseDocumentSettings::title($companyId),
                'number' => 'Number',
                'date' => 'Date',
            ],
            'vendors' => $vendors,
            'vendorsJson' => $vendors->map(fn ($v) => $v->toPickerArray())->values(),
            'customers' => $customers,
            'expenseCategories' => $expenseCategories,
            'expenseCategoriesJson' => $expenseCategories,
            'taxRates' => $taxRates,
            'shippingModes' => config('invoice.shipping_modes'),
            'discountAllTypes' => config('invoice.discount_all_types'),
            'defaultPrefix' => $expense?->expense_prefix ?? $defaultPrefix,
            'defaultSeq' => $expense?->expense_seq ?? $defaultSeq,
            'itemsJson' => collect($expenseCategories)->map(fn ($c) => [
                'id' => $c['id'],
                'name' => $c['name'],
                'unit' => 'NOS',
                'rate' => 0,
                'hsn_sac' => '',
                'tax_rate' => 0,
            ])->values(),
            'unitsCatalog' => UnitHelper::allForCompany($companyId),
            'unitsJson' => UnitHelper::jsonForCompany($companyId),
            'taxRatesJson' => $taxRates->map(fn ($t) => [
                'id' => $t->id,
                'name' => $t->name,
                'rate' => (float) $t->rate,
            ])->values(),
            'lineItems' => $expense && $expense->exists
                ? $expense->items->map(fn ($li) => [
                    'expense_category_id' => $li->expense_category_id,
                    'item_name' => $li->item_name,
                    'description' => $li->description,
                    'unit' => $li->unit,
                    'quantity' => (float) $li->quantity,
                    'rate' => (float) $li->rate,
                    'discount_percent' => (float) $li->discount_percent,
                    'tax_rate' => (float) $li->tax_rate,
                    'hsn_sac' => $li->hsn_sac,
                ])->values()->all()
                : $lineItems,
        ];
    }

    private function validateExpense(Request $request, ?Expense $expense = null): array
    {
        $paymentKeys = implode(',', array_keys(config('expense.payment_methods', [])));

        $data = $request->validate([
            'vendor_id' => ['required', 'exists:vendors,id'],
            'customer_id' => ['nullable', 'exists:customers,id'],
            'expense_prefix' => ['required', 'string', 'max:20'],
            'expense_seq' => ['required', 'integer', 'min:1'],
            'expense_date' => ['required', 'date'],
            'payment_method' => ['required', 'string', 'in:'.$paymentKeys],
            'terms' => ['nullable', 'string'],
            'private_notes' => ['nullable', 'string'],
            'notes' => ['nullable', 'string'],
            'shipping_charges' => ['nullable', 'numeric', 'min:0'],
            'shipping_mode' => ['nullable', 'string', 'max:30'],
            'discount_on_total' => ['nullable', 'numeric', 'min:0'],
            'discount_all_type' => ['nullable', 'in:percent,fixed'],
            'discount_all_value' => ['nullable', 'numeric', 'min:0'],
            'custom_amount' => ['nullable', 'numeric', 'min:0'],
            'custom_amount_label' => ['nullable', 'string', 'max:100'],
            'place_of_supply' => ['nullable', 'string', 'max:100'],
            'tax_on' => ['nullable', 'in:bill_to,ship_to'],
            'total_quantity_unit' => ['nullable', 'string', 'max:20'],
            'total_quantity' => ['nullable', 'numeric', 'min:0'],
        ]);

        $data['company_id'] = $this->companyId();
        $data['expense_number'] = trim($data['expense_prefix']).'-'.str_pad((string) $data['expense_seq'], 4, '0', STR_PAD_LEFT);
        $data['has_shipping'] = $request->boolean('has_shipping');
        $data['reverse_charge'] = $request->boolean('reverse_charge');
        $data['has_discount_total'] = $request->boolean('has_discount_total');
        $data['has_discount_all'] = $request->boolean('has_discount_all');
        $data['has_custom_amount'] = $request->boolean('has_custom_amount');
        $data['has_total_quantity'] = $request->boolean('has_total_quantity');
        $data['show_cess'] = $request->boolean('show_cess');

        if (! $data['has_shipping']) {
            $data['shipping_charges'] = 0;
            $data['shipping_mode'] = null;
        }
        if (! $data['has_discount_total']) {
            $data['discount_on_total'] = 0;
        }
        if (! $data['has_discount_all']) {
            $data['discount_all_value'] = 0;
            $data['discount_all_percent'] = 0;
        } else {
            $data['discount_all_type'] = $data['discount_all_type'] ?? 'percent';
            $data['discount_all_percent'] = ($data['discount_all_type'] === 'percent')
                ? (float) ($data['discount_all_value'] ?? 0)
                : 0;
        }
        if (! $data['has_custom_amount']) {
            $data['custom_amount'] = 0;
        }
        if (! $data['has_total_quantity']) {
            $data['total_quantity'] = null;
            $data['total_quantity_unit'] = null;
        }

        $data['expense_seq'] = (int) $data['expense_seq'];

        $exists = Expense::where('company_id', $this->companyId())
            ->where('expense_number', $data['expense_number'])
            ->when($expense, fn ($q) => $q->where('id', '!=', $expense->id))
            ->exists();

        if ($exists) {
            throw \Illuminate\Validation\ValidationException::withMessages([
                'expense_seq' => ['This document number already exists.'],
            ]);
        }

        return $data;
    }

    private function validateLineItems(Request $request): array
    {
        $request->validate([
            'line_items' => ['required', 'array', 'min:1'],
            'line_items.*.item_name' => ['required', 'string', 'max:255'],
            'line_items.*.quantity' => ['required', 'numeric', 'min:0.001'],
            'line_items.*.rate' => ['required', 'numeric', 'min:0'],
            'line_items.*.discount_percent' => ['nullable', 'numeric', 'min:0', 'max:100'],
            'line_items.*.tax_rate' => ['nullable', 'numeric', 'min:0', 'max:100'],
            'line_items.*.hsn_sac' => ['nullable', 'string', 'max:20'],
        ]);

        return collect($request->input('line_items', []))
            ->filter(fn ($row) => ! empty($row['item_name']))
            ->values()
            ->all();
    }

    private function calculateTotals(array $data, array $items): array
    {
        $data['company_id'] = $data['company_id'] ?? $this->companyId();
        $lineResult = $this->processGstLineItemsForVendor($data, $items);
        $processedItems = $lineResult['processed'];
        $subtotal = $lineResult['subtotal'];
        $gst = $lineResult['gst'];

        $discountAll = 0;
        if ($data['has_discount_all']) {
            $discVal = (float) ($data['discount_all_value'] ?? 0);
            $discountAll = ($data['discount_all_type'] ?? 'percent') === 'fixed'
                ? $discVal
                : $subtotal * ($discVal / 100);
        }

        $totalTax = $gst['total_tax'];
        $grand = $subtotal
            + $totalTax
            + (float) ($data['shipping_charges'] ?? 0)
            + (float) ($data['custom_amount'] ?? 0)
            - (float) ($data['discount_on_total'] ?? 0)
            - $discountAll;

        $grand = max(0, round($grand, 2));
        $roundOff = 0;

        if (CompanySettings::bool('round_off_amount', $data['company_id'])) {
            $rounded = round($grand);
            $roundOff = round($rounded - $grand, 2);
            $grand = $rounded;
        }

        return [
            'subtotal' => round($subtotal, 2),
            'taxable_amount' => round($subtotal, 2),
            'discount_amount' => round($discountAll + (float) ($data['discount_on_total'] ?? 0), 2),
            'total_tax' => $totalTax,
            'cgst_amount' => $gst['cgst_amount'],
            'sgst_amount' => $gst['sgst_amount'],
            'igst_amount' => $gst['igst_amount'],
            'place_of_supply' => $lineResult['place_of_supply'],
            'is_igst' => $lineResult['is_igst'],
            'tax_on' => $lineResult['tax_on'],
            'round_off' => $roundOff,
            'grand_total' => $grand,
            'items' => $processedItems,
        ];
    }

    private function syncLineItems(Expense $expense, array $items): void
    {
        foreach ($items as $row) {
            $categoryId = $row['expense_category_id'] ?? null;
            if (! $categoryId && ! empty($row['item_name'])) {
                $categoryId = ExpenseCategory::firstOrCreate(
                    ['company_id' => $expense->company_id, 'name' => $row['item_name']],
                    ['is_active' => true]
                )->id;
            }

            ExpenseItem::create([
                'expense_id' => $expense->id,
                'expense_category_id' => $categoryId,
                'item_name' => $row['item_name'],
                'description' => $row['description'] ?? null,
                'hsn_sac' => $row['hsn_sac'] ?? null,
                'unit' => $row['unit'] ?? 'NOS',
                'quantity' => (float) $row['quantity'],
                'rate' => (float) $row['rate'],
                'discount_percent' => (float) ($row['discount_percent'] ?? 0),
                'discount' => round((float) ($row['discount'] ?? 0), 2),
                'tax_rate' => (float) ($row['tax_rate'] ?? 0),
                'cgst_rate' => (float) ($row['cgst_rate'] ?? 0),
                'sgst_rate' => (float) ($row['sgst_rate'] ?? 0),
                'igst_rate' => (float) ($row['igst_rate'] ?? 0),
                'tax_amount' => round((float) ($row['tax_amount'] ?? 0), 2),
                'cgst_amount' => round((float) ($row['cgst_amount'] ?? 0), 2),
                'sgst_amount' => round((float) ($row['sgst_amount'] ?? 0), 2),
                'igst_amount' => round((float) ($row['igst_amount'] ?? 0), 2),
                'amount' => round((float) ($row['amount'] ?? 0), 2),
            ]);
        }
    }
}
