<?php

namespace App\Http\Controllers\Company\Accounting;

use App\Exports\VoucherListExport;
use App\Http\Controllers\Controller;
use App\Models\{
    Account, AuditLog, ChequeBook, Currency, Dimension, DimensionValue, FiscalYear, LedgerEntry,
    Setting, TaxCode, TransactionAttachment, Voucher, VoucherLine
};
use App\Services\{BooksClosingService, ChequeBookService, FiscalYearService, LedgerReportService, VoucherAttachmentService, VoucherListService, VoucherNumberService, VoucherPostingService};
use App\Support\QuickBooksChartOfAccounts;
use App\Support\VoucherPaymentMethods;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Maatwebsite\Excel\Facades\Excel;

class AccountingVouchersController extends Controller
{
    public function __construct(
        private readonly FiscalYearService $fiscalYears,
        private readonly BooksClosingService $booksClosing,
        private readonly LedgerReportService $ledgerReports,
        private readonly VoucherAttachmentService $attachments,
        private readonly VoucherListService $voucherList,
        private readonly ChequeBookService $chequeBooks,
    ) {
    }

    private const TYPES = [
        'cash_payment','bank_payment',
        'cash_receipt','bank_receipt',
        'contra','journal',
    ];

    // ── List ──────────────────────────────────────────────────────────────────
    public function index(Request $request, string $voucherType)
    {
        $this->validateType($voucherType);
        $this->gate('vouchers', 'view');

        return view('accounting.vouchers.index', $this->listViewData($request, $voucherType, true));
    }

    public function printList(Request $request, string $voucherType)
    {
        $this->validateType($voucherType);
        $this->gate('vouchers', 'view');

        $data = $this->listViewData($request, $voucherType, false);
        $companyName = auth()->user()->company?->name ?? config('app.name');
        $data = array_merge($data, $this->voucherList->reportHeader($data['filters'], $data['label'], $companyName));

        return view('accounting.vouchers.print', $data);
    }

    public function exportList(Request $request, string $voucherType)
    {
        $this->validateType($voucherType);
        $this->gate('vouchers', 'view');

        $data = $this->listViewData($request, $voucherType, false);
        $companyName = auth()->user()->company?->name ?? config('app.name');
        $header = $this->voucherList->reportHeader($data['filters'], $data['label'], $companyName);
        $slug = str_replace('_', '-', $voucherType);
        $filename = $slug . '-list-' . now()->format('Y-m-d-His') . '.xlsx';

        return Excel::download(
            new VoucherListExport(
                $data['rows'],
                $data['visibleColumns'],
                $data['columnLabels'],
                $header['listTitle'],
                $header['companyName'],
                $header['dateRangeLabel'],
            ),
            $filename,
        );
    }

    private function listViewData(Request $request, string $voucherType, bool $paginate): array
    {
        $companyId = auth()->user()->company_id;
        $filters = $this->voucherList->parseFilters($request, $voucherType);
        $filterOptions = $this->voucherList->filterOptions($companyId, $voucherType);

        if ($paginate) {
            $vouchers = $this->voucherList->paginate($companyId, $voucherType, $filters);
            $rows = $this->voucherList->enrichRows($vouchers, $filters, $companyId);
        } else {
            $vouchers = $this->voucherList->list($companyId, $voucherType, $filters);
            $rows = $this->voucherList->enrichCollection($vouchers, $filters, $companyId);
        }

        $label = VoucherNumberService::voucherTypes()[$voucherType];
        $approvalReq = Setting::get($companyId, 'voucher_approval_required', '1') === '1';
        $selectedColumnsLabel = $this->voucherList->selectedColumnsLabel($filters, $voucherType);
        $selectedPartyLabel = $this->voucherList->selectedPartyLabel($filters);
        $dimensionLabels = [];

        foreach ($filterOptions['dimensionTrees'] as $slot => $dimension) {
            $selected = $filters['dim'][$slot] ?? [];
            $dimensionLabels[$slot] = $selected === []
                ? 'All ' . $dimension['name']
                : $this->voucherList->selectedDimensionLabel($selected, 'All ' . $dimension['name']);
        }

        $columnLabels = $filterOptions['columns'];
        $visibleColumns = $filters['columns'];
        $recordLabel = match ($voucherType) {
            'cash_payment', 'bank_payment' => 'payment',
            'cash_receipt', 'bank_receipt' => 'receipt',
            'contra' => 'contra entry',
            'journal' => 'journal entry',
            default => 'voucher',
        };

        return compact(
            'vouchers',
            'rows',
            'voucherType',
            'label',
            'approvalReq',
            'filters',
            'filterOptions',
            'selectedColumnsLabel',
            'selectedPartyLabel',
            'dimensionLabels',
            'columnLabels',
            'visibleColumns',
            'recordLabel',
        );
    }

    // ── Create ────────────────────────────────────────────────────────────────
    public function create(string $voucherType)
    {
        $this->validateType($voucherType);
        $this->gate('vouchers', 'create');

        $companyId = auth()->user()->company_id;

        $fiscalYear = $this->fiscalYears->current($companyId);

        $preview = VoucherNumberService::preview($companyId, $voucherType);
        $approvalReq = Setting::get($companyId, 'voucher_approval_required', '1') === '1';

        return view('accounting.vouchers.create', array_merge(
            $this->formData($voucherType),
            compact('voucherType', 'fiscalYear', 'preview', 'approvalReq')
        ));
    }

