<?php

namespace App\Http\Controllers\Admin;

use App\Helpers\PaymentDocumentSettings;
use App\Models\Customer;
use App\Models\Invoice;
use App\Models\Payment;
use App\Models\PaymentAllocation;
use App\Models\PurchaseBill;
use App\Models\Vendor;
use App\Services\PaymentDocumentPresenter;
use App\Services\PaymentPdfGenerator;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Symfony\Component\HttpFoundation\StreamedResponse;

class PaymentController extends AdminController
{
    public function __construct(
        private readonly PaymentDocumentPresenter $documentPresenter,
    ) {}

    public function index(Request $request)
    {
        $companyId = $this->companyId();
        $filters = $request->only([
            'q', 'kind', 'account_name', 'payment_number', 'method',
            'date_from', 'date_to', 'amount_min', 'amount_max',
        ]);

        $query = Payment::with(['customer', 'vendor', 'allocations'])
            ->forCompany($companyId)
            ->when($filters['kind'] ?? null, fn ($q, $v) => $v !== 'all' ? $q->where('kind', $v) : $q)
            ->when($filters['payment_number'] ?? null, fn ($q, $v) => $q->where('payment_number', 'like', "%{$v}%"))
            ->when($filters['method'] ?? null, fn ($q, $v) => $q->where('payment_method', $v))
            ->when($filters['date_from'] ?? null, fn ($q, $v) => $q->whereDate('payment_date', '>=', $v))
            ->when($filters['date_to'] ?? null, fn ($q, $v) => $q->whereDate('payment_date', '<=', $v))
            ->when($filters['amount_min'] ?? null, fn ($q, $v) => $q->where('amount', '>=', $v))
            ->when($filters['amount_max'] ?? null, fn ($q, $v) => $q->where('amount', '<=', $v))
            ->when($filters['account_name'] ?? null, function ($q, $v) {
                $q->where(function ($sub) use ($v) {
                    $sub->whereHas('customer', fn ($c) => $c->where('name', 'like', "%{$v}%"))
                        ->orWhereHas('vendor', fn ($c) => $c->where('name', 'like', "%{$v}%"));
                });
            })
            ->when($filters['q'] ?? null, function ($q, $v) {
                $q->where(function ($sub) use ($v) {
                    $sub->where('payment_number', 'like', "%{$v}%")
                        ->orWhere('transaction_ref', 'like', "%{$v}%")
                        ->orWhereHas('customer', fn ($c) => $c->where('name', 'like', "%{$v}%"))
                        ->orWhereHas('vendor', fn ($c) => $c->where('name', 'like', "%{$v}%"));
                });
            });

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

        $paymentId = $request->query('payment');
        $selected = $paymentId ? $payments->firstWhere('id', (int) $paymentId) : null;

        if ($paymentId && ! $selected) {
            $selected = Payment::with(['customer', 'vendor', 'allocations'])
                ->forCompany($companyId)
                ->find($paymentId);
            if ($selected) {
                $payments = $payments->push($selected)
                    ->sortByDesc(fn ($p) => $p->payment_date->format('Y-m-d').'-'.$p->id)
                    ->values();
            }
        }

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

        return view('admin.payments.index', array_merge([
            'payments' => $payments,
            'filters' => $filters,
            'hasFilters' => $hasFilters,
            'kinds' => config('payment.kinds'),
            'paymentMethods' => config('payment.payment_methods'),
            'features' => config('payment.features'),
            'selected' => $selected,
            'panelOpen' => (bool) ($paymentId && $selected),
        ], $panelData));
    }

    public function create(Request $request)
    {
        $kind = $this->resolveKind($request->query('kind', 'received'));

        return view('admin.payments.create', $this->formData(null, $kind));
    }

    public function store(Request $request)
    {
        $data = $this->validatePayment($request);
        $allocations = $this->validateAllocations($request, $data['kind']);

        $payment = null;
        DB::transaction(function () use ($data, $allocations, &$payment) {
            $payment = Payment::create($data);
            $this->syncAllocations($payment, $allocations);
            $this->refreshPaymentTotals($payment);
        });

        if ($request->input('save_action') === 'preview') {
            return redirect()->route('admin.payments.index', ['payment' => $payment->id])
                ->with('success', 'Payment recorded successfully.');
        }

        return redirect()->route('admin.payments.index')
            ->with('success', 'Payment saved successfully.');
    }

    public function edit(Payment $payment)
    {
        $this->authorizePayment($payment);
        $payment->load(['allocations']);

        return view('admin.payments.edit', $this->formData($payment, $payment->kind));
    }

