<?php

declare(strict_types=1);

namespace Maestro\Core\Modules\Invoices\Http\Controllers;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Schema;
use Maestro\Core\Http\Controllers\Controller;
use Maestro\Core\Models\Organization;
use Maestro\Core\Modules\Invoices\Enums\InvoiceStatus;
use Maestro\Core\Modules\Invoices\Enums\InvoiceType;
use Maestro\Core\Modules\Invoices\Enums\IssuedVia;
use Maestro\Core\Modules\Invoices\Http\Requests\SendInvoiceRequest;
use Maestro\Core\Modules\Invoices\Http\Requests\StoreInvoiceRequest;
use Maestro\Core\Modules\Invoices\Http\Requests\UpdateInvoiceRequest;
use Maestro\Core\Modules\Invoices\Http\Resources\InvoiceListResource;
use Maestro\Core\Modules\Invoices\Http\Resources\InvoiceResource;
use Maestro\Core\Modules\Invoices\Http\Resources\VatRateResource;
use Maestro\Core\Modules\Invoices\Mail\InvoiceSentMailable;
use Maestro\Core\Modules\Invoices\Models\Invoice;
use Maestro\Core\Modules\Invoices\Models\InvoiceLine;
use Maestro\Core\Modules\Invoices\Models\VatRate;
use Maestro\Core\Modules\Invoices\Services\InvoiceDocumentBuilder;
use Maestro\Core\Modules\Invoices\Services\InvoiceNumberService;
use Maestro\Core\Modules\Invoices\Services\InvoicePdfExporter;
use Maestro\Core\Modules\Invoices\Services\InvoiceTotalsService;
use Maestro\Core\Modules\Items\Models\Item;
use Symfony\Component\HttpFoundation\Response;

final class InvoiceController extends Controller
{
    public function meta(Request $request): JsonResponse
    {
        abort_unless($request->user()?->can('invoices.view'), 403);

        $rates = VatRate::query()->orderBy('sort_order')->orderBy('id')->get();
        $defaultRate = $rates->firstWhere('is_active', true) ?? $rates->first();

        $itemsEnabled = config('core.modules.items', false) === true
            && Schema::hasTable('maestro_items');

        $itemsPayload = [];
        if ($itemsEnabled) {
            $itemsPayload = Item::query()
                ->where('is_active', true)
                ->orderBy('sort_order')
                ->orderBy('name')
                ->get(['id', 'name', 'description', 'default_unit_price_ex_vat', 'default_vat_rate_id'])
                ->all();
        }

        return response()->json([
            'data' => [
                'vat_rates' => VatRateResource::collection($rates),
                'default_vat_rate_id' => $defaultRate?->id,
                'items_enabled' => $itemsEnabled,
                'items' => $itemsPayload,
            ],
        ]);
    }

    public function index(Request $request): JsonResponse
    {
        abort_unless($request->user()?->can('invoices.view'), 403);

        $query = Invoice::query()->with('customer');

        if ($request->filled('type')) {
            $query->where('type', (string) $request->input('type'));
        }
        if ($request->filled('status')) {
            $query->where('status', (string) $request->input('status'));
        }

        $search = trim((string) $request->input('search', ''));
        if ($search !== '') {
            $like = '%'.addcslashes($search, '%_\\').'%';
            $query->where(function (Builder $q) use ($like): void {
                $q->where('document_number', 'like', $like)
                    ->orWhereHas('customer', fn (Builder $cq) => $cq->where('name', 'like', $like));
            });
        }

        $sortBy = (string) $request->input('sort_by', 'invoice_date');
        $sortDir = strtolower((string) $request->input('sort_dir', 'desc')) === 'asc' ? 'asc' : 'desc';

        $allowedSort = ['invoice_date', 'expiration_date', 'document_number', 'updated_at', 'total_inc_vat', 'status'];
        if (! in_array($sortBy, $allowedSort, true)) {
            $sortBy = 'invoice_date';
        }

        $query->orderBy($sortBy, $sortDir);

        $perPage = min(max($request->integer('per_page', 15), 1), 100);

        return InvoiceListResource::collection($query->paginate($perPage))->response();
    }