    // ── Store ─────────────────────────────────────────────────────────────────
    public function store(Request $request, string $voucherType)
    {
        $this->validateType($voucherType);
        $this->gate('vouchers', 'create');

        $companyId = auth()->user()->company_id;

        $request->validate(array_merge([
            'voucher_date'       => 'required|date',
            'reference'          => 'nullable|string|max:100',
            'currency_id'        => 'nullable|integer|exists:currencies,id',
            'exchange_rate'      => 'nullable|numeric|min:0.000001',
            'narration'          => 'nullable|string|max:500',
            'lines'              => 'required|array|min:1',
            'lines.*.account_id' => 'required|integer|exists:accounts,id',
            'lines.*.dr_cr'      => 'nullable|in:dr,cr',
            'lines.*.debit'      => 'nullable|numeric|min:0',
            'lines.*.credit'     => 'nullable|numeric|min:0',
            'lines.*.amount'     => 'nullable|numeric|min:0',
            'lines.*.description'=> 'nullable|string|max:255',
            'lines.*.tax_code_id'=> 'nullable|integer|exists:tax_codes,id',
            'lines.*.party'      => 'nullable|string|max:100',
            'lines.*.dim1_id'    => 'nullable|integer',
            'lines.*.dim2_id'    => 'nullable|integer',
            'lines.*.dim3_id'    => 'nullable|integer',
            'lines.*.dim4_id'    => 'nullable|integer',
            'lines.*.dim5_id'    => 'nullable|integer',
            'lines.*.dim6_id'    => 'nullable|integer',
            'lines.*.dim7_id'    => 'nullable|integer',
            'attachments'        => 'nullable|array',
            'attachments.*'      => VoucherAttachmentService::fileRules(),
            'attachment_descriptions'   => 'nullable|array',
            'attachment_descriptions.*' => 'nullable|string|max:255',
        ], $this->voucherHeaderRules($voucherType)));

        if ($error = $this->validateVoucherAccountSelection($request, $companyId, $voucherType)) {
            return back()->withErrors(['lines' => $error])->withInput();
        }

        try {
            $this->validateChequeRegisterSelection($request, $companyId, $voucherType);
        } catch (\RuntimeException $e) {
            return back()->withErrors(['cheque_no' => $e->getMessage()])->withInput();
        }

        try {
            VoucherPostingService::assertNoNegativeCashBalanceForLines($companyId, $request->input('lines', []));
        } catch (\RuntimeException $e) {
            return back()->withErrors(['lines' => $e->getMessage()])->withInput();
        }

        try {
            $fiscalYear = $this->fiscalYears->requireOpenForDate($companyId, $request->voucher_date);
        } catch (\RuntimeException $e) {
            return back()->withErrors(['voucher_date' => $e->getMessage()])->withInput();
        }

        try {
            $this->booksClosing->assertCanTransact($companyId, $request->voucher_date, $request->input('closing_password'));
        } catch (\RuntimeException $e) {
            return back()->withErrors(['voucher_date' => $e->getMessage()])->withInput();
        }

        try {
            $voucher = DB::transaction(function () use ($request, $voucherType, $companyId, $fiscalYear) {

            $voucherNumber = VoucherNumberService::generate(
                $companyId,
                $voucherType,
                new \DateTime($request->voucher_date)
            );

            $voucher = Voucher::withoutGlobalScopes()->create([
                'company_id'     => $companyId,
                'fiscal_year_id' => $fiscalYear->id,
                'voucher_type'   => $voucherType,
                'voucher_number' => $voucherNumber,
                'voucher_date'   => $request->voucher_date,
                'reference'      => $request->reference,
                'party_id'       => $request->party_id ?: null,
                'party_type'     => $request->party_type ?: null,
                'currency_id'    => $request->currency_id ?: null,
                'exchange_rate'  => $request->exchange_rate ?: 1,
                'narration'      => $request->narration,
                'extra_data'     => $this->voucherExtraData($request, $voucherType),
                'status'         => 'draft',
                'created_by'     => auth()->id(),
            ]);

            $lineNum     = 1;
            $totalDebit  = 0;
            $totalCredit = 0;

            // FIX: Pre-load tax codes to avoid N+1 inside loop
            $taxCodeIds = collect($request->lines)
                ->pluck('tax_code_id')
                ->filter()
                ->unique()
                ->values();

            $taxCodeMap = TaxCode::withoutGlobalScopes()
                ->where('company_id', $companyId)
                ->whereIn('id', $taxCodeIds)
                ->get()
                ->keyBy('id');

            foreach ($request->lines as $lineData) {
                if (empty($lineData['account_id'])) continue;
                // Handle separate debit/credit columns
                $debit  = (float)($lineData['debit']  ?? 0);
                $credit = (float)($lineData['credit'] ?? 0);
                if ($debit > 0)       { $lineData['dr_cr'] = 'dr'; $lineData['amount'] = $debit; }
                elseif ($credit > 0)  { $lineData['dr_cr'] = 'cr'; $lineData['amount'] = $credit; }
                elseif (!empty($lineData['amount'])) { /* keep existing amount+dr_cr */ }
                else continue;

                $amount    = (float) ($lineData['amount'] ?? 0);
                if ($amount <= 0) continue;
                $taxAmount = 0;
                $taxRate   = 0;
                $taxCodeId = !empty($lineData['tax_code_id']) ? (int)$lineData['tax_code_id'] : null;

                // FIX: Reuse pre-loaded tax code — no second DB query
                if ($taxCodeId && isset($taxCodeMap[$taxCodeId])) {
                    $tax     = $taxCodeMap[$taxCodeId];
                    $taxRate = (float) $tax->rate;
                    if ($taxRate > 0) {
                        if ($tax->type === 'exclusive') {
                            $taxAmount = round($amount * $taxRate / 100, 2);
                        } else {
                            // Inclusive: tax is already inside the amount
                            $taxAmount = round($amount - ($amount / (1 + $taxRate / 100)), 2);
                        }
                    }
                }

                VoucherLine::withoutGlobalScopes()->create([
                    'company_id'  => $companyId,
                    'voucher_id'  => $voucher->id,
                    'line_number' => $lineNum++,
                    'account_id'  => (int) $lineData['account_id'],
                    'description' => $lineData['description'] ?? null,
                    'quantity'    => 1,
                    'unit_price'  => $amount,
                    'line_total'  => $amount,
                    'dr_cr'       => $lineData['dr_cr'],
                    'tax_code_id' => $taxCodeId,
                    'tax_rate'    => $taxRate,   // FIX: use pre-loaded value
                    'tax_amount'  => $taxAmount,
                    ...$this->linePartyFields($lineData),
                    'dim1_id'     => !empty($lineData['dim1_id']) ? (int) $lineData['dim1_id'] : null,
                    'dim2_id'     => !empty($lineData['dim2_id']) ? (int) $lineData['dim2_id'] : null,
                    'dim3_id'     => !empty($lineData['dim3_id']) ? (int) $lineData['dim3_id'] : null,
                    'dim4_id'     => !empty($lineData['dim4_id']) ? (int) $lineData['dim4_id'] : null,
                    'dim5_id'     => !empty($lineData['dim5_id']) ? (int) $lineData['dim5_id'] : null,
                    'dim6_id'     => !empty($lineData['dim6_id']) ? (int) $lineData['dim6_id'] : null,
                    'dim7_id'     => !empty($lineData['dim7_id']) ? (int) $lineData['dim7_id'] : null,
                ]);

                if (($lineData['dr_cr'] ?? 'dr') === 'dr') $totalDebit  += $amount;
                else                              $totalCredit += $amount;
            }

            $voucher->update([
                'total_amount' => max($totalDebit, $totalCredit),
                'grand_total'  => max($totalDebit, $totalCredit),
            ]);

            // Auto-post immediately when approval workflow is disabled.
            $approvalReq = Setting::get($companyId, 'voucher_approval_required', '1') === '1';
            if (!$approvalReq) {
                VoucherPostingService::post($voucher, $request->input('closing_password'));
            }

            return $voucher;
            });
        } catch (\RuntimeException $e) {
            return back()->withErrors(['lines' => $e->getMessage()])->withInput();
        }

        $this->attachments->storeMany(
            $voucher,
            $request->file('attachments', []),
            $request->input('attachment_descriptions', []),
        );

        AuditLog::record('create', 'vouchers', (string) $voucher->id, null,
            ['type' => $voucherType, 'number' => $voucher->voucher_number],
            "Voucher created: {$voucher->voucher_number}");

        $approvalReq = Setting::get($companyId, 'voucher_approval_required', '1') === '1';
        $successMessage = $approvalReq
            ? "Voucher {$voucher->voucher_number} saved as draft."
            : "Voucher {$voucher->voucher_number} saved and posted to ledger.";

        return $this->redirectAfterVoucherSave($request, $voucherType, $voucher, $successMessage);
    }

