<?php

namespace App\Http\Controllers\Admin;

use App\Http\Controllers\Admin\Concerns\CalculatesGstDocumentTotals;
use App\Helpers\CompanySettings;
use App\Helpers\PurchaseBillDocumentSettings;
use App\Helpers\GstHelper;
use App\Helpers\UnitHelper;
use App\Models\Company;
use App\Models\PurchaseBill;
use App\Models\PurchaseBillItem;
use App\Models\Item;
use App\Models\TaxRate;
use App\Models\Vendor;
use App\Services\PurchaseBillDocumentPresenter;
use App\Services\PurchaseBillPdfGenerator;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Symfony\Component\HttpFoundation\StreamedResponse;

class PurchaseBillController extends AdminController
{
    use CalculatesGstDocumentTotals;

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

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

        $query = PurchaseBill::with(['vendor'])
            ->forCompany($companyId)
            ->when($filters['vendor_name'] ?? null, fn ($q, $v) => $q->whereHas('vendor', fn ($c) => $c->where('name', 'like', "%{$v}%")))
            ->when($filters['bill_number'] ?? null, fn ($q, $v) => $q->where('bill_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('bill_date', '>=', $v))
            ->when($filters['issued_to'] ?? null, fn ($q, $v) => $q->whereDate('bill_date', '<=', $v))
            ->when($filters['due_from'] ?? null, fn ($q, $v) => $q->whereDate('due_date', '>=', $v))
            ->when($filters['due_to'] ?? null, fn ($q, $v) => $q->whereDate('due_date', '<=', $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('bill_number', 'like', "%{$v}%")
                        ->orWhereHas('vendor', fn ($c) => $c->where('name', 'like', "%{$v}%"));
                });
            });

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

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

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

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

        return view('admin.purchase-bills.index', array_merge([
            'purchaseBills' => $purchaseBills,
            'filters' => $filters,
            'hasFilters' => $hasFilters,
            'statuses' => config('purchase_bill.statuses'),
            'features' => config('purchase_bill.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.purchase-bills.create', array_merge($data, ['emptyVendors' => true]));
        }

        return view('admin.purchase-bills.create', $data);
    }

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

        $status = $request->input('save_action') === 'draft' ? 'draft' : 'open';
        $calc['amount_due'] = $status === 'open' ? $calc['grand_total'] : 0;

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

        if (in_array($request->input('save_action'), ['save', 'preview'], true)) {
            return redirect()->route('admin.purchase-bills.index', ['purchase_bill' => $purchaseBill->id])
                ->with('success', 'Purchase bill created successfully.');
        }

        return redirect()->route('admin.purchase-bills.index')
            ->with('success', 'Purchase bill saved as draft.');
    }

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