    public function store(StoreInvoiceRequest $request): JsonResponse
    {
        abort_unless($request->user()?->can('invoices.create'), 403);

        $validated = $request->validated();
        $type = InvoiceType::from($validated['type']);

        $this->guardCreditNoteOriginal($type, isset($validated['credited_invoice_id']) ? (int) $validated['credited_invoice_id'] : null);

        $lines = $validated['lines'] ?? [];
        $this->guardLineItems($lines);

        $creditedId = $type === InvoiceType::CreditNote
            ? (int) ($validated['credited_invoice_id'] ?? 0)
            : null;

        $invoice = Invoice::query()->create([
            'type' => $type,
            'status' => InvoiceStatus::Draft,
            'issued_via' => null,
            'document_number' => null,
            'fiscal_year' => null,
            'sequence_value' => null,
            'customer_id' => (int) $validated['customer_id'],
            'credited_invoice_id' => $creditedId ?: null,
            'invoice_date' => $validated['invoice_date'],
            'expiration_date' => $validated['expiration_date'],
            'description' => $validated['description'] ?? null,
            'email_body' => $validated['email_body'] ?? null,
            'subtotal_ex_vat' => 0,
            'total_vat' => 0,
            'total_inc_vat' => 0,
            'finalized_at' => null,
            'email_sent_at' => null,
        ]);

        $this->replaceLines($invoice, $lines);
        InvoiceTotalsService::recalculate($invoice->fresh(['lines.vatRate']));

        return (new InvoiceResource($invoice->load(['customer', 'lines.vatRate'])))
            ->response()
            ->setStatusCode(201);
    }

    public function show(Request $request, Invoice $invoice): JsonResponse
    {
        abort_unless($request->user()?->can('invoices.view'), 403);

        $invoice->load(['customer', 'lines.vatRate', 'creditedInvoice']);

        return (new InvoiceResource($invoice))->response();
    }

    public function update(UpdateInvoiceRequest $request, Invoice $invoice): JsonResponse
    {
        abort_unless($request->user()?->can('invoices.edit'), 403);
        abort_if($invoice->isFinalized(), 422, 'Invoice is finalized.');

        $validated = $request->validated();

        if ($invoice->type === InvoiceType::CreditNote && isset($validated['credited_invoice_id'])) {
            $this->guardCreditNoteOriginal(
                $invoice->type,
                (int) $validated['credited_invoice_id']
            );
        }

        $invoice->fill([
            'customer_id' => $validated['customer_id'] ?? $invoice->customer_id,
            'credited_invoice_id' => array_key_exists('credited_invoice_id', $validated)
                ? $validated['credited_invoice_id']
                : $invoice->credited_invoice_id,
            'invoice_date' => $validated['invoice_date'] ?? $invoice->invoice_date->toDateString(),
            'expiration_date' => $validated['expiration_date'] ?? $invoice->expiration_date->toDateString(),
            'description' => array_key_exists('description', $validated) ? $validated['description'] : $invoice->description,
            'email_body' => array_key_exists('email_body', $validated) ? $validated['email_body'] : $invoice->email_body,
        ]);
        $invoice->save();

        if (isset($validated['lines'])) {
            $this->guardLineItems($validated['lines']);
            $this->replaceLines($invoice, $validated['lines']);
            InvoiceTotalsService::recalculate($invoice->fresh(['lines.vatRate']));
        }

        return (new InvoiceResource($invoice->fresh(['customer', 'lines.vatRate'])))->response();
    }

    public function destroy(Request $request, Invoice $invoice): JsonResponse
    {
        abort_unless($request->user()?->can('invoices.delete'), 403);
        abort_if($invoice->isFinalized(), 422, 'Cannot delete a finalized invoice.');

        $invoice->delete();

        return response()->json(['message' => 'Deleted.']);
    }

    public function pdf(Request $request, Invoice $invoice): Response
    {
        abort_unless($request->user()?->can('invoices.view'), 403);

        $invoice->load(['customer', 'lines.vatRate']);

        return InvoicePdfExporter::downloadResponse($invoice);
    }

    public function send(SendInvoiceRequest $request, Invoice $invoice): JsonResponse
    {
        abort_unless($request->user()?->can('invoices.send'), 403);

        $organization = Organization::instance();
        abort_if($organization === null, 422, 'Organization profile is missing.');

        $this->finalizeInternal($invoice, IssuedVia::AppEmail, true, $request);

        return response()->json([
            'data' => [
                'email_sent' => true,
            ],
        ]);
    }

    public function book(Request $request, Invoice $invoice): JsonResponse
    {
        abort_unless($request->user()?->can('invoices.book'), 403);

        $this->finalizeInternal($invoice, IssuedVia::External, false, null);

        return response()->json([
            'data' => [
                'booked' => true,
            ],
        ]);
    }