    // ── Show ──────────────────────────────────────────────────────────────────
    public function show(string $voucherType, Voucher $voucher)
    {
        $this->validateType($voucherType);
        $this->gate('vouchers', 'view');
        $this->sameCompany($voucher);

        $voucher->load(['lines.account','lines.taxCode','approvals.user',
                        'currency','fiscalYear','createdBy','approvedBy','postedBy',
                        'attachments.uploadedBy']);

        $companyId   = auth()->user()->company_id;
        $approvalReq = Setting::get($companyId, 'voucher_approval_required', '1');
        $label       = VoucherNumberService::voucherTypes()[$voucherType];

        // Safe access to extra_data
        $extraData    = $voucher->extra_data ?? [];
        $isReversed   = !empty($extraData['reversed_by']);
        $reversedById = $extraData['reversed_by'] ?? null;

        // Settings for print template
        $company  = auth()->user()->company;
        $settings = array_merge(\App\Models\Setting::defaults(),
            \App\Models\Setting::where('company_id', $companyId)
                ->get()->pluck('value','key')->toArray()
        );

        $partyLabel = match($voucherType) {
            "cash_payment","bank_payment" => "Payee Name",
            "cash_receipt","bank_receipt" => "Received From",
            default => null,
        };
        $partyName = null;
        if ($voucher->party_id && $voucher->party_type) {
            $tbl = ["customer"=>"customers","vendor"=>"vendors","employee"=>"employees","other_name"=>"other_names"];
            if (isset($tbl[$voucher->party_type])) {
                $partyName = \Illuminate\Support\Facades\DB::table($tbl[$voucher->party_type])->find($voucher->party_id)->name ?? null;
            }
        }
        return view("accounting.vouchers.show", compact(
            "voucher","voucherType","label","approvalReq",
            "isReversed","reversedById","company","settings",
            "partyName","partyLabel"
        ));
    }

    // ── Edit ──────────────────────────────────────────────────────────────────
    public function edit(string $voucherType, Voucher $voucher)
    {
        $this->validateType($voucherType);
        $this->gate('vouchers', 'edit');
        $this->sameCompany($voucher);

        if (!empty(($voucher->extra_data ?? [])['reversed_by'])) {
            return redirect()->route('vouchers.show', [$voucherType, $voucher])
                ->with('error', 'This voucher has been reversed and cannot be edited.');
        }

        $voucher->load(['lines.account','lines.taxCode','currency','attachments.uploadedBy']);
        $approvalReq = Setting::get($voucher->company_id, 'voucher_approval_required', '1') === '1';

        return view('accounting.vouchers.edit', array_merge(
            $this->formData($voucherType),
            compact('voucher', 'voucherType', 'approvalReq')
        ));
    }

