<?php

namespace App\Controllers;

use App\Libraries\InvoicePdf;
use App\Libraries\LeadMailer;
use App\Models\ClientModel;
use App\Models\InvoiceItemModel;
use App\Models\InvoiceModel;
use App\Models\QuotationItemModel;
use App\Models\QuotationModel;

class Invoices extends BaseController
{
    private const STATUSES = ["draft", "sent", "paid", "overdue", "cancelled"];

    public function index()
    {
        $search = trim((string) $this->request->getGet("search"));
        $status = (string) $this->request->getGet("status");
        $model = new InvoiceModel();
        $model
            ->select(
                "invoices.*, clients.first_name, clients.last_name, clients.email, quotations.quotation_number",
            )
            ->join("clients", "clients.id = invoices.client_id")
            ->join(
                "quotations",
                "quotations.id = invoices.quotation_id",
                "left",
            );
        if ($search !== "") {
            $model
                ->groupStart()
                ->like("invoices.invoice_number", $search)
                ->orLike("quotations.quotation_number", $search)
                ->orLike("clients.first_name", $search)
                ->orLike("clients.last_name", $search)
                ->orLike("clients.email", $search)
                ->groupEnd();
        }
        if (in_array($status, self::STATUSES, true)) {
            $model->where("invoices.status", $status);
        }

        $totalValue = (new InvoiceModel())->selectSum("total")->first();
        $outstandingValue = (new InvoiceModel())
            ->selectSum("total")
            ->whereIn("status", ["draft", "sent", "overdue"])
            ->first();

        return view("admin/invoices/index", [
            "invoices" => $model
                ->orderBy("invoices.created_at", "DESC")
                ->findAll(250),
            "search" => $search,
            "activeStatus" => $status,
            "statuses" => self::STATUSES,
            "metrics" => [
                "total" => (new InvoiceModel())->countAll(),
                "paid" => (new InvoiceModel())
                    ->where("status", "paid")
                    ->countAllResults(),
                "value" => (float) ($totalValue["total"] ?? 0),
                "outstanding" => (float) ($outstandingValue["total"] ?? 0),
            ],
        ]);
    }

    public function create()
    {
        if ($this->request->getMethod() === "post") {
            return $this->saveInvoice();
        }

        $quotationId = (int) $this->request->getGet("quotation_id");
        $standalone =
            (string) $this->request->getGet("standalone") === "1" &&
            !$quotationId;
        $quotation = $quotationId
            ? $this->acceptedQuotation($quotationId)
            : null;
        if ($quotationId && !$quotation) {
            return redirect()
                ->to(base_url("admin/invoices/new"))
                ->with(
                    "error",
                    "Choose an accepted quotation that has not been invoiced.",
                );
        }

        return view("admin/invoices/form", [
            "invoice" => null,
            "items" => $quotation ? $this->quotationItems($quotationId) : [],
            "quotation" => $quotation,
            "acceptedQuotations" => $this->availableQuotations(),
            "clients" => $standalone ? $this->clients() : [],
            "standalone" => $standalone,
            "statuses" => self::STATUSES,
            "mode" => "create",
        ]);
    }

    public function show($id)
    {
        $invoice = $this->invoiceWithClient((int) $id);
        if (!$invoice) {
            return $this->notFound();
        }

        return view("admin/invoices/show", [
            "invoice" => $invoice,
            "items" => $this->items((int) $id),
        ]);
    }

    public function edit($id)
    {
        $invoice = (new InvoiceModel())->find((int) $id);
        if (!$invoice) {
            return $this->notFound();
        }

        if ($this->request->getMethod() === "post") {
            return $this->saveInvoice((int) $id);
        }

        $quotationId = (int) ($invoice["quotation_id"] ?? 0);
        $standalone = $quotationId === 0;

        return view("admin/invoices/form", [
            "invoice" => $invoice,
            "items" => $this->items((int) $id),
            "quotation" => $quotationId
                ? $this->acceptedQuotation($quotationId, true)
                : null,
            "acceptedQuotations" => [],
            "clients" => $standalone ? $this->clients() : [],
            "standalone" => $standalone,
            "statuses" => self::STATUSES,
            "mode" => "edit",
        ]);
    }