    public function sendCopy(SendInvoiceRequest $request, Invoice $invoice): JsonResponse
    {
        abort_unless($request->user()?->can('invoices.send'), 403);
        abort_unless($invoice->isFinalized(), 422, 'Invoice is not finalized.');

        $organization = Organization::instance();
        abort_if($organization === null, 422, 'Organization profile is missing.');

        $validated = $request->validated();
        $invoice->load(['customer', 'lines.vatRate']);

        $to = isset($validated['to']) ? trim((string) $validated['to']) : '';
        if ($to === '') {
            $to = trim((string) ($invoice->customer->email ?? ''));
        }
        abort_if($to === '', 422, 'Email recipient required.');

        $doc = InvoiceDocumentBuilder::build($invoice);
        $pdf = InvoicePdfExporter::rawPdf($invoice);
        $filename = sprintf(
            '%s.pdf',
            preg_replace('/[^a-zA-Z0-9._-]/', '_', (string) ($invoice->document_number ?? 'invoice'))
        );

        Mail::to($to)->send(new InvoiceSentMailable(
            $organization,
            $doc,
            $pdf,
            $filename,
            (string) ($invoice->email_body ?? ''),
        ));

        $invoice->email_sent_at = now();
        $invoice->save();

        return response()->json(['data' => ['email_sent' => true]]);
    }

    /**
     * @param  list<array<string, mixed>>  $lines
     */
    private function replaceLines(Invoice $invoice, array $lines): void
    {
        $invoice->lines()->delete();

        $order = 0;
        foreach ($lines as $row) {
            $order++;
            InvoiceLine::query()->create([
                'invoice_id' => $invoice->id,
                'sort_order' => $order,
                'item_id' => isset($row['item_id']) ? (int) $row['item_id'] : null,
                'description' => (string) $row['description'],
                'quantity' => (string) $row['quantity'],
                'unit_price_ex_vat' => (string) $row['unit_price_ex_vat'],
                'vat_rate_id' => (int) $row['vat_rate_id'],
                'line_total_ex_vat' => 0,
                'line_vat_amount' => 0,
                'line_total_inc_vat' => 0,
            ]);
        }
    }

    /**
     * @param  list<array<string, mixed>>  $lines
     */
    private function guardLineItems(array $lines): void
    {
        $itemsEnabled = config('core.modules.items', false) === true
            && Schema::hasTable('maestro_items');

        foreach ($lines as $row) {
            if (empty($row['item_id'])) {
                continue;
            }
            abort_unless($itemsEnabled, 422, 'Items module is disabled — remove item references or enable Items.');
            abort_unless(
                Item::query()->whereKey((int) $row['item_id'])->exists(),
                422,
                'Invalid catalog item.'
            );
        }
    }

    private function guardCreditNoteOriginal(InvoiceType $type, ?int $creditedInvoiceId): void
    {
        if ($type !== InvoiceType::CreditNote) {
            return;
        }

        abort_if($creditedInvoiceId === null || $creditedInvoiceId === 0, 422, 'Credit note requires credited_invoice_id.');

        $original = Invoice::query()->whereKey($creditedInvoiceId)->first();
        abort_if($original === null, 422, 'Credited invoice not found.');
        abort_unless($original->type === InvoiceType::Invoice, 422, 'You can only reference an invoice document.');
        abort_unless($original->isFinalized(), 422, 'The original invoice must be finalized.');
    }

    private function finalizeInternal(
        Invoice $invoice,
        IssuedVia $issuedVia,
        bool $sendEmail,
        ?SendInvoiceRequest $sendRequest
    ): void {
        abort_unless($invoice->isDraft(), 422, 'Invoice is already finalized.');
        abort_if($invoice->lines()->count() < 1, 422, 'Add at least one line before finalizing.');

        if ($invoice->type === InvoiceType::CreditNote) {
            $this->guardCreditNoteOriginal($invoice->type, $invoice->credited_invoice_id);
        }

        $invoice->load(['lines.vatRate', 'customer']);
        InvoiceTotalsService::recalculate($invoice);

        $invoice->refresh();
        InvoiceNumberService::assignNext($invoice);
        $invoice->refresh();

        $invoice->status = InvoiceStatus::Finalized;
        $invoice->issued_via = $issuedVia;
        $invoice->finalized_at = now();
        $invoice->save();

        if ($sendEmail && $sendRequest !== null) {
            $validated = $sendRequest->validated();
            $organization = Organization::instance();
            abort_if($organization === null, 422, 'Organization profile is missing.');

            $to = isset($validated['to']) ? trim((string) $validated['to']) : '';
            if ($to === '') {
                $invoice->load('customer');
                $to = trim((string) ($invoice->customer->email ?? ''));
            }
            abort_if($to === '', 422, 'Customer has no email — provide `to` explicitly.');

            $invoice->load(['customer', 'lines.vatRate']);
            $doc = InvoiceDocumentBuilder::build($invoice);
            $pdf = InvoicePdfExporter::rawPdf($invoice);
            $filename = sprintf(
                '%s.pdf',
                preg_replace('/[^a-zA-Z0-9._-]/', '_', (string) ($invoice->document_number ?? 'invoice'))
            );

            Mail::to($to)->send(new InvoiceSentMailable(
                $organization,
                $doc,
                $pdf,
                $filename,
                (string) ($invoice->email_body ?? ''),
            ));

            $invoice->email_sent_at = now();
            $invoice->save();
        }
    }
}