    // ── Update ────────────────────────────────────────────────────────────────
    public function update(Request $request, string $voucherType, Voucher $voucher)
    {
        $this->validateType($voucherType);
        $this->gate('vouchers', 'edit');
        $this->sameCompany($voucher);

        if (!empty(($voucher->extra_data ?? [])['reversed_by'])) {
            return back()->with('error', 'This voucher has been reversed and cannot be edited.');
        }

        $companyId = auth()->user()->company_id;

        $request->validate(array_merge([
            'voucher_date'       => 'required|date',
            'reference'          => 'nullable|string|max:100',
            'currency_id'        => 'nullable|integer|exists:currencies,id',
            'exchange_rate'      => 'nullable|numeric|min:0.000001',
            'narration'          => 'nullable|string|max:500',
            'lines'              => 'required|array|min:1',
            'lines.*.account_id' => 'required|integer|exists:accounts,id',
            'lines.*.dr_cr'      => 'nullable|in:dr,cr',
            'lines.*.amount'     => 'nullable|numeric|min:0',
            'lines.*.description'=> 'nullable|string|max:255',
            'lines.*.tax_code_id'=> 'nullable|integer|exists:tax_codes,id',
            'lines.*.party'      => 'nullable|string|max:100',
            'lines.*.dim1_id'    => 'nullable|integer',
            'lines.*.dim2_id'    => 'nullable|integer',
            'lines.*.dim3_id'    => 'nullable|integer',
            'lines.*.dim4_id'    => 'nullable|integer',
            'lines.*.dim5_id'    => 'nullable|integer',
            'lines.*.dim6_id'    => 'nullable|integer',
            'lines.*.dim7_id'    => 'nullable|integer',
            'attachments'        => 'nullable|array',
            'attachments.*'      => VoucherAttachmentService::fileRules(),
            'attachment_descriptions'   => 'nullable|array',
            'attachment_descriptions.*' => 'nullable|string|max:255',
        ], $this->voucherHeaderRules($voucherType)));

        if ($error = $this->validateVoucherAccountSelection($request, $companyId, $voucherType)) {
            return back()->withErrors(['lines' => $error])->withInput();
        }

        try {
            $this->validateChequeRegisterSelection($request, $companyId, $voucherType, $voucher->id);
        } catch (\RuntimeException $e) {
            return back()->withErrors(['cheque_no' => $e->getMessage()])->withInput();
        }

        try {
            VoucherPostingService::assertNoNegativeCashBalanceForLines($companyId, $request->input('lines', []), $voucher->id);
        } catch (\RuntimeException $e) {
            return back()->withErrors(['lines' => $e->getMessage()])->withInput();
        }

        try {
            $this->booksClosing->assertCanTransact($companyId, $request->voucher_date, $request->input('closing_password'));
        } catch (\RuntimeException $e) {
            return back()->withErrors(['voucher_date' => $e->getMessage()])->withInput();
        }

        try {
            DB::transaction(function () use ($request, $voucher, $companyId, $voucherType) {

            $previousStatus = $voucher->status;
            $fiscalYear = $this->resolveFiscalYear($companyId, $request->voucher_date);
            $approvalReq = Setting::get($companyId, 'voucher_approval_required', '1') === '1';

            if (in_array($previousStatus, ['posted', 'approved', 'cancelled'], true)) {
                LedgerEntry::withoutGlobalScopes()
                    ->where('voucher_id', $voucher->id)
                    ->delete();
            }

            $voucher->update([
                'fiscal_year_id' => $fiscalYear?->id ?? $voucher->fiscal_year_id,
                'voucher_date'   => $request->voucher_date,
                'reference'      => $request->reference,
                'party_id'       => $request->party_id ?: null,
                'party_type'     => $request->party_type ?: null,
                'currency_id'    => $request->currency_id ?: null,
                'exchange_rate'  => $request->exchange_rate ?: 1,
                'narration'      => $request->narration,
                'extra_data'     => $this->voucherExtraData($request, $voucherType, $voucher->extra_data ?? []),
                'status'         => 'draft',
                'posted_by'      => null,
                'posted_at'      => null,
                'approved_by'    => null,
                'approved_at'    => null,
                'cancelled_by'   => null,
                'cancelled_at'   => null,
                'rejection_reason' => null,
            ]);

            // Delete old lines and recreate fresh
            VoucherLine::withoutGlobalScopes()
                ->where('voucher_id', $voucher->id)
                ->delete();

            $lineNum     = 1;
            $totalDebit  = 0;
            $totalCredit = 0;

            // FIX: Pre-load tax codes to avoid N+1
            $taxCodeIds = collect($request->lines)->pluck('tax_code_id')->filter()->unique()->values();
            $taxCodeMap = TaxCode::withoutGlobalScopes()
                ->where('company_id', $companyId)
                ->whereIn('id', $taxCodeIds)
                ->get()
                ->keyBy('id');

            foreach ($request->lines as $lineData) {
                if (empty($lineData['account_id'])) continue;
                // Handle separate debit/credit columns
                $debit  = (float)($lineData['debit']  ?? 0);
                $credit = (float)($lineData['credit'] ?? 0);
                if ($debit > 0)       { $lineData['dr_cr'] = 'dr'; $lineData['amount'] = $debit; }
                elseif ($credit > 0)  { $lineData['dr_cr'] = 'cr'; $lineData['amount'] = $credit; }
                elseif (!empty($lineData['amount'])) { /* keep existing amount+dr_cr */ }
                else continue;

                $amount    = (float) ($lineData['amount'] ?? 0);
                if ($amount <= 0) continue;
                $taxAmount = 0;
                $taxRate   = 0;
                $taxCodeId = !empty($lineData['tax_code_id']) ? (int)$lineData['tax_code_id'] : null;

                // FIX: use pre-loaded tax code
                if ($taxCodeId && isset($taxCodeMap[$taxCodeId])) {
                    $tax     = $taxCodeMap[$taxCodeId];
                    $taxRate = (float) $tax->rate;
                    if ($taxRate > 0) {
                        $taxAmount = $tax->type === 'exclusive'
                            ? round($amount * $taxRate / 100, 2)
                            : round($amount - ($amount / (1 + $taxRate / 100)), 2);
                    }
                }

                VoucherLine::withoutGlobalScopes()->create([
                    'company_id'  => $companyId,
                    'voucher_id'  => $voucher->id,
                    'line_number' => $lineNum++,
                    'account_id'  => (int) $lineData['account_id'],
                    'description' => $lineData['description'] ?? null,
                    'quantity'    => 1,
                    'unit_price'  => $amount,
                    'line_total'  => $amount,
                    'dr_cr'       => $lineData['dr_cr'],
                    'tax_code_id' => $taxCodeId,
                    'tax_rate'    => $taxRate,   // FIX: was hardcoded 0 before
                    'tax_amount'  => $taxAmount,
                    ...$this->linePartyFields($lineData),
                    'dim1_id'     => !empty($lineData['dim1_id']) ? (int) $lineData['dim1_id'] : null,
                    'dim2_id'     => !empty($lineData['dim2_id']) ? (int) $lineData['dim2_id'] : null,
                    'dim3_id'     => !empty($lineData['dim3_id']) ? (int) $lineData['dim3_id'] : null,
                    'dim4_id'     => !empty($lineData['dim4_id']) ? (int) $lineData['dim4_id'] : null,
                    'dim5_id'     => !empty($lineData['dim5_id']) ? (int) $lineData['dim5_id'] : null,
                    'dim6_id'     => !empty($lineData['dim6_id']) ? (int) $lineData['dim6_id'] : null,
                    'dim7_id'     => !empty($lineData['dim7_id']) ? (int) $lineData['dim7_id'] : null,
                ]);

                if (($lineData['dr_cr'] ?? 'dr') === 'dr') $totalDebit  += $amount;
                else                              $totalCredit += $amount;
            }

            $voucher->update([
                'total_amount' => max($totalDebit, $totalCredit),
                'grand_total'  => max($totalDebit, $totalCredit),
            ]);

            if (!$approvalReq) {
                VoucherPostingService::post($voucher, $request->input('closing_password'));
            }
            });
        } catch (\RuntimeException $e) {
            return back()->withErrors(['lines' => $e->getMessage()])->withInput();
        }

        $this->attachments->storeMany(
            $voucher,
            $request->file('attachments', []),
            $request->input('attachment_descriptions', []),
        );

        $approvalReq = Setting::get($companyId, 'voucher_approval_required', '1') === '1';
        $newStatus = $approvalReq ? 'draft' : 'posted';

        AuditLog::record('update', 'vouchers', (string) $voucher->id,
            null, ['status' => $newStatus],
            "Voucher updated: {$voucher->voucher_number}");

        $successMessage = $approvalReq
            ? 'Voucher updated and reset to Draft.'
            : 'Voucher updated and posted to ledger.';

        return $this->redirectAfterVoucherSave($request, $voucherType, $voucher, $successMessage);
    }