    public function delete($id)
    {
        $invoice = (new InvoiceModel())->find((int) $id);
        if (!$invoice) {
            return $this->notFound();
        }

        (new InvoiceModel())->delete((int) $id);
        return redirect()
            ->to(base_url("admin/invoices"))
            ->with(
                "success",
                "Invoice " . $invoice["invoice_number"] . " was deleted.",
            );
    }

    public function download($id)
    {
        $invoice = $this->invoiceWithClient((int) $id);
        if (!$invoice) {
            return $this->notFound();
        }

        $pdf = (new InvoicePdf())->generate($invoice, $this->items((int) $id));
        return $this->response
            ->setContentType("application/pdf")
            ->setHeader(
                "Content-Disposition",
                'attachment; filename="' . $invoice["invoice_number"] . '.pdf"',
            )
            ->setHeader("Content-Length", (string) strlen($pdf))
            ->setBody($pdf);
    }

    public function emailPdf($id)
    {
        $invoice = $this->invoiceWithClient((int) $id);
        if (!$invoice) {
            return $this->notFound();
        }

        $pdf = (new InvoicePdf())->generate($invoice, $this->items((int) $id));
        $tempFile = tempnam(WRITEPATH . "cache", "invoice_");
        if (
            $tempFile === false ||
            file_put_contents($tempFile, $pdf) === false
        ) {
            if ($tempFile && is_file($tempFile)) {
                unlink($tempFile);
            }
            return redirect()
                ->back()
                ->with("error", "The invoice PDF could not be prepared.");
        }

        try {
            $sent = (new LeadMailer())->sendInvoice(
                $invoice["email"],
                $this->emailVariables($invoice, $this->items((int) $id)),
                $tempFile,
            );
        } finally {
            if (is_file($tempFile)) {
                unlink($tempFile);
            }
        }

        return $this->emailResult((int) $id, $sent, $invoice["email"], true);
    }

    public function emailInvoice($id)
    {
        $invoice = $this->invoiceWithClient((int) $id);
        if (!$invoice) {
            return $this->notFound();
        }

        $sent = (new LeadMailer())->sendInvoiceEmail(
            $invoice["email"],
            $this->emailVariables($invoice, $this->items((int) $id)),
        );

        return $this->emailResult((int) $id, $sent, $invoice["email"], false);
    }