    public function update(Request $request, Payment $payment)
    {
        $this->authorizePayment($payment);

        $data = $this->validatePayment($request, $payment);
        $allocations = $this->validateAllocations($request, $data['kind']);

        DB::transaction(function () use ($payment, $data, $allocations) {
            $this->revertAllocations($payment);
            $payment->update($data);
            $payment->allocations()->delete();
            $this->syncAllocations($payment, $allocations);
            $this->refreshPaymentTotals($payment);
        });

        if ($request->input('save_action') === 'preview') {
            return redirect()->route('admin.payments.index', ['payment' => $payment->id])
                ->with('success', 'Payment updated successfully.');
        }

        return redirect()->route('admin.payments.index')
            ->with('success', 'Payment updated successfully.');
    }

    public function destroy(Payment $payment)
    {
        $this->authorizePayment($payment);

        DB::transaction(function () use ($payment) {
            $this->revertAllocations($payment);
            $payment->delete();
        });

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

    public function show(Payment $payment)
    {
        return redirect()->route('admin.payments.index', ['payment' => $payment->id]);
    }

    public function print(Payment $payment, Request $request)
    {
        $this->authorizePayment($payment);

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

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

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

        return $pdfGenerator->download($payment, $data, $copyLabel);
    }

    public function shareLink(Payment $payment): JsonResponse
    {
        $this->authorizePayment($payment);

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

    public function unpaidDocuments(Request $request): JsonResponse
    {
        $companyId = $this->companyId();
        $kind = $this->resolveKind($request->query('kind', 'received'));
        $months = (int) $request->query('months', 6);

        if (in_array($kind, ['received', 'advance_received'], true)) {
            $customerId = (int) $request->query('customer_id');
            if (! $customerId) {
                return response()->json(['documents' => []]);
            }

            $from = now()->subMonths(max(1, $months))->startOfDay();

            $docs = Invoice::forCompany($companyId)
                ->where('customer_id', $customerId)
                ->whereIn('status', ['sent', 'partial', 'unpaid', 'overdue'])
                ->whereDate('invoice_date', '>=', $from)
                ->orderByDesc('invoice_date')
                ->get()
                ->filter(fn ($inv) => $inv->balance > 0.001)
                ->values()
                ->map(fn ($inv) => [
                    'id' => $inv->id,
                    'type' => 'invoice',
                    'date' => $inv->invoice_date->format('Y-m-d'),
                    'number' => $inv->invoice_number,
                    'amount' => (float) $inv->grand_total,
                    'amount_due' => (float) $inv->balance,
                ]);
        } else {
            $vendorId = (int) $request->query('vendor_id');
            if (! $vendorId) {
                return response()->json(['documents' => []]);
            }

            $from = now()->subMonths(max(1, $months))->startOfDay();

            $docs = PurchaseBill::forCompany($companyId)
                ->where('vendor_id', $vendorId)
                ->whereIn('status', ['open', 'partial', 'overdue'])
                ->whereDate('bill_date', '>=', $from)
                ->orderByDesc('bill_date')
                ->get()
                ->map(function ($bill) {
                    $due = max(0, (float) $bill->grand_total - (float) $bill->paid_amount);

                    return ['bill' => $bill, 'due' => $due];
                })
                ->filter(fn ($row) => $row['due'] > 0.001)
                ->values()
                ->map(fn ($row) => [
                    'id' => $row['bill']->id,
                    'type' => 'purchase_bill',
                    'date' => $row['bill']->bill_date->format('Y-m-d'),
                    'number' => $row['bill']->bill_number,
                    'amount' => (float) $row['bill']->grand_total,
                    'amount_due' => $row['due'],
                ]);
        }

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

    public function export(Request $request): StreamedResponse
    {
        $companyId = $this->companyId();
        $payments = Payment::with(['customer', 'vendor'])
            ->forCompany($companyId)
            ->latest('payment_date')
            ->get();

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

        return response()->streamDownload(function () use ($payments) {
            $out = fopen('php://output', 'w');
            fputcsv($out, ['Date', 'Number', 'Type', 'Account', 'Document #', 'Method', 'Amount', 'Available Credit', 'Notes']);

            foreach ($payments as $p) {
                fputcsv($out, [
                    $p->payment_date->format('Y-m-d'),
                    $p->payment_number,
                    $p->kindMeta()['label'] ?? $p->kind,
                    $p->partyName(),
                    $p->primaryDocumentNumber(),
                    $p->paymentMethodLabel(),
                    $p->amount,
                    $p->available_credit,
                    $p->notes,
                ]);
            }
            fclose($out);
        }, $filename, ['Content-Type' => 'text/csv']);
    }

    private function authorizePayment(Payment $payment): void
    {
        if ($payment->company_id !== $this->companyId()) {
            abort(403);
        }
    }

    private function resolveKind(?string $kind): string
    {
        $kinds = array_keys(config('payment.kinds', []));

        return in_array($kind, $kinds, true) ? $kind : 'received';
    }

    /**
     * @return array<string, mixed>
     */
    private function formData(?Payment $payment, string $kind): array
    {
        $companyId = $this->companyId();
        $count = Payment::where('company_id', $companyId)->count();
        $defaultSeq = $payment?->payment_seq ?? ($count + 1);
        $defaultPrefix = $payment?->payment_prefix ?? config('payment.prefixes.default', '');

        $kindMeta = config('payment.kinds.'.$kind, config('payment.kinds.received'));
        $isReceived = in_array($kind, ['received', 'advance_received'], true);
        $isAdvance = in_array($kind, ['advance_received', 'advance_made'], true);

        $allocations = [];
        if ($payment) {
            $allocations = $payment->allocations->map(fn ($a) => [
                'invoice_id' => $a->invoice_id,
                'purchase_bill_id' => $a->purchase_bill_id,
                'document_number' => $a->document_number,
                'document_date' => $a->document_date?->format('Y-m-d'),
                'document_amount' => (float) $a->document_amount,
                'amount_due' => (float) $a->amount_due,
                'allocated_amount' => (float) $a->allocated_amount,
            ])->values()->all();
        }

        return [
            'payment' => $payment,
            'kind' => $kind,
            'kindMeta' => $kindMeta,
            'isReceived' => $isReceived,
            'isAdvance' => $isAdvance,
            'typeLabel' => $isAdvance
                ? ($isReceived ? 'Advance Payment Received' : 'Advance Payment Made')
                : ($isReceived ? 'Payment Received' : 'Payment Made'),
            'paymentMethods' => config('payment.payment_methods'),
            'customers' => Customer::where('company_id', $companyId)->where('is_active', true)->orderBy('name')->get(),
            'vendors' => Vendor::where('company_id', $companyId)->where('is_active', true)->orderBy('name')->get(),
            'customersJson' => Customer::where('company_id', $companyId)->where('is_active', true)->orderBy('name')->get()->map(fn ($c) => $c->toPickerArray())->values(),
            'vendorsJson' => Vendor::where('company_id', $companyId)->where('is_active', true)->orderBy('name')->get()->map(fn ($v) => $v->toPickerArray())->values(),
            'defaultPrefix' => $defaultPrefix,
            'defaultSeq' => $defaultSeq,
            'allocations' => $allocations,
        ];
    }

  /**
     * @return array<string, mixed>
     */
    private function validatePayment(Request $request, ?Payment $payment = null): array
    {
        $kind = $this->resolveKind($request->input('kind', $payment?->kind ?? 'received'));
        $methodKeys = implode(',', array_keys(config('payment.payment_methods', [])));
        $isReceived = in_array($kind, ['received', 'advance_received'], true);

        $rules = [
            'kind' => ['required', 'in:'.implode(',', array_keys(config('payment.kinds', [])))],
            'payment_prefix' => ['nullable', 'string', 'max:20'],
            'payment_seq' => ['required', 'integer', 'min:1'],
            'payment_date' => ['required', 'date'],
            'amount' => ['required', 'numeric', 'min:0.01'],
            'payment_method' => ['required', 'string', 'in:'.$methodKeys],
            'transaction_ref' => ['nullable', 'string', 'max:100'],
            'bank_charges' => ['nullable', 'numeric', 'min:0'],
            'notes' => ['nullable', 'string', 'max:5000'],
        ];

        if ($isReceived) {
            $rules['customer_id'] = ['required', 'exists:customers,id'];
        } else {
            $rules['vendor_id'] = ['required', 'exists:vendors,id'];
        }

        $validated = $request->validate($rules);

        $prefix = trim((string) ($validated['payment_prefix'] ?? ''));
        $seq = (int) $validated['payment_seq'];
        $number = $prefix !== '' ? trim($prefix.' '.$seq) : (string) $seq;

        $companyId = $this->companyId();

        $exists = Payment::where('company_id', $companyId)
            ->where('payment_number', $number)
            ->when($payment, fn ($q) => $q->where('id', '!=', $payment->id))
            ->exists();

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

        $paymentType = in_array($kind, ['made', 'advance_made'], true) ? 'made' : 'received';

        return [
            'company_id' => $companyId,
            'kind' => $kind,
            'payment_type' => $paymentType,
            'payment_prefix' => $prefix,
            'payment_seq' => $seq,
            'payment_number' => $number,
            'payment_date' => $validated['payment_date'],
            'amount' => round((float) $validated['amount'], 2),
            'payment_method' => $validated['payment_method'],
            'payment_mode' => $this->legacyMode($validated['payment_method']),
            'bank_charges' => round((float) ($validated['bank_charges'] ?? 0), 2),
            'transaction_ref' => $validated['transaction_ref'] ?? null,
            'notes' => $validated['notes'] ?? null,
            'customer_id' => $isReceived ? (int) $validated['customer_id'] : null,
            'vendor_id' => $isReceived ? null : (int) $validated['vendor_id'],
            'reference_type' => in_array($kind, ['advance_received', 'advance_made'], true)
                ? ($isReceived ? 'customer' : 'vendor')
                : ($isReceived ? 'invoice' : 'purchase_bill'),
        ];
    }

    private function legacyMode(string $method): string
    {
        return match ($method) {
            'bank_transfer', 'pay_slip', 'payment_gateway' => 'bank',
            'upi', 'qr_code' => 'upi',
            'cheque' => 'cheque',
            'credit_card' => 'card',
            default => in_array($method, ['cash', 'cash_memo', 'tds', 'tcs', 'credit_note', 'other'], true) ? 'cash' : 'other',
        };
    }

    /**
     * @return list<array<string, mixed>>
     */
    private function validateAllocations(Request $request, string $kind): array
    {
        if (in_array($kind, ['advance_received', 'advance_made'], true)) {
            return [];
        }

        $rows = collect($request->input('allocations', []))
            ->filter(fn ($row) => (float) ($row['allocated_amount'] ?? 0) > 0)
            ->values()
            ->all();

        return $rows;
    }

    /**
     * @param  list<array<string, mixed>>  $rows
     */
    private function syncAllocations(Payment $payment, array $rows): void
    {
        $companyId = $payment->company_id;

        foreach ($rows as $row) {
            $allocated = round((float) ($row['allocated_amount'] ?? 0), 2);
            if ($allocated <= 0) {
                continue;
            }

            if ($payment->isReceived()) {
                $invoice = Invoice::forCompany($companyId)->findOrFail((int) $row['invoice_id']);
                PaymentAllocation::create([
                    'payment_id' => $payment->id,
                    'invoice_id' => $invoice->id,
                    'document_number' => $invoice->invoice_number,
                    'document_date' => $invoice->invoice_date,
                    'document_amount' => $invoice->grand_total,
                    'amount_due' => $row['amount_due'] ?? $invoice->balance,
                    'allocated_amount' => $allocated,
                ]);
                $invoice->paid_amount = round((float) $invoice->paid_amount + $allocated, 2);
                $invoice->payment_date = $payment->payment_date;
                if ($invoice->paid_amount >= (float) $invoice->grand_total) {
                    $invoice->status = 'paid';
                } elseif ($invoice->paid_amount > 0) {
                    $invoice->status = 'partial';
                }
                $invoice->save();
            } else {
                $bill = PurchaseBill::forCompany($companyId)->findOrFail((int) $row['purchase_bill_id']);
                PaymentAllocation::create([
                    'payment_id' => $payment->id,
                    'purchase_bill_id' => $bill->id,
                    'document_number' => $bill->bill_number,
                    'document_date' => $bill->bill_date,
                    'document_amount' => $bill->grand_total,
                    'amount_due' => $row['amount_due'] ?? max(0, (float) $bill->grand_total - (float) $bill->paid_amount),
                    'allocated_amount' => $allocated,
                ]);
                $bill->paid_amount = round((float) $bill->paid_amount + $allocated, 2);
                $bill->payment_date = $payment->payment_date;
                $bill->amount_due = max(0, round((float) $bill->grand_total - (float) $bill->paid_amount, 2));
                if ($bill->paid_amount >= (float) $bill->grand_total) {
                    $bill->status = 'paid';
                } elseif ($bill->paid_amount > 0) {
                    $bill->status = 'partial';
                }
                $bill->save();
            }
        }
    }

    private function revertAllocations(Payment $payment): void
    {
        $payment->load('allocations');

        foreach ($payment->allocations as $alloc) {
            if ($alloc->invoice_id) {
                $invoice = Invoice::find($alloc->invoice_id);
                if ($invoice) {
                    $invoice->paid_amount = max(0, round((float) $invoice->paid_amount - (float) $alloc->allocated_amount, 2));
                    $invoice->save();
                }
            }
            if ($alloc->purchase_bill_id) {
                $bill = PurchaseBill::find($alloc->purchase_bill_id);
                if ($bill) {
                    $bill->paid_amount = max(0, round((float) $bill->paid_amount - (float) $alloc->allocated_amount, 2));
                    $bill->amount_due = max(0, round((float) $bill->grand_total - (float) $bill->paid_amount, 2));
                    $bill->save();
                }
            }
        }
    }

    private function refreshPaymentTotals(Payment $payment): void
    {
        $payment->load('allocations');
        $used = round((float) $payment->allocations->sum('allocated_amount'), 2);
        $payment->update([
            'amount_used' => $used,
            'available_credit' => max(0, round((float) $payment->amount - $used, 2)),
        ]);
    }
}
