<?php

namespace App\Http\Controllers\Admin;

use App\Http\Controllers\Admin\Concerns\CalculatesGstDocumentTotals;
use App\Helpers\CompanySettings;
use App\Helpers\CreditNoteDocumentSettings;
use App\Helpers\GstHelper;
use App\Helpers\ItemCatalogHelper;
use App\Helpers\UnitHelper;
use App\Models\Company;
use App\Models\CreditNote;
use App\Models\CreditNoteItem;
use App\Models\Customer;
use App\Models\Invoice;
use App\Models\Item;
use App\Models\TaxRate;
use App\Services\CreditNoteDocumentPresenter;
use App\Services\CreditNotePdfGenerator;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Symfony\Component\HttpFoundation\StreamedResponse;

class CreditNoteController extends AdminController
{
    use CalculatesGstDocumentTotals;

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

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

        $query = CreditNote::with(['customer', 'invoice'])
            ->forCompany($companyId)
            ->when($filters['client_name'] ?? null, fn ($q, $v) => $q->whereHas('customer', fn ($c) => $c->where('name', 'like', "%{$v}%")))
            ->when($filters['credit_note_number'] ?? null, fn ($q, $v) => $q->where('credit_note_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('customer', fn ($c) => $c->where('city', 'like', "%{$v}%")))
            ->when($filters['issued_from'] ?? null, fn ($q, $v) => $q->whereDate('credit_note_date', '>=', $v))
            ->when($filters['issued_to'] ?? null, fn ($q, $v) => $q->whereDate('credit_note_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('credit_note_number', 'like', "%{$v}%")
                        ->orWhereHas('customer', fn ($c) => $c->where('name', 'like', "%{$v}%"));
                });
            });

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

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

        if ($noteId && ! $selected) {
            $selected = CreditNote::with(['customer', 'invoice'])->forCompany($companyId)->find($noteId);
            if ($selected) {
                $creditNotes = $creditNotes->push($selected)
                    ->sortByDesc(fn ($n) => $n->credit_note_date->format('Y-m-d').'-'.$n->id)
                    ->values();
            }
        }

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

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

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

        if ($data['customers']->isEmpty()) {
            return view('admin.credit-notes.create', array_merge($data, ['emptyCustomers' => true]));
        }