    private function saveInvoice(?int $id = null)
    {
        $rules = [
            "quotation_id" => "permit_empty|integer|greater_than[0]",
            "client_id" => "permit_empty|integer|greater_than[0]",
            "invoice_date" => "required|valid_date[Y-m-d]",
            "due_date" => "permit_empty|valid_date[Y-m-d]",
            "status" =>
                "required|in_list[" . implode(",", self::STATUSES) . "]",
            "notes" => "permit_empty|max_length[3000]",
        ];
        if (!$this->validate($rules)) {
            return $this->validationError(
                implode(" ", $this->validator->getErrors()),
            );
        }

        $quotationValue = trim(
            (string) $this->request->getPost("quotation_id"),
        );
        $quotationId = $quotationValue !== "" ? (int) $quotationValue : null;
        $quotation = $quotationId
            ? $this->acceptedQuotation($quotationId, $id !== null)
            : null;
        $clientId = 0;
        if ($quotationId) {
            if (!$quotation) {
                return $this->validationError(
                    "Choose an accepted quotation that has not already been invoiced.",
                );
            }
            $clientId = (int) $quotation["client_id"];
        } else {
            $clientId = (int) $this->request->getPost("client_id");
            if (!(new ClientModel())->find($clientId)) {
                return $this->validationError(
                    "Choose a valid client for the standalone invoice.",
                );
            }
        }
        if ($id !== null) {
            $existingInvoice = (new InvoiceModel())->find($id);
            $existingQuotationId =
                $existingInvoice && $existingInvoice["quotation_id"] !== null
                    ? (int) $existingInvoice["quotation_id"]
                    : null;
            if (!$existingInvoice || $existingQuotationId !== $quotationId) {
                return $this->validationError(
                    "The invoice source cannot be changed after it is created.",
                );
            }
        }

        $invoiceDate = (string) $this->request->getPost("invoice_date");
        $dueDate = (string) $this->request->getPost("due_date");
        if ($dueDate !== "" && $dueDate < $invoiceDate) {
            return $this->validationError(
                "The due date cannot be before the invoice date.",
            );
        }

        [$items, $itemError] = $this->prepareItems();
        if ($itemError !== null) {
            return $this->validationError($itemError);
        }
        $total = array_sum(array_column($items, "line_total"));
        $data = [
            "client_id" => $clientId,
            "quotation_id" => $quotationId,
            "invoice_date" => $invoiceDate,
            "due_date" => $dueDate !== "" ? $dueDate : null,
            "status" => (string) $this->request->getPost("status"),
            "notes" => trim((string) $this->request->getPost("notes")),
            "subtotal" => number_format($total, 2, ".", ""),
            "total" => number_format($total, 2, ".", ""),
        ];

        $db = db_connect();
        $db->transStart();
        $invoices = new InvoiceModel();
        if ($id === null) {
            $data["invoice_number"] = "PENDING-" . bin2hex(random_bytes(6));
            $id = (int) $invoices->insert($data);
            $invoices->update($id, [
                "invoice_number" =>
                    "BS-INV-" .
                    date("Y") .
                    "-" .
                    str_pad($id, 5, "0", STR_PAD_LEFT),
            ]);
        } else {
            $invoices->update($id, $data);
            $db->table("invoice_items")->where("invoice_id", $id)->delete();
        }
        foreach ($items as $index => $item) {
            $item["invoice_id"] = $id;
            $item["sort_order"] = $index;
            (new InvoiceItemModel())->insert($item);
        }
        $db->transComplete();

        if (!$db->transStatus()) {
            return $this->validationError(
                "The invoice could not be saved. Please try again.",
            );
        }

        return redirect()
            ->to(base_url("admin/invoices/" . $id))
            ->with(
                "success",
                $this->request->getPost("status") === "draft"
                    ? "Invoice saved as draft."
                    : "Invoice saved successfully.",
            );
    }

    private function prepareItems(): array
    {
        $names = (array) $this->request->getPost("item_name");
        $descriptions = (array) $this->request->getPost("description");
        $quantities = (array) $this->request->getPost("quantity");
        $prices = (array) $this->request->getPost("price");
        $items = [];
        foreach ($names as $index => $name) {
            $name = trim((string) $name);
            $description = trim((string) ($descriptions[$index] ?? ""));
            $quantity = $quantities[$index] ?? "";
            $price = $prices[$index] ?? "";
            if ($name === "" || strlen($name) > 150) {
                return [[], "Every invoice item needs a valid name."];
            }
            if (strlen($description) > 2000) {
                return [
                    [],
                    "Invoice item descriptions cannot exceed 2,000 characters.",
                ];
            }
            if (
                !is_numeric($quantity) ||
                (float) $quantity <= 0 ||
                !is_numeric($price) ||
                (float) $price < 0
            ) {
                return [
                    [],
                    "Every item needs a positive quantity and valid price.",
                ];
            }
            $quantity = round((float) $quantity, 2);
            $price = round((float) $price, 2);
            $items[] = [
                "item_name" => $name,
                "description" => $description,
                "quantity" => number_format($quantity, 2, ".", ""),
                "price" => number_format($price, 2, ".", ""),
                "line_total" => number_format(
                    round($quantity * $price, 2),
                    2,
                    ".",
                    "",
                ),
            ];
        }
        return $items ? [$items, null] : [[], "Add at least one invoice item."];
    }