        return view('admin.purchase-bills.edit', $this->formData($purchase_bill, request()));
    }

    public function update(Request $request, PurchaseBill $purchase_bill)
    {
        $this->authorizeOrder($purchase_bill);

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

        $status = $request->input('save_action') === 'draft'
            ? 'draft'
            : ($purchase_bill->status === 'draft' ? 'open' : $purchase_bill->status);

        if (in_array($status, ['void', 'paid'], true)) {
            $calc['amount_due'] = 0;
        } elseif ($status === 'open') {
            $calc['amount_due'] = $calc['grand_total'];
        } else {
            $calc['amount_due'] = $purchase_bill->amount_due;
        }

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

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

        return redirect()->route('admin.purchase-bills.index')
            ->with('success', 'Purchase bill saved as draft.');
    }

    public function show(PurchaseBill $purchase_bill)
    {
        return redirect()->route('admin.purchase-bills.index', ['purchase_bill' => $purchase_bill->id]);
    }

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

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

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

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

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

    public function shareLink(PurchaseBill $purchase_bill)
    {
        $this->authorizeOrder($purchase_bill);

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

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

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

        $orders = PurchaseBill::with('vendor')
            ->forCompany($companyId)
            ->when($filters['vendor_name'] ?? null, fn ($q, $v) => $q->whereHas('vendor', fn ($c) => $c->where('name', 'like', "%{$v}%")))
            ->when($filters['bill_number'] ?? null, fn ($q, $v) => $q->where('bill_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('bill_date', '>=', $v))
            ->when($filters['issued_to'] ?? null, fn ($q, $v) => $q->whereDate('bill_date', '<=', $v))
            ->when($filters['q'] ?? null, function ($q, $v) {
                $q->where(function ($sub) use ($v) {
                    $sub->where('bill_number', 'like', "%{$v}%")
                        ->orWhereHas('vendor', fn ($c) => $c->where('name', 'like', "%{$v}%"));
                });
            })
            ->latest('bill_date')
            ->latest('id')
            ->get();

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

        return response()->streamDownload(function () use ($orders) {
            $out = fopen('php://output', 'w');
            fputcsv($out, ['Issue Date', 'Doc No', 'Status', 'Vendor', 'Valid Until', 'Amount', 'Amount Due']);
            foreach ($orders as $order) {
                fputcsv($out, [
                    $order->bill_date->format('Y-m-d'),
                    $order->bill_number,
                    $order->statusMeta()['label'],
                    $order->partyName(),
                    $order->due_date?->format('Y-m-d') ?? '',
                    $order->grand_total,
                    $order->amount_due,
                ]);
            }
            fclose($out);
        }, $filename, ['Content-Type' => 'text/csv']);
    }

    public function destroy(PurchaseBill $purchase_bill)
    {
        $this->authorizeOrder($purchase_bill);
        $purchase_bill->delete();

        return redirect()->route('admin.purchase-bills.index')->with('success', 'Purchase bill deleted successfully.');
    }

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

    private function formData(?PurchaseBill $purchaseBill, Request $request): array
    {
        $companyId = $this->companyId();
        $count = PurchaseBill::where('company_id', $companyId)->count();
        $defaultSeq = $count + ($purchaseBill ? 0 : 1);
        $defaultPrefix = config('purchase_bill.prefixes.default', 'PB');
        $dueDays = (int) config('purchase_bill.default_due_days', 15);

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

        if ($request->query('copy_from') && ! $purchaseBill) {
            $source = PurchaseBill::with('items')->forCompany($companyId)->find($request->query('copy_from'));
            if ($source) {
                $lineItems = $source->items->map(fn ($li) => [
                    'item_id' => $li->item_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();
                $purchaseBill = new PurchaseBill([
                    'vendor_id' => $source->vendor_id,
                    'place_of_supply' => $source->place_of_supply,
                    'tax_on' => $source->tax_on,
                    'po_number' => $source->po_number,
                    'po_date' => $source->po_date,
                    'vendor_invoice_number' => $source->vendor_invoice_number,
                    '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_advance_payment' => $source->has_advance_payment,
                    'has_total_quantity' => $source->has_total_quantity,
                    'show_cess' => $source->show_cess,
                    'reverse_charge' => $source->reverse_charge,
                ]);
            }
        }

        $catalogItems = Item::with('taxRate')
            ->where('company_id', $companyId)->where('is_active', true)
            ->orderBy('name')->get(['id', 'name', 'unit', 'purchase_rate', 'hsn_sac', 'tax_rate_id']);

        $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();

        return [
            'purchaseBill' => $purchaseBill,
            'company' => $company,
            'gstEnabled' => GstHelper::usesRegularGst($company),
            'indianStates' => config('company.indian_states', []),
            'refLabels' => config('purchase_bill.ref_labels'),
            'typeLabel' => 'Purchase Order',
            'docLabels' => [
                'title' => PurchaseBillDocumentSettings::title($companyId),
                'number' => 'Number',
                'date' => 'PO date',
            ],
            'vendors' => $vendors,
            'vendorsJson' => $vendors->map(fn ($v) => $v->toPickerArray())->values(),
            'catalogItems' => $catalogItems,
            'taxRates' => $taxRates,
            'shippingModes' => config('invoice.shipping_modes'),
            'discountAllTypes' => config('invoice.discount_all_types'),
            'defaultPrefix' => $purchaseBill?->bill_prefix ?? $defaultPrefix,
            'defaultSeq' => $purchaseBill?->bill_seq ?? $defaultSeq,
            'defaultDueDate' => $purchaseBill?->due_date?->format('Y-m-d')
                ?? now()->addDays($dueDays)->format('Y-m-d'),
            'itemsJson' => $catalogItems->map(fn ($i) => [
                'id' => $i->id,
                'name' => $i->name,
                'unit' => $i->unit,
                'rate' => (float) ($i->purchase_rate ?: 0),
                'hsn_sac' => $i->hsn_sac ?? '',
                'tax_rate' => (float) ($i->taxRate?->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' => $purchaseBill && $purchaseBill->exists
                ? $purchaseBill->items->map(fn ($li) => [
                    'item_id' => $li->item_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 validatePurchaseBill(Request $request, ?PurchaseBill $purchaseBill = null): array
    {
        $refKeys = implode(',', array_keys(config('purchase_bill.ref_labels', [])));

        $data = $request->validate([
            'vendor_id' => ['required', 'exists:vendors,id'],
            'bill_prefix' => ['required', 'string', 'max:20'],
            'bill_seq' => ['required', 'integer', 'min:1'],
            'bill_date' => ['required', 'date'],
            'due_date' => ['nullable', 'date', 'after_or_equal:bill_date'],
            'ref_label' => ['required', 'string', 'in:'.$refKeys],
            'ref_number' => ['nullable', 'string', 'max:100'],
            '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'],
            'advance_payment' => ['nullable', 'numeric', 'min:0'],
            '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['bill_number'] = trim($data['bill_prefix']).'-'.str_pad((string) $data['bill_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_advance_payment'] = $request->boolean('has_advance_payment');
        $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_advance_payment']) {
            $data['advance_payment'] = 0;
        }
        if (! $data['has_total_quantity']) {
            $data['total_quantity'] = null;
            $data['total_quantity_unit'] = null;
        }

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

        $exists = PurchaseBill::where('company_id', $this->companyId())
            ->where('bill_number', $data['bill_number'])
            ->when($purchaseBill, fn ($q) => $q->where('id', '!=', $purchaseBill->id))
            ->exists();

        if ($exists) {
            throw \Illuminate\Validation\ValidationException::withMessages([
                'bill_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
            - (float) ($data['advance_payment'] ?? 0);

        $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(PurchaseBill $purchaseBill, array $items): void
    {
        foreach ($items as $row) {
            PurchaseBillItem::create([
                'purchase_bill_id' => $purchaseBill->id,
                'item_id' => $row['item_id'] ?? null,
                '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),
            ]);
        }
    }
}