    // ── Workflow Actions ──────────────────────────────────────────────────────
    public function submit(Request $request, string $voucherType, Voucher $voucher)
    {
        $this->validateType($voucherType);
        $this->gate('vouchers', 'submit');
        $this->sameCompany($voucher);

        try {
            DB::transaction(fn() => VoucherPostingService::submit($voucher));
        } catch (\RuntimeException $e) {
            return back()->with('error', $e->getMessage());
        }

        return back()->with('success', 'Voucher submitted for approval.');
    }

    public function approve(Request $request, string $voucherType, Voucher $voucher)
    {
        $this->validateType($voucherType);
        $this->gate('vouchers', 'approve');
        $this->sameCompany($voucher);

        try {
            DB::transaction(fn() => VoucherPostingService::approve($voucher, $request->comments ?? ''));
        } catch (\RuntimeException $e) {
            return back()->with('error', $e->getMessage());
        }

        return back()->with('success', 'Voucher approved and posted to ledger.');
    }

    public function reject(Request $request, string $voucherType, Voucher $voucher)
    {
        $this->validateType($voucherType);
        $this->gate('vouchers', 'approve');
        $this->sameCompany($voucher);

        $request->validate(['reason' => 'required|string|max:500']);

        try {
            DB::transaction(fn() => VoucherPostingService::reject($voucher, $request->reason));
        } catch (\RuntimeException $e) {
            return back()->with('error', $e->getMessage());
        }

        return back()->with('success', 'Voucher rejected and returned to draft.');
    }

    public function post(Request $request, string $voucherType, Voucher $voucher)
    {
        $this->validateType($voucherType);
        $this->gate('vouchers', 'post');
        $this->sameCompany($voucher);

        $approvalReq = Setting::get($voucher->company_id, 'voucher_approval_required', '1');

        // FIX: Correct approval gate — if approval required, ONLY approved vouchers can be posted
        if ($approvalReq && !$voucher->isApproved()) {
            return back()->with('error',
                "Approval is required before posting. Current status: " . ucfirst($voucher->status) .
                ". Please submit the voucher for approval first."
            );
        }

        try {
            $closingPassword = $request->input('closing_password');
            DB::transaction(fn() => VoucherPostingService::post($voucher, $closingPassword));
        } catch (\RuntimeException $e) {
            return back()->with('error', $e->getMessage());
        }

        return back()->with('success', "Voucher {$voucher->voucher_number} posted to ledger successfully.");
    }

    public function cancel(Request $request, string $voucherType, Voucher $voucher)
    {
        $this->validateType($voucherType);
        $this->gate('vouchers', 'cancel');
        $this->sameCompany($voucher);

        $request->validate(['reason' => 'required|string|max:500']);

        try {
            DB::transaction(fn() => VoucherPostingService::cancel($voucher, $request->reason));
        } catch (\RuntimeException $e) {
            return back()->with('error', $e->getMessage());
        }

        return back()->with('success', 'Voucher cancelled.');
    }

    public function reverse(Request $request, string $voucherType, Voucher $voucher)
    {
        $this->validateType($voucherType);
        $this->gate('vouchers', 'post');
        $this->sameCompany($voucher);

        $request->validate([
            'reason'        => 'required|string|max:500',
            'reversal_date' => 'required|date',
        ]);

        try {
            $reversal = DB::transaction(fn() =>
                VoucherPostingService::reverse($voucher, $request->reason, $request->reversal_date)
            );
        } catch (\RuntimeException $e) {
            return back()->with('error', $e->getMessage());
        }

        return redirect()->route('vouchers.show', [$voucherType, $reversal])
            ->with('success', "Reversal voucher {$reversal->voucher_number} created.");
    }