    private function availableQuotations(): array
    {
        return (new QuotationModel())
            ->select(
                "quotations.*, clients.first_name, clients.last_name, clients.email",
            )
            ->join("clients", "clients.id = quotations.client_id")
            ->join("invoices", "invoices.quotation_id = quotations.id", "left")
            ->where("quotations.status", "accepted")
            ->where("invoices.id", null)
            ->orderBy("quotations.created_at", "DESC")
            ->findAll();
    }

    private function acceptedQuotation(
        int $id,
        bool $allowInvoiced = false
    ): ?array {
        $model = new QuotationModel();
        $model
            ->select(
                "quotations.*, clients.first_name, clients.last_name, clients.email",
            )
            ->join("clients", "clients.id = quotations.client_id")
            ->where("quotations.id", $id);
        if (!$allowInvoiced) {
            $model
                ->join(
                    "invoices",
                    "invoices.quotation_id = quotations.id",
                    "left",
                )
                ->where("quotations.status", "accepted")
                ->where("invoices.id", null);
        }
        return $model->first();
    }

    private function quotationItems(int $id): array
    {
        return (new QuotationItemModel())
            ->where("quotation_id", $id)
            ->orderBy("sort_order", "ASC")
            ->findAll();
    }

    private function clients(): array
    {
        return (new ClientModel())
            ->orderBy("first_name", "ASC")
            ->orderBy("last_name", "ASC")
            ->findAll();
    }

    private function items(int $id): array
    {
        return (new InvoiceItemModel())
            ->where("invoice_id", $id)
            ->orderBy("sort_order", "ASC")
            ->findAll();
    }

    private function invoiceWithClient(int $id): ?array
    {
        return (new InvoiceModel())
            ->select(
                "invoices.*, clients.first_name, clients.last_name, clients.email, clients.mobile, clients.address, quotations.quotation_number",
            )
            ->join("clients", "clients.id = invoices.client_id")
            ->join(
                "quotations",
                "quotations.id = invoices.quotation_id",
                "left",
            )
            ->where("invoices.id", $id)
            ->first();
    }

    private function emailVariables(array $invoice, array $items): array
    {
        $lines = [];
        foreach ($items as $item) {
            $lines[] =
                $item["item_name"] .
                " × " .
                number_format((float) $item["quantity"], 2) .
                " — ₹" .
                number_format((float) $item["line_total"], 2);
        }
        return [
            "name" => $invoice["first_name"],
            "business_name" =>
                $invoice["first_name"] . " " . $invoice["last_name"],
            "invoice_number" => $invoice["invoice_number"],
            "items_summary" => implode("\n", $lines),
            "amount" => "₹" . number_format((float) $invoice["total"], 2),
            "due_date" => $invoice["due_date"]
                ? date("d M Y", strtotime($invoice["due_date"]))
                : "on receipt",
            "attachment_name" => $invoice["invoice_number"] . ".pdf",
        ];
    }

    private function emailResult(
        int $id,
        bool $sent,
        string $email,
        bool $withPdf
    ) {
        if (!$sent) {
            return redirect()
                ->back()
                ->with(
                    "error",
                    "The invoice could not be emailed. Check SMTP settings and confirm the invoice email template is active.",
                );
        }
        $invoices = new InvoiceModel();
        $invoice = $invoices->find($id);
        if ($invoice && $invoice["status"] === "draft") {
            $invoices->update($id, ["status" => "sent"]);
        }
        return redirect()
            ->back()
            ->with(
                "success",
                ($withPdf ? "Invoice PDF" : "Invoice") .
                    " emailed to " .
                    $email .
                    ".",
            );
    }

    private function validationError(string $message)
    {
        return redirect()->back()->withInput()->with("error", $message);
    }

    private function notFound()
    {
        return redirect()
            ->to(base_url("admin/invoices"))
            ->with("error", "Invoice not found.");
    }
}