        return view('admin.credit-notes.create', $data);
    }

    public function store(Request $request)
    {
        $data = $this->validateCreditNote($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['credits_available'] = $status === 'open' ? $calc['grand_total'] : 0;

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

        if (in_array($request->input('save_action'), ['save', 'preview'], true)) {
            return redirect()->route('admin.credit-notes.index', ['credit_note' => $creditNote->id])
                ->with('success', 'Credit note created successfully.');
        }

        return redirect()->route('admin.credit-notes.index')
            ->with('success', 'Credit note saved as draft.');
    }

    public function edit(CreditNote $credit_note)
    {
        $this->authorizeNote($credit_note);
        $credit_note->load(['items', 'invoice']);

        return view('admin.credit-notes.edit', $this->formData($credit_note, request()));
    }

    public function update(Request $request, CreditNote $credit_note)
    {
        $this->authorizeNote($credit_note);

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

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

        if ($status === 'void' || $status === 'closed') {
            $calc['credits_available'] = 0;
        } elseif ($status === 'open') {
            $calc['credits_available'] = $calc['grand_total'];
        } else {
            $calc['credits_available'] = $credit_note->credits_available;
        }

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

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

        return redirect()->route('admin.credit-notes.index')
            ->with('success', 'Credit note saved as draft.');
    }

    public function show(CreditNote $credit_note)
    {
        return redirect()->route('admin.credit-notes.index', ['credit_note' => $credit_note->id]);
    }

    public function print(CreditNote $credit_note, Request $request)
    {
        $this->authorizeNote($credit_note);
        $copyKey = $request->query('copy', 'original');

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

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

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

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

    public function shareLink(CreditNote $credit_note)
    {
        $this->authorizeNote($credit_note);

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

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

    public function invoicesJson(Request $request): JsonResponse
    {
        $request->validate(['customer_id' => ['required', 'exists:customers,id']]);

        $invoices = Invoice::forCompany($this->companyId())
            ->where('customer_id', $request->customer_id)
            ->whereNotIn('status', ['cancelled', 'draft'])
            ->latest('invoice_date')
            ->latest('id')
            ->limit(100)
            ->get(['id', 'invoice_number', 'invoice_date', 'grand_total', 'place_of_supply', 'tax_on']);

        return response()->json($invoices->map(fn ($inv) => [
            'id' => $inv->id,
            'invoice_number' => $inv->invoice_number,
            'invoice_date' => $inv->invoice_date?->format('Y-m-d'),
            'grand_total' => (float) $inv->grand_total,
            'place_of_supply' => $inv->place_of_supply,
            'tax_on' => $inv->tax_on,
        ]));
    }

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

        $notes = CreditNote::with('customer')
            ->forCompany($companyId)
            ->when($filters['client_name'] ?? null, fn ($q, $v) => $q->whereHas('customer', fn ($c) => $c->where('name', 'like', "%{$v}%")))
            ->when($filters['credit_note_number'] ?? null, fn ($q, $v) => $q->where('credit_note_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('credit_note_date', '>=', $v))
            ->when($filters['issued_to'] ?? null, fn ($q, $v) => $q->whereDate('credit_note_date', '<=', $v))
            ->when($filters['q'] ?? null, function ($q, $v) {
                $q->where(function ($sub) use ($v) {
                    $sub->where('credit_note_number', 'like', "%{$v}%")
                        ->orWhereHas('customer', fn ($c) => $c->where('name', 'like', "%{$v}%"));
                });
            })
            ->latest('credit_note_date')
            ->latest('id')
            ->get();

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

        return response()->streamDownload(function () use ($notes) {
            $out = fopen('php://output', 'w');
            fputcsv($out, ['Date', 'Doc No', 'Status', 'Client', 'Amount', 'Credits Available']);
            foreach ($notes as $note) {
                fputcsv($out, [
                    $note->credit_note_date->format('Y-m-d'),
                    $note->credit_note_number,
                    $note->statusMeta()['label'],
                    $note->customer->name ?? '',
                    $note->grand_total,
                    $note->credits_available,
                ]);
            }
            fclose($out);
        }, $filename, ['Content-Type' => 'text/csv']);
    }

    public function destroy(CreditNote $credit_note)
    {
        $this->authorizeNote($credit_note);
        $credit_note->delete();

        return redirect()->route('admin.credit-notes.index')->with('success', 'Credit note deleted successfully.');
    }

    private function authorizeNote(CreditNote $creditNote): void
    {
        if ($creditNote->company_id !== $this->companyId()) {
            abort(403);
        }
    }

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

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

        if ($request->query('copy_from_invoice')) {
            $invoice = Invoice::with('items')->forCompany($companyId)->find($request->query('copy_from_invoice'));
            if ($invoice) {
                $lineItems = $invoice->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();
                if (! $creditNote) {
                    $creditNote = new CreditNote([
                        'customer_id' => $invoice->customer_id,
                        'invoice_id' => $invoice->id,
                        'related_invoice_number' => $invoice->invoice_number,
                        'related_invoice_date' => $invoice->invoice_date,
                        'place_of_supply' => $invoice->place_of_supply,
                        'tax_on' => $invoice->tax_on,
                    ]);
                }
            }
        }

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

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

        return [
            'creditNote' => $creditNote,
            'company' => $company,
            'gstEnabled' => GstHelper::usesRegularGst($company),
            'indianStates' => config('company.indian_states', []),
            'reasons' => config('credit_note.reasons'),
            'clientsJson' => $customers->map(fn ($c) => $c->toPickerArray())->values(),
            'clientPickerOptions' => [
                'gstTreatments' => config('customer.gst_treatments'),
                'currencies' => config('customer.currencies'),
                'countries' => config('customer.countries'),
                'indianStates' => config('customer.indian_states'),
            ],
            'typeLabel' => 'Credit Note',
            'docLabels' => [
                'title' => CreditNoteDocumentSettings::title($companyId),
                'number' => 'Credit Note no',
                'date' => 'Date',
            ],
            'customers' => $customers,
            'catalogItems' => $catalogItems,
            'taxRates' => $taxRates,
            'shippingModes' => config('invoice.shipping_modes'),
            'discountAllTypes' => config('invoice.discount_all_types'),
            'defaultPrefix' => $creditNote?->credit_note_prefix ?? $defaultPrefix,
            'defaultSeq' => $creditNote?->credit_note_seq ?? $defaultSeq,
            'invoicesJsonUrl' => route('admin.credit-notes.invoices-json'),
            'itemsJson' => ItemCatalogHelper::jsonForForm($catalogItems, 'sale_rate'),
            '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' => $creditNote && $creditNote->exists
                ? $creditNote->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 validateCreditNote(Request $request, ?CreditNote $creditNote = null): array
    {
        $reasonKeys = implode(',', array_keys(config('credit_note.reasons', [])));

        $data = $request->validate([
            'customer_id' => ['required', 'exists:customers,id'],
            'invoice_id' => ['nullable', 'exists:invoices,id'],
            'credit_note_prefix' => ['required', 'string', 'max:20'],
            'credit_note_seq' => ['required', 'integer', 'min:1'],
            'credit_note_date' => ['required', 'date'],
            'related_invoice_date' => ['nullable', 'date'],
            'reason' => ['required', 'string', 'in:'.$reasonKeys],
            '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['credit_note_number'] = $data['credit_note_prefix'].'-'.str_pad((string) $data['credit_note_seq'], 4, '0', STR_PAD_LEFT);
        $data['restore_stock'] = $request->boolean('restore_stock');
        $data['has_shipping'] = $request->boolean('has_shipping');
        $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['invoice_id'] ?? null) {
            $invoice = Invoice::forCompany($this->companyId())->find($data['invoice_id']);
            if ($invoice) {
                $data['related_invoice_number'] = $invoice->invoice_number;
                $data['related_invoice_date'] = $invoice->invoice_date;
            }
        } else {
            $data['invoice_id'] = null;
            $data['related_invoice_number'] = $request->input('related_invoice_number');
        }

        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['credit_note_seq'] = (int) $data['credit_note_seq'];

        $exists = CreditNote::where('company_id', $this->companyId())
            ->where('credit_note_number', $data['credit_note_number'])
            ->when($creditNote, fn ($q) => $q->where('id', '!=', $creditNote->id))
            ->exists();

        if ($exists) {
            throw \Illuminate\Validation\ValidationException::withMessages([
                'credit_note_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->processGstLineItems($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(CreditNote $creditNote, array $items): void
    {
        foreach ($items as $row) {
            CreditNoteItem::create([
                'credit_note_id' => $creditNote->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),
            ]);
        }
    }
}