    public function storeAttachment(Request $request, string $voucherType, Voucher $voucher)
    {
        $this->validateType($voucherType);
        $this->sameCompany($voucher);

        if (!$this->attachments->canUpload(auth()->user(), $voucher)) {
            abort(403);
        }

        $request->validate([
            'attachments'   => 'required|array|min:1',
            'attachments.*' => VoucherAttachmentService::fileRules(true),
            'attachment_descriptions'   => 'nullable|array',
            'attachment_descriptions.*' => 'nullable|string|max:255',
        ]);

        $created = $this->attachments->storeMany(
            $voucher,
            $request->file('attachments', []),
            $request->input('attachment_descriptions', []),
        );

        if ($request->expectsJson()) {
            return response()->json([
                'message' => count($created) . ' document(s) uploaded.',
                'attachments' => collect($created)->load('uploadedBy')->map(fn (TransactionAttachment $attachment) => [
                    'id' => $attachment->id,
                    'file_name' => $attachment->file_name,
                    'file_size_human' => $attachment->file_size_human,
                    'description' => $attachment->description,
                    'uploaded_by' => $attachment->uploadedBy?->name,
                    'created_at' => $attachment->created_at?->format('d M Y H:i'),
                    'download_url' => route('vouchers.attachments.download', [$voucherType, $voucher, $attachment]),
                    'delete_url' => route('vouchers.attachments.destroy', [$voucherType, $voucher, $attachment]),
                    'is_image' => $attachment->isImage(),
                    'is_pdf' => $attachment->isPdf(),
                ]),
            ]);
        }

        return back()->with('success', count($created) . ' document(s) uploaded.');
    }

    public function downloadAttachment(string $voucherType, Voucher $voucher, TransactionAttachment $attachment)
    {
        $this->validateType($voucherType);
        $this->gate('vouchers', 'view');
        $this->sameCompany($voucher);
        $this->assertAttachmentBelongsToVoucher($voucher, $attachment);

        return $this->attachments->download($attachment);
    }

    public function destroyAttachment(Request $request, string $voucherType, Voucher $voucher, TransactionAttachment $attachment)
    {
        $this->validateType($voucherType);
        $this->sameCompany($voucher);
        $this->assertAttachmentBelongsToVoucher($voucher, $attachment);

        if (!$this->attachments->canDelete(auth()->user(), $attachment)) {
            abort(403);
        }

        $this->attachments->delete($attachment);

        if ($request->expectsJson()) {
            return response()->json(['message' => 'Document removed.']);
        }

        return back()->with('success', 'Document removed.');
    }

    public function destroy(string $voucherType, Voucher $voucher)
    {
        $this->validateType($voucherType);
        $this->gate('vouchers', 'delete');
        $this->sameCompany($voucher);

        $number = $voucher->voucher_number ?: '(Draft #' . $voucher->id . ')';

        try {
            DB::transaction(fn () => VoucherPostingService::hardDelete($voucher));
        } catch (\RuntimeException $e) {
            return back()->with('error', $e->getMessage());
        }

        return redirect()->route('vouchers.index', $voucherType)
            ->with('success', "Voucher {$number} permanently deleted.");
    }

    // ── Helpers ───────────────────────────────────────────────────────────────

    // ── Party Search (AJAX) ───────────────────────────────────────────────────
    public function partySearch(\Illuminate\Http\Request $request)
    {
        $companyId = auth()->user()->company_id;
        $q         = trim($request->input('q', ''));
        $results   = [];
        $like      = '%' . $q . '%';

        // Helper to query any table directly
        $search = function(string $table, string $type, string $partyType) use ($companyId, $like, &$results) {
            $rows = \Illuminate\Support\Facades\DB::table($table)
                ->where('company_id', $companyId)
                ->whereNull('deleted_at')
                ->where('is_active', 1)
                ->where(function($x) use ($like) {
                    $x->where('name', 'like', $like)
                      ->orWhere('code', 'like', $like);
                })
                ->orderBy('name')
                ->limit(15)
                ->get(['id','name','code']);

            foreach ($rows as $r) {
                $results[] = [
                    'id'         => $partyType . '_' . $r->id,
                    'text'       => $r->name,
                    'code'       => $r->code ?? '',
                    'type'       => $type,
                    'party_id'   => $r->id,
                    'party_type' => $partyType,
                ];
            }
        };

        $search('customers',   'Customer',   'customer');
        $search('vendors',     'Vendor',     'vendor');
        $search('employees',   'Employee',   'employee');
        $search('other_names', 'Other Name', 'other_name');

        return response()->json(['results' => $results]);
    }

    private function formData(string $voucherType): array
    {
        $companyId = auth()->user()->company_id;

        $accounts = $this->voucherAccountQuery($companyId, $voucherType)
            ->orderBy('code')
            ->get(['id','code','name','type','is_bank','is_cash']);

        $accountTree = $this->buildVoucherAccountTree($companyId, $voucherType);

        $currencies = Currency::withoutGlobalScopes()
            ->where('company_id', $companyId)
            ->where('is_active', true)
            ->orderBy('code')
            ->get(['id','code','symbol']);

        $taxCodes = TaxCode::withoutGlobalScopes()
            ->where('company_id', $companyId)
            ->where('is_active', true)
            ->orderBy('code')
            ->get(['id','code','name','rate','type']);

        $dimensions = Dimension::withoutGlobalScopes()
            ->where('company_id', $companyId)
            ->where('is_active', true)
            ->orderBy('slot')
            ->get();

        $dimensionTrees = [];

        if ($dimensions->isNotEmpty()) {
            $values = DimensionValue::withoutGlobalScopes()
                ->where('company_id', $companyId)
                ->where('is_active', true)
                ->whereNull('deleted_at')
                ->whereIn('dimension_id', $dimensions->pluck('id'))
                ->orderBy('sort_order')
                ->orderBy('name')
                ->get();

            foreach ($dimensions as $dimension) {
                $dimensionTrees[$dimension->slot] = $this->buildVoucherDimensionValueTree(
                    $values->where('dimension_id', $dimension->id)->values(),
                );
            }
        }

        $label = VoucherNumberService::voucherTypes()[$voucherType];
        $partyTree = $this->ledgerReports->profitLossPartyTree($companyId);
        $paymentMethods = VoucherPaymentMethods::options();
        $useChequeRegister = Setting::get($companyId, 'use_cheque_register', '0') === '1';
        $bankAccountIds = $accounts->where('is_bank', true)->where('is_cash', false)->pluck('id')->values();

        return compact('accounts', 'currencies', 'taxCodes', 'dimensions', 'dimensionTrees', 'partyTree', 'accountTree', 'label', 'paymentMethods', 'useChequeRegister', 'bankAccountIds');
    }

    private function buildVoucherAccountTree(int $companyId, string $voucherType): array
    {
        $allowedIds = $this->voucherAccountQuery($companyId, $voucherType)
            ->pluck('id')
            ->map(fn ($id) => (int) $id)
            ->flip();

        $allAccounts = Account::withoutGlobalScopes()
            ->where('company_id', $companyId)
            ->where('is_active', true)
            ->whereNull('deleted_at')
            ->orderBy('code')
            ->orderBy('name')
            ->get(['id', 'parent_id', 'code', 'name', 'type', 'sub_type', 'detail_type', 'is_group', 'is_bank', 'is_cash', 'sort_order']);

        $neededIds = collect();
        foreach ($allAccounts->whereIn('id', $allowedIds->keys()) as $leaf) {
            $currentId = $leaf->id;
            while ($currentId) {
                $neededIds->put($currentId, true);
                $parentId = $allAccounts->firstWhere('id', $currentId)?->parent_id;
                $currentId = $parentId ? (int) $parentId : null;
            }
        }

        $filtered = $allAccounts
            ->filter(fn (Account $account) => $neededIds->has($account->id))
            ->each(function (Account $account) {
                $account->qb_section = QuickBooksChartOfAccounts::sectionForAccount($account);
                $account->qb_account_type = QuickBooksChartOfAccounts::accountTypeLabel($account);
            });

        $tree = [];

        foreach (QuickBooksChartOfAccounts::sections() as $sectionKey => $sectionMeta) {
            $sectionAccounts = $filtered->filter(
                fn (Account $account) => $account->qb_section === $sectionKey,
            );

            if ($sectionAccounts->isEmpty()) {
                continue;
            }

            $children = $this->buildVoucherAccountSectionNodes($sectionAccounts, null, $allowedIds);

            if ($children === []) {
                continue;
            }

            $tree[] = [
                'id' => 'section:' . $sectionKey,
                'name' => $sectionMeta['label'],
                'code' => '',
                'type_label' => $sectionMeta['label'],
                'selectable' => false,
                'value' => null,
                'children' => $children,
            ];
        }

        return $tree;
    }

    private function buildVoucherAccountSectionNodes($sectionAccounts, ?int $parentId, \Illuminate\Support\Collection $allowedIds): array
    {
        $sectionIds = $sectionAccounts->pluck('id')->flip();
        $nodes = [];

        $siblings = $sectionAccounts
            ->when(
                $parentId === null,
                fn ($accounts) => $accounts->filter(
                    fn (Account $account) => $account->parent_id === null || !$sectionIds->has($account->parent_id),
                ),
                fn ($accounts) => $accounts->where('parent_id', $parentId),
            )
            ->sortBy(fn (Account $account) => sprintf(
                '%06d|%s|%s',
                (int) $account->sort_order,
                $account->code ?? '',
                strtolower($account->name),
            ));

        foreach ($siblings as $account) {
            $children = $this->buildVoucherAccountSectionNodes($sectionAccounts, $account->id, $allowedIds);
            $isSelectable = !$account->is_group && $allowedIds->has($account->id);

            if ($account->is_group && $children === []) {
                continue;
            }

            if (!$account->is_group && !$isSelectable) {
                continue;
            }

            $nodes[] = [
                'id' => ($account->is_group ? 'group:' : 'account:') . $account->id,
                'name' => $account->name,
                'code' => $account->code ?? '',
                'type_label' => $account->qb_account_type ?? QuickBooksChartOfAccounts::accountTypeLabel($account),
                'selectable' => $isSelectable,
                'value' => $isSelectable ? (string) $account->id : null,
                'children' => $children,
            ];
        }

        return $nodes;
    }

    private function linePartyFields(array $lineData): array
    {
        $party = $lineData['party'] ?? null;
        if (!is_string($party) || !str_contains($party, ':')) {
            return ['party_id' => null, 'party_type' => null];
        }

        [$partyType, $partyId] = explode(':', $party, 2);
        $allowed = ['customer', 'vendor', 'employee', 'other_name'];

        if (!in_array($partyType, $allowed, true) || (int) $partyId <= 0) {
            return ['party_id' => null, 'party_type' => null];
        }

        return [
            'party_id' => (int) $partyId,
            'party_type' => $partyType,
        ];
    }

    private function buildVoucherDimensionValueTree($values, ?int $parentId = null): array
    {
        $nodes = [];

        foreach ($values->where('parent_id', $parentId) as $value) {
            $nodes[] = [
                'id' => $value->id,
                'name' => $value->name,
                'code' => $value->code,
                'children' => $this->buildVoucherDimensionValueTree($values, $value->id),
            ];
        }

        return $nodes;
    }


    private function voucherAccountQuery(int $companyId, string $voucherType)
    {
        $query = Account::withoutGlobalScopes()
            ->where('company_id', $companyId)
            ->where('is_group', false)
            ->where('is_active', true)
            ->whereNull('deleted_at');

        return match ($voucherType) {
            'cash_payment', 'cash_receipt' => $query->where('is_bank', false),
            'bank_payment', 'bank_receipt' => $query->where('is_cash', false),
            'contra' => $query->where(fn($q) => $q->where('is_bank', true)->orWhere('is_cash', true)),
            default => $query,
        };
    }

    private function validateVoucherAccountSelection(Request $request, int $companyId, string $voucherType): ?string
    {
        $accountIds = collect($request->input('lines', []))
            ->pluck('account_id')
            ->filter()
            ->map(fn($id) => (int) $id)
            ->unique()
            ->values();

        if ($accountIds->isEmpty()) {
            return null;
        }

        $allowedIds = $this->voucherAccountQuery($companyId, $voucherType)
            ->whereIn('id', $accountIds)
            ->pluck('id')
            ->map(fn($id) => (int) $id);

        if ($accountIds->diff($allowedIds)->isEmpty()) {
            return null;
        }

        return match ($voucherType) {
            'cash_payment', 'cash_receipt' => 'Cash Payment and Cash Receipt vouchers cannot use Bank Account ledgers.',
            'bank_payment', 'bank_receipt' => 'Bank Payment and Bank Receipt vouchers cannot use Cash Account ledgers.',
            'contra' => 'Contra vouchers can use only Bank Account or Cash Account ledgers.',
            default => 'One or more selected accounts are not allowed for this voucher type.',
        };
    }

    private function resolveFiscalYear(int $companyId, string $date): ?FiscalYear
    {
        return $this->fiscalYears->resolveForDate($companyId, $date);
    }

    private function voucherHeaderRules(string $voucherType): array
    {
        $rules = [];

        if (in_array($voucherType, ['bank_payment', 'bank_receipt', 'contra'], true)) {
            $rules['cheque_no'] = 'nullable|string|max:50';
            $rules['cheque_date'] = 'nullable|date';
        }

        if (VoucherPaymentMethods::appliesTo($voucherType)) {
            $rules['payment_method'] = 'nullable|in:' . implode(',', array_keys(VoucherPaymentMethods::options()));
        }

        return $rules;
    }

    private function voucherExtraData(Request $request, string $voucherType, array $existing = []): array
    {
        $extra = $existing;

        if (in_array($voucherType, ['bank_payment', 'bank_receipt', 'contra'], true)) {
            $extra['cheque_no'] = $request->input('cheque_no') ?: null;
            $extra['cheque_date'] = $request->input('cheque_date') ?: null;

            if (!empty($extra['cheque_no']) && ($extra['cheque_status'] ?? '') !== 'void') {
                $extra['cheque_status'] = 'issued';
            }
        }

        if (VoucherPaymentMethods::appliesTo($voucherType)) {
            $method = $request->input('payment_method');
            if ($method) {
                $extra['payment_method'] = $method;
            } else {
                unset($extra['payment_method']);
            }

            if ($method === 'cheque' && $request->filled('cheque_book_id')) {
                $book = ChequeBook::withoutGlobalScopes()
                    ->where('company_id', auth()->user()->company_id)
                    ->find((int) $request->input('cheque_book_id'));

                if ($book) {
                    $extra['cheque_book_id'] = $book->id;
                    $extra['cheque_bank'] = $book->bank;
                    $extra['cheque_branch'] = $book->branch;
                    $extra['cheque_book_label'] = trim($book->bank . ' # ' . $book->book_no);
                }
            } else {
                unset($extra['cheque_book_id'], $extra['cheque_bank'], $extra['cheque_branch'], $extra['cheque_book_label']);
            }
        }

        return $extra;
    }

    private function validateChequeRegisterSelection(
        Request $request,
        int $companyId,
        string $voucherType,
        ?int $voucherId = null,
    ): void {
        if (!in_array($voucherType, ['bank_payment', 'contra'], true)) {
            return;
        }

        if (Setting::get($companyId, 'use_cheque_register', '0') !== '1') {
            return;
        }

        if ($request->input('payment_method') !== 'cheque') {
            return;
        }

        $bankAccountId = $this->bankCreditAccountIdFromLines($request->input('lines', []), $companyId);
        if (!$bankAccountId) {
            throw new \RuntimeException('Select a Bank Account ledger on the credit line when payment method is Cheque.');
        }

        $bookId = (int) $request->input('cheque_book_id');
        if ($bookId <= 0) {
            throw new \RuntimeException('Select a cheque book for the bank account.');
        }

        $book = ChequeBook::withoutGlobalScopes()
            ->where('company_id', $companyId)
            ->where('account_id', $bankAccountId)
            ->find($bookId);

        if (!$book) {
            throw new \RuntimeException('Selected cheque book does not belong to the credited bank account.');
        }

        $this->chequeBooks->assertChequeNumberAvailable(
            $companyId,
            $book,
            (string) $request->input('cheque_no', ''),
            $voucherId,
        );
    }

    private function bankCreditAccountIdFromLines(array $lines, int $companyId): ?int
    {
        foreach ($lines as $line) {
            $credit = (float) ($line['credit'] ?? 0);
            if ($credit <= 0 || empty($line['account_id'])) {
                continue;
            }

            $account = Account::withoutGlobalScopes()
                ->where('company_id', $companyId)
                ->where('id', (int) $line['account_id'])
                ->first();

            if ($account && $account->is_bank && !$account->is_cash) {
                return (int) $account->id;
            }
        }

        return null;
    }

    private function validateType(string $type): void
    {
        if (!in_array($type, self::TYPES)) {
            abort(404, "Invalid voucher type: {$type}");
        }
    }

    private function gate(string $module, string $action): void
    {
        if (!auth()->user()->hasPermission($module, $action)) abort(403);
    }

    private function sameCompany(Voucher $voucher): void
    {
        if ($voucher->company_id !== auth()->user()->company_id) abort(403);
    }

    private function assertAttachmentBelongsToVoucher(Voucher $voucher, TransactionAttachment $attachment): void
    {
        if ((int) $attachment->company_id !== (int) $voucher->company_id
            || $attachment->attachable_type !== $voucher->getMorphClass()
            || (int) $attachment->attachable_id !== (int) $voucher->id) {
            abort(404);
        }
    }

    private function redirectAfterVoucherSave(Request $request, string $voucherType, Voucher $voucher, string $successMessage)
    {
        if ($request->input('save_action') === 'save_and_new') {
            return redirect()->route('vouchers.create', $voucherType)
                ->with('success', $successMessage);
        }

        return redirect()->route('vouchers.show', [$voucherType, $voucher])
            ->with('success', $successMessage);
    }
}
