<?php

namespace App\Controllers;

use App\Models\BarangModel;
use App\Models\PelangganModel;
use App\Models\PenjualanDetailModel;
use App\Models\PenjualanModel;
use App\Models\PenjualanPembayaranModel;
use Config\Database;
use Dompdf\Dompdf;
use Dompdf\Options;

class PenjualanController extends BaseController
{
    private PenjualanModel $penjualanModel;
    private PenjualanDetailModel $detailModel;
    private PenjualanPembayaranModel $pembayaranModel;
    private BarangModel $barangModel;
    private PelangganModel $pelangganModel;

    public function __construct()
    {
        $this->penjualanModel  = new PenjualanModel();
        $this->detailModel     = new PenjualanDetailModel();
        $this->pembayaranModel = new PenjualanPembayaranModel();
        $this->barangModel     = new BarangModel();
        $this->pelangganModel  = new PelangganModel();
    }

    public function index()
    {
        if (! $this->isLoggedIn()) {
            return redirect()->to('/login')->with('error', 'Silakan login terlebih dahulu.');
        }

        if ($redirect = $this->requireInventoryAccess('/admin/dashboard', 'Akses ditolak. Penjualan hanya dapat diakses oleh administrator atau kasir.')) {
            return $redirect;
        }

        $statusPembayaran = (string) $this->request->getGet('status_pembayaran');
        $statusPembayaran = in_array($statusPembayaran, ['lunas', 'piutang'], true) ? $statusPembayaran : '';

        $builder = $this->penjualanModel
            ->select('penjualan.*, pelanggan.nama AS nama_pelanggan, user.name AS nama_user, COUNT(penjualan_detail.id) AS total_item, COALESCE(SUM(penjualan_detail.jumlah), 0) AS total_jumlah')
            ->select('(SELECT COALESCE(SUM(jumlah), 0) FROM penjualan_pembayaran WHERE penjualan_pembayaran.penjualan_id = penjualan.id) AS total_dibayar', false)
            ->join('pelanggan', 'pelanggan.id = penjualan.pelanggan_id', 'left')
            ->join('user', 'user.id = penjualan.user_id', 'left')
            ->join('penjualan_detail', 'penjualan_detail.penjualan_id = penjualan.id', 'left');

        if ($statusPembayaran !== '') {
            $builder->where('penjualan.status_pembayaran', $statusPembayaran);
        }

        $transaksi = $builder
            ->groupBy('penjualan.id')
            ->orderBy('penjualan.id', 'DESC')
            ->findAll();

        return view('admin/penjualan/index', [
            'transaksi'         => $transaksi,
            'statusPembayaran'  => $statusPembayaran,
            'userName'          => session()->get('userName'),
            'userEmail'         => session()->get('userEmail'),
            'userRole'          => session()->get('userRole'),
        ]);
    }

    public function create()
    {
        if (! $this->isLoggedIn()) {
            return redirect()->to('/login')->with('error', 'Silakan login terlebih dahulu.');
        }

        if ($redirect = $this->requireInventoryAccess('/admin/penjualan')) {
            return $redirect;
        }

        $tanggal = date('Y-m-d');

        return view('admin/penjualan/create', [
            'pelanggan'       => $this->pelangganOptions(),
            'tanggalPenjualan' => $tanggal,
            'noTransaksi'     => $this->generateTransactionNumber($tanggal),
            'initialItems'    => [],
            'pajakPersen'     => $this->defaultTaxPercent(),
            'userName'        => session()->get('userName'),
            'userEmail'       => session()->get('userEmail'),
            'userRole'        => session()->get('userRole'),
        ]);
    }

    public function detail(int $id)
    {
        if (! $this->isLoggedIn()) {
            return redirect()->to('/login')->with('error', 'Silakan login terlebih dahulu.');
        }

        if ($redirect = $this->requireInventoryAccess('/admin/penjualan')) {
            return $redirect;
        }

        $transaksi = $this->penjualanWithCustomer($id);

        if (! $transaksi) {
            return redirect()->to('/admin/penjualan')->with('error', 'Transaksi penjualan tidak ditemukan.');
        }

        $details = $this->detailModel
            ->where('penjualan_id', $id)
            ->orderBy('id', 'ASC')
            ->findAll();

        return view('admin/penjualan/detail', [
            'transaksi'    => $transaksi,
            'details'      => $details,
            'totalDibayar' => $this->paymentTotal($id),
            'userName'     => session()->get('userName'),
            'userEmail'    => session()->get('userEmail'),
            'userRole'     => session()->get('userRole'),
        ]);
    }

    public function nota(int $id)
    {
        if (! $this->isLoggedIn()) {
            return redirect()->to('/login')->with('error', 'Silakan login terlebih dahulu.');
        }

        if ($redirect = $this->requireInventoryAccess('/admin/penjualan')) {
            return $redirect;
        }

        $data = $this->notaData($id);

        if ($data === null) {
            return redirect()->to('/admin/penjualan')->with('error', 'Transaksi penjualan tidak ditemukan.');
        }

        $receiptSize = $this->salesReceiptSize();

        if (in_array($receiptSize, ['a4', 'continuous_form'], true)) {
            return redirect()->to('/admin/penjualan/nota/pdf/' . $id);
        }

        return view($this->salesReceiptPrintView($receiptSize), $data);
    }

    public function notaPdf(int $id)
    {
        if (! $this->isLoggedIn()) {
            return redirect()->to('/login')->with('error', 'Silakan login terlebih dahulu.');
        }

        if ($redirect = $this->requireInventoryAccess('/admin/penjualan')) {
            return $redirect;
        }

        $data = $this->notaData($id);

        if ($data === null) {
            return redirect()->to('/admin/penjualan')->with('error', 'Transaksi penjualan tidak ditemukan.');
        }

        $options = new Options();
        $options->set('isHtml5ParserEnabled', true);
        $options->set('isRemoteEnabled', false);

        $receiptSize = $this->salesReceiptSize();

        if (in_array($receiptSize, ['thermal_58', 'thermal_80'], true)) {
            return redirect()->to('/admin/penjualan/nota/' . $id);
        }

        $paper = $this->salesReceiptPdfPaper($receiptSize);

        $dompdf = new Dompdf($options);
        $dompdf->loadHtml(view($this->salesReceiptPdfView($receiptSize), $data));
        $dompdf->setPaper($paper['size'], $paper['orientation']);
        $dompdf->render();

        $fileName = 'nota-' . preg_replace('/[^A-Za-z0-9_.-]/', '-', (string) $data['transaksi']['no_transaksi']) . '.pdf';

        return $this->response
            ->setHeader('Content-Type', 'application/pdf')
            ->setHeader('Content-Disposition', 'inline; filename="' . $fileName . '"')
            ->setBody($dompdf->output());
    }

    public function store()
    {
        if (! $this->isLoggedIn()) {
            return redirect()->to('/login')->with('error', 'Silakan login terlebih dahulu.');
        }

        if ($redirect = $this->requireInventoryAccess('/admin/penjualan')) {
            return $redirect;
        }

        if (! $this->validate($this->validationRules())) {
            return redirect()->back()->withInput()->with('errors', $this->validator->getErrors());
        }

        $items = $this->validatedItems();

        if ($items === []) {
            return redirect()->back()->withInput()->with('errors', [
                'items' => 'Minimal tambahkan 1 item penjualan.',
            ]);
        }

        $priceErrors = $this->belowCostPriceErrors($items);

        if ($priceErrors !== []) {
            return redirect()->back()->withInput()->with('errors', $priceErrors);
        }

        $stockErrors = $this->stockErrorsAfterDelta($this->outgoingStockDeltas([], $items));

        if ($stockErrors !== []) {
            return redirect()->back()->withInput()->with('errors', $stockErrors);
        }

        $tanggalPenjualan = (string) $this->request->getPost('tanggal_penjualan');
        $pajakPersen = $this->defaultTaxPercent();
        $totals = $this->saleTotals($items, $pajakPersen);
        $jumlahPembayaran = $this->paymentAmountFromPost();

        if ($jumlahPembayaran > $totals['total']) {
            return redirect()->back()->withInput()->with('errors', [
                'jumlah_pembayaran' => 'Jumlah pembayaran tidak boleh lebih besar dari total penjualan.',
            ]);
        }

        $db = Database::connect();
        $db->transStart();

        $penjualanId = $this->penjualanModel->insert([
            'no_transaksi'      => $this->generateTransactionNumber($tanggalPenjualan),
            'tanggal_penjualan' => $tanggalPenjualan,
            'pelanggan_id'      => $this->request->getPost('pelanggan_id'),
            'subtotal'          => $totals['subtotal'],
            'pajak_persen'      => $pajakPersen,
            'pajak_nominal'     => $totals['pajak'],
            'total_penjualan'   => $totals['total'],
            'status_pembayaran' => 'piutang',
            'user_id'           => session()->get('userId'),
            'catatan'           => $this->request->getPost('catatan') ?: null,
        ], true);

        $this->saveDetails((int) $penjualanId, $items);
        $this->saveInitialPayment((int) $penjualanId, $tanggalPenjualan, $jumlahPembayaran);
        $this->refreshPaymentStatus((int) $penjualanId);
        $this->applyStockDeltas($this->outgoingStockDeltas([], $items));

        $db->transComplete();

        if (! $db->transStatus()) {
            return redirect()->back()->withInput()->with('error', 'Gagal menyimpan transaksi penjualan.');
        }

        return redirect()->to('/admin/penjualan')->with('success', 'Transaksi penjualan berhasil ditambahkan.');
    }

    public function edit(int $id)
    {
        if (! $this->isLoggedIn()) {
            return redirect()->to('/login')->with('error', 'Silakan login terlebih dahulu.');
        }

        if ($redirect = $this->requireAdministrator('/admin/penjualan')) {
            return $redirect;
        }

        $transaksi = $this->penjualanModel->find($id);

        if (! $transaksi) {
            return redirect()->to('/admin/penjualan')->with('error', 'Transaksi penjualan tidak ditemukan.');
        }

        $details = $this->detailModel
            ->where('penjualan_id', $id)
            ->orderBy('id', 'ASC')
            ->findAll();

        return view('admin/penjualan/edit', [
            'transaksi'          => $transaksi,
            'pelanggan'          => $this->pelangganOptions(),
            'initialItems'       => $this->detailsForForm($details),
            'totalDibayar'       => $this->paymentTotal($id),
            'showInitialPayment' => false,
            'userName'           => session()->get('userName'),
            'userEmail'          => session()->get('userEmail'),
            'userRole'           => session()->get('userRole'),
        ]);
    }

    public function update(int $id)
    {
        if (! $this->isLoggedIn()) {
            return redirect()->to('/login')->with('error', 'Silakan login terlebih dahulu.');
        }

        if ($redirect = $this->requireAdministrator('/admin/penjualan')) {
            return $redirect;
        }

        $transaksi = $this->penjualanModel->find($id);

        if (! $transaksi) {
            return redirect()->to('/admin/penjualan')->with('error', 'Transaksi penjualan tidak ditemukan.');
        }

        if (! $this->validate($this->validationRules())) {
            return redirect()->back()->withInput()->with('errors', $this->validator->getErrors());
        }

        $items = $this->validatedItems();

        if ($items === []) {
            return redirect()->back()->withInput()->with('errors', [
                'items' => 'Minimal tambahkan 1 item penjualan.',
            ]);
        }

        $priceErrors = $this->belowCostPriceErrors($items);

        if ($priceErrors !== []) {
            return redirect()->back()->withInput()->with('errors', $priceErrors);
        }

        $oldItems = $this->detailModel
            ->where('penjualan_id', $id)
            ->findAll();
        $deltas = $this->outgoingStockDeltas($oldItems, $items);
        $stockErrors = $this->stockErrorsAfterDelta($deltas);

        if ($stockErrors !== []) {
            return redirect()->back()->withInput()->with('errors', $stockErrors);
        }

        $pajakPersen = (int) ($transaksi['pajak_persen'] ?? $this->defaultTaxPercent());
        $totals = $this->saleTotals($items, $pajakPersen);
        $totalDibayar = $this->paymentTotal($id);

        if ($totalDibayar > $totals['total']) {
            return redirect()->back()->withInput()->with('errors', [
                'pembayaran' => 'Jumlah pembayaran tidak boleh lebih besar dari jumlah penjualan. Silakan hapus riwayat pembayaran dulu untuk menyesuaikan jumlah pembayaran penjualan ini.',
            ]);
        }

        $tanggalPenjualan = (string) $this->request->getPost('tanggal_penjualan');
        $db = Database::connect();
        $db->transStart();

        $this->detailModel->where('penjualan_id', $id)->delete();

        $this->penjualanModel->update($id, [
            'no_transaksi'      => $this->generateTransactionNumber($tanggalPenjualan, $id),
            'tanggal_penjualan' => $tanggalPenjualan,
            'pelanggan_id'      => $this->request->getPost('pelanggan_id'),
            'subtotal'          => $totals['subtotal'],
            'pajak_persen'      => $pajakPersen,
            'pajak_nominal'     => $totals['pajak'],
            'total_penjualan'   => $totals['total'],
            'catatan'           => $this->request->getPost('catatan') ?: null,
        ]);

        $this->saveDetails($id, $items);
        $this->applyStockDeltas($deltas);
        $this->refreshPaymentStatus($id);

        $db->transComplete();

        if (! $db->transStatus()) {
            return redirect()->back()->withInput()->with('error', 'Gagal memperbarui transaksi penjualan.');
        }

        return redirect()->to('/admin/penjualan')->with('success', 'Transaksi penjualan berhasil diperbarui.');
    }

    public function delete(int $id)
    {
        if (! $this->isLoggedIn()) {
            return redirect()->to('/login')->with('error', 'Silakan login terlebih dahulu.');
        }

        if ($redirect = $this->requireAdministrator('/admin/penjualan')) {
            return $redirect;
        }

        $transaksi = $this->penjualanModel->find($id);

        if (! $transaksi) {
            return redirect()->to('/admin/penjualan')->with('error', 'Transaksi penjualan tidak ditemukan.');
        }

        $items = $this->detailModel->where('penjualan_id', $id)->findAll();

        $db = Database::connect();
        $db->transStart();

        $this->applyStockDeltas($this->outgoingStockDeltas($items, []));
        $this->penjualanModel->delete($id);

        $db->transComplete();

        if (! $db->transStatus()) {
            return redirect()->to('/admin/penjualan')->with('error', 'Gagal menghapus transaksi penjualan.');
        }

        return redirect()->to('/admin/penjualan')->with('success', 'Transaksi penjualan berhasil dihapus dan stok dikembalikan.');
    }

    public function cicilan(int $id)
    {
        if (! $this->isLoggedIn()) {
            return redirect()->to('/login')->with('error', 'Silakan login terlebih dahulu.');
        }

        if ($redirect = $this->requireInventoryAccess('/admin/penjualan')) {
            return $redirect;
        }

        $transaksi = $this->penjualanWithCustomer($id);

        if (! $transaksi) {
            return redirect()->to('/admin/penjualan')->with('error', 'Transaksi penjualan tidak ditemukan.');
        }

        $totalPenjualan = (int) ($transaksi['total_penjualan'] ?? 0);
        $totalDibayar = $this->paymentTotal($id);

        return view('admin/penjualan/cicilan', [
            'transaksi'      => $transaksi,
            'payments'       => $this->payments($id),
            'totalPenjualan' => $totalPenjualan,
            'totalDibayar'   => $totalDibayar,
            'sisaPiutang'    => max(0, $totalPenjualan - $totalDibayar),
            'userName'       => session()->get('userName'),
            'userEmail'      => session()->get('userEmail'),
            'userRole'       => session()->get('userRole'),
        ]);
    }

    public function storeCicilan(int $id)
    {
        if (! $this->isLoggedIn()) {
            return redirect()->to('/login')->with('error', 'Silakan login terlebih dahulu.');
        }

        if ($redirect = $this->requireInventoryAccess('/admin/penjualan')) {
            return $redirect;
        }

        $transaksi = $this->penjualanModel->find($id);

        if (! $transaksi) {
            return redirect()->to('/admin/penjualan')->with('error', 'Transaksi penjualan tidak ditemukan.');
        }

        $rules = [
            'tanggal_pembayaran' => 'required|valid_date[Y-m-d]',
            'jumlah_setoran'     => 'required|max_length[30]',
            'catatan_pembayaran' => 'permit_empty|max_length[1000]',
        ];

        if (! $this->validate($rules)) {
            return redirect()->back()->withInput()->with('errors', $this->validator->getErrors());
        }

        $jumlahSetoran = $this->paymentAmountFromPost('jumlah_setoran');

        if ($jumlahSetoran < 1) {
            return redirect()->back()->withInput()->with('errors', [
                'jumlah_setoran' => 'Jumlah setoran wajib lebih dari 0.',
            ]);
        }

        $totalPenjualan = (int) ($transaksi['total_penjualan'] ?? 0);
        $totalDibayar = $this->paymentTotal($id);

        if (($totalDibayar + $jumlahSetoran) > $totalPenjualan) {
            return redirect()->back()->withInput()->with('errors', [
                'jumlah_setoran' => 'Jumlah setoran tidak boleh lebih besar dari sisa piutang.',
            ]);
        }

        $db = Database::connect();
        $db->transStart();

        $this->pembayaranModel->insert([
            'penjualan_id'        => $id,
            'tanggal_pembayaran'  => $this->request->getPost('tanggal_pembayaran'),
            'jumlah'              => $jumlahSetoran,
            'is_pembayaran_awal'  => 0,
            'catatan'             => $this->request->getPost('catatan_pembayaran') ?: null,
            'user_id'             => session()->get('userId'),
        ]);

        $this->refreshPaymentStatus($id);

        $db->transComplete();

        if (! $db->transStatus()) {
            return redirect()->back()->withInput()->with('error', 'Gagal menyimpan cicilan penjualan.');
        }

        return redirect()->to('/admin/penjualan/cicilan/' . $id)->with('success', 'Cicilan penjualan berhasil disimpan.');
    }

    public function deleteCicilan(int $id, int $paymentId)
    {
        if (! $this->isLoggedIn()) {
            return redirect()->to('/login')->with('error', 'Silakan login terlebih dahulu.');
        }

        if ($redirect = $this->requireInventoryAccess('/admin/penjualan')) {
            return $redirect;
        }

        $transaksi = $this->penjualanModel->find($id);

        if (! $transaksi) {
            return redirect()->to('/admin/penjualan')->with('error', 'Transaksi penjualan tidak ditemukan.');
        }

        $payment = $this->pembayaranModel
            ->where('penjualan_id', $id)
            ->where('id', $paymentId)
            ->first();

        if (! $payment) {
            return redirect()->to('/admin/penjualan/cicilan/' . $id)->with('error', 'Pembayaran tidak ditemukan.');
        }

        $lastPayment = $this->lastPayment($id);

        if (! $lastPayment || (int) $lastPayment['id'] !== (int) $payment['id']) {
            return redirect()->to('/admin/penjualan/cicilan/' . $id)->with('error', 'Hanya pembayaran terakhir yang dapat dihapus.');
        }

        $db = Database::connect();
        $db->transStart();

        $this->pembayaranModel->delete($paymentId);
        $this->refreshPaymentStatus($id);

        $db->transComplete();

        if (! $db->transStatus()) {
            return redirect()->to('/admin/penjualan/cicilan/' . $id)->with('error', 'Gagal menghapus pembayaran.');
        }

        return redirect()->to('/admin/penjualan/cicilan/' . $id)->with('success', 'Pembayaran terakhir berhasil dihapus.');
    }

    public function generateNumber()
    {
        if (! $this->isLoggedIn() || ! $this->isInventoryRole()) {
            return $this->response->setStatusCode(403)->setJSON(['message' => 'Akses ditolak.']);
        }

        $tanggal = (string) ($this->request->getGet('tanggal_penjualan') ?: date('Y-m-d'));

        if (! $this->isValidDate($tanggal)) {
            return $this->response->setStatusCode(422)->setJSON(['message' => 'Tanggal tidak valid.']);
        }

        return $this->response->setJSON([
            'no_transaksi' => $this->generateTransactionNumber($tanggal),
        ]);
    }

    public function lookupBarang()
    {
        if (! $this->isLoggedIn() || ! $this->isInventoryRole()) {
            return $this->response->setStatusCode(403)->setJSON(['message' => 'Akses ditolak.']);
        }

        $kode = trim((string) $this->request->getGet('kode_barang'));

        if ($kode === '') {
            return $this->response->setStatusCode(422)->setJSON(['message' => 'Kode barang wajib diisi.']);
        }

        $barang = $this->barangModel
            ->select('barang.id, barang.kode_barang, barang.nama_barang, barang.satuan, barang.stok, barang.harga_beli, barang.harga_jual, kategori.nama_kategori')
            ->join('kategori', 'kategori.id = barang.kategori_id', 'left')
            ->where('barang.kode_barang', strtoupper($kode))
            ->first();

        if (! $barang) {
            return $this->response->setStatusCode(404)->setJSON(['message' => 'Barang tidak ditemukan.']);
        }

        return $this->response->setJSON(['barang' => $barang]);
    }

    public function barangDataTable()
    {
        if (! $this->isLoggedIn() || ! $this->isInventoryRole()) {
            return $this->response->setStatusCode(403)->setJSON(['message' => 'Akses ditolak.']);
        }

        $draw = (int) ($this->request->getGet('draw') ?? 1);
        $start = max(0, (int) ($this->request->getGet('start') ?? 0));
        $length = (int) ($this->request->getGet('length') ?? 10);
        $length = $length > 0 ? min($length, 100) : 10;
        $search = trim((string) ($this->request->getGet('search')['value'] ?? ''));

        $db = Database::connect();
        $recordsTotal = $db->table('barang')->countAllResults();
        $filteredBuilder = $this->barangModel
            ->select('barang.id, barang.kode_barang, barang.nama_barang, barang.satuan, barang.stok, barang.harga_beli, barang.harga_jual, kategori.nama_kategori')
            ->join('kategori', 'kategori.id = barang.kategori_id', 'left');

        if ($search !== '') {
            $filteredBuilder
                ->groupStart()
                ->like('barang.kode_barang', $search)
                ->orLike('barang.nama_barang', $search)
                ->orLike('kategori.nama_kategori', $search)
                ->groupEnd();
        }

        $recordsFiltered = $filteredBuilder->countAllResults(false);
        $data = $filteredBuilder
            ->orderBy('barang.nama_barang', 'ASC')
            ->findAll($length, $start);

        $rows = array_map(static function (array $item): array {
            $stok = (int) ($item['stok'] ?? 0);
            $disabled = $stok <= 0 ? ' disabled' : '';

            return [
                esc($item['kode_barang']),
                esc($item['nama_barang']),
                esc($item['nama_kategori'] ?? '-'),
                esc($item['satuan']),
                esc((string) $stok),
                'Rp ' . esc(number_format((float) ($item['harga_jual'] ?? 0), 0, ',', '.')),
                '<button type="button" class="btn btn-sm btn-dark pilih-barang-btn" data-id="' . esc($item['id'], 'attr') . '" data-kode="' . esc($item['kode_barang'], 'attr') . '" data-nama="' . esc($item['nama_barang'], 'attr') . '" data-satuan="' . esc($item['satuan'], 'attr') . '" data-stok="' . esc((string) $stok, 'attr') . '" data-harga-beli="' . esc((string) ($item['harga_beli'] ?? 0), 'attr') . '" data-harga-jual="' . esc((string) ($item['harga_jual'] ?? 0), 'attr') . '"' . $disabled . '><i class="bi bi-check2"></i>Pilih</button>',
            ];
        }, $data);

        return $this->response->setJSON([
            'draw'            => $draw,
            'recordsTotal'    => $recordsTotal,
            'recordsFiltered' => $recordsFiltered,
            'data'            => $rows,
        ]);
    }

    private function validationRules(): array
    {
        return [
            'tanggal_penjualan' => 'required|valid_date[Y-m-d]',
            'pelanggan_id'      => 'required|is_not_unique[pelanggan.id]',
            'pajak_persen'      => 'required|integer|greater_than_equal_to[0]|less_than_equal_to[100]',
            'jumlah_pembayaran' => 'permit_empty|max_length[30]',
            'catatan'           => 'permit_empty|max_length[1000]',
        ];
    }

    private function validatedItems(): array
    {
        $rawItems = json_decode((string) $this->request->getPost('items_json'), true);

        if (! is_array($rawItems)) {
            return [];
        }

        $items = [];

        foreach ($rawItems as $rawItem) {
            $barangId = (int) ($rawItem['barang_id'] ?? 0);
            $jumlah = (int) ($rawItem['jumlah'] ?? 0);
            $hargaJual = (int) round((float) ($rawItem['harga_jual'] ?? -1));

            if ($barangId < 1 || $jumlah < 1 || $hargaJual < 0) {
                continue;
            }

            $barang = $this->barangModel->find($barangId);

            if (! $barang) {
                continue;
            }

            $previousJumlah = (int) ($items[$barangId]['jumlah'] ?? 0);
            $previousHargaJual = (int) ($items[$barangId]['harga_jual'] ?? 0);
            $newJumlah = $previousJumlah + $jumlah;
            $newHargaJual = $previousJumlah > 0
                ? (($previousJumlah * $previousHargaJual) + ($jumlah * $hargaJual)) / $newJumlah
                : $hargaJual;
            $hargaBeli = (int) ($barang['harga_beli'] ?? 0);

            $items[$barangId] = [
                'barang_id'   => $barangId,
                'kode_barang' => $barang['kode_barang'],
                'nama_barang' => $barang['nama_barang'],
                'satuan'      => $barang['satuan'],
                'jumlah'      => $newJumlah,
                'harga_jual'  => (int) round($newHargaJual),
                'harga_beli'  => $hargaBeli,
            ];
        }

        return array_values($items);
    }

    private function belowCostPriceErrors(array $items): array
    {
        $errors = [];

        foreach ($items as $item) {
            $hargaJual = (int) ($item['harga_jual'] ?? 0);
            $hargaBeli = (int) ($item['harga_beli'] ?? 0);

            if ($hargaBeli > 0 && $hargaJual < $hargaBeli) {
                $errors[] = 'Harga jual ' . $item['kode_barang'] . ' - ' . $item['nama_barang'] . ' tidak boleh lebih kecil dari modal. Modal Rp ' . number_format($hargaBeli, 0, ',', '.') . '.';
            }
        }

        return $errors;
    }

    private function saveDetails(int $penjualanId, array $items): void
    {
        foreach ($items as $item) {
            $jumlah = (int) $item['jumlah'];
            $hargaJual = (int) $item['harga_jual'];
            $hargaBeli = (int) ($item['harga_beli'] ?? 0);

            $this->detailModel->insert([
                'penjualan_id' => $penjualanId,
                'barang_id'    => $item['barang_id'],
                'kode_barang'  => $item['kode_barang'],
                'nama_barang'  => $item['nama_barang'],
                'satuan'       => $item['satuan'],
                'jumlah'       => $jumlah,
                'harga_jual'   => $hargaJual,
                'harga_beli'   => $hargaBeli,
                'subtotal'     => $jumlah * $hargaJual,
                'total_modal'  => $jumlah * $hargaBeli,
            ]);
        }
    }

    private function outgoingStockDeltas(array $oldItems, array $newItems): array
    {
        $deltas = [];

        foreach ($oldItems as $item) {
            $barangId = (int) $item['barang_id'];
            $deltas[$barangId] = ($deltas[$barangId] ?? 0) + (int) $item['jumlah'];
        }

        foreach ($newItems as $item) {
            $barangId = (int) $item['barang_id'];
            $deltas[$barangId] = ($deltas[$barangId] ?? 0) - (int) $item['jumlah'];
        }

        return array_filter($deltas, static fn (int $delta): bool => $delta !== 0);
    }

    private function stockErrorsAfterDelta(array $deltas): array
    {
        $errors = [];

        foreach ($deltas as $barangId => $delta) {
            $barang = $this->barangModel->find((int) $barangId);

            if (! $barang) {
                continue;
            }

            $currentStock = (int) ($barang['stok'] ?? 0);
            $newStock = $currentStock + (int) $delta;

            if ($newStock < 0) {
                $errors[] = 'Transaksi tidak dapat diproses karena stok ' . $barang['kode_barang'] . ' - ' . $barang['nama_barang'] . ' tidak cukup. Stok saat ini ' . $currentStock . '.';
            }
        }

        return $errors;
    }

    private function applyStockDeltas(array $deltas): void
    {
        foreach ($deltas as $barangId => $delta) {
            $barang = $this->barangModel->find((int) $barangId);

            if (! $barang) {
                continue;
            }

            $this->barangModel->update((int) $barangId, [
                'stok' => (int) ($barang['stok'] ?? 0) + (int) $delta,
            ]);
        }
    }

    private function detailsForForm(array $details): array
    {
        return array_map(function (array $detail): array {
            $barang = $this->barangModel->find((int) $detail['barang_id']);

            return [
                'barang_id'   => (int) $detail['barang_id'],
                'kode_barang' => $detail['kode_barang'],
                'nama_barang' => $detail['nama_barang'],
                'satuan'      => $detail['satuan'],
                'jumlah'      => (int) $detail['jumlah'],
                'harga_jual'  => (int) ($detail['harga_jual'] ?? 0),
                'harga_beli'  => (int) ($detail['harga_beli'] ?? ($barang['harga_beli'] ?? 0)),
                'stok_saat_ini' => (int) ($barang['stok'] ?? 0) + (int) $detail['jumlah'],
            ];
        }, $details);
    }

    private function saleTotals(array $items, int $pajakPersen): array
    {
        $subtotal = 0;

        foreach ($items as $item) {
            $subtotal += (int) $item['jumlah'] * (int) $item['harga_jual'];
        }

        $pajak = (int) round($subtotal * $pajakPersen / 100);

        return [
            'subtotal' => $subtotal,
            'pajak'    => $pajak,
            'total'    => $subtotal + $pajak,
        ];
    }

    private function paymentAmountFromPost(string $field = 'jumlah_pembayaran'): int
    {
        $value = (string) $this->request->getPost($field);
        $digits = preg_replace('/\D/', '', $value) ?: '0';

        return max(0, (int) $digits);
    }

    private function defaultTaxPercent(): int
    {
        $setting = function_exists('app_setting') ? app_setting() : [];

        return max(0, min(100, (int) ($setting['pajak_persen'] ?? 11)));
    }

    private function salesReceiptSize(): string
    {
        $setting = function_exists('app_setting') ? app_setting() : [];
        $size = (string) ($setting['nota_penjualan_ukuran'] ?? 'thermal_58');

        return in_array($size, ['thermal_58', 'thermal_80', 'a4', 'continuous_form'], true) ? $size : 'thermal_58';
    }

    private function salesReceiptPrintView(string $size): string
    {
        return $size === 'thermal_80'
            ? 'admin/penjualan/nota_thermal_80'
            : 'admin/penjualan/nota_thermal_58';
    }

    private function salesReceiptPdfView(string $size): string
    {
        return match ($size) {
            'thermal_80' => 'admin/penjualan/nota_pdf_thermal_80',
            'a4' => 'admin/penjualan/nota_pdf_a4',
            'continuous_form' => 'admin/penjualan/nota_pdf_continuous_form',
            default => 'admin/penjualan/nota_pdf_thermal_58',
        };
    }

    private function salesReceiptPdfPaper(string $size): array
    {
        return match ($size) {
            'thermal_80' => ['size' => [0, 0, 226.77, 841.89], 'orientation' => 'portrait'],
            'a4' => ['size' => 'A4', 'orientation' => 'portrait'],
            'continuous_form' => ['size' => [0, 0, 595.28, 396.85], 'orientation' => 'portrait'],
            default => ['size' => [0, 0, 164.41, 841.89], 'orientation' => 'portrait'],
        };
    }

    private function saveInitialPayment(int $penjualanId, string $tanggalPenjualan, int $amount): void
    {
        if ($amount < 1) {
            return;
        }

        $this->pembayaranModel->insert([
            'penjualan_id'        => $penjualanId,
            'tanggal_pembayaran'  => $tanggalPenjualan,
            'jumlah'              => $amount,
            'is_pembayaran_awal'  => 1,
            'catatan'             => 'Pembayaran awal',
            'user_id'             => session()->get('userId'),
        ]);
    }

    private function notaData(int $id): ?array
    {
        $transaksi = $this->penjualanWithCustomer($id);

        if (! $transaksi) {
            return null;
        }

        return [
            'transaksi'    => $transaksi,
            'details'      => $this->detailModel
                ->where('penjualan_id', $id)
                ->orderBy('id', 'ASC')
                ->findAll(),
            'totalDibayar' => $this->paymentTotal($id),
            'userName'     => session()->get('userName'),
        ];
    }

    private function paymentTotal(int $penjualanId): int
    {
        $row = Database::connect()
            ->table('penjualan_pembayaran')
            ->select('COALESCE(SUM(jumlah), 0) AS total', false)
            ->where('penjualan_id', $penjualanId)
            ->get()
            ->getRowArray();

        return (int) ($row['total'] ?? 0);
    }

    private function payments(int $penjualanId): array
    {
        return $this->pembayaranModel
            ->select('penjualan_pembayaran.*, user.name AS nama_user')
            ->join('user', 'user.id = penjualan_pembayaran.user_id', 'left')
            ->where('penjualan_id', $penjualanId)
            ->orderBy('tanggal_pembayaran', 'ASC')
            ->orderBy('id', 'ASC')
            ->findAll();
    }

    private function lastPayment(int $penjualanId): ?array
    {
        return $this->pembayaranModel
            ->where('penjualan_id', $penjualanId)
            ->orderBy('tanggal_pembayaran', 'DESC')
            ->orderBy('id', 'DESC')
            ->first();
    }

    private function refreshPaymentStatus(int $penjualanId): void
    {
        $penjualan = $this->penjualanModel->find($penjualanId);

        if (! $penjualan) {
            return;
        }

        $totalPenjualan = (int) ($penjualan['total_penjualan'] ?? 0);
        $totalDibayar = $this->paymentTotal($penjualanId);
        $status = $totalPenjualan > 0 && $totalDibayar >= $totalPenjualan ? 'lunas' : 'piutang';

        $this->penjualanModel->update($penjualanId, [
            'status_pembayaran' => $status,
        ]);
    }

    private function penjualanWithCustomer(int $id): ?array
    {
        return $this->penjualanModel
            ->select('penjualan.*, pelanggan.nama AS nama_pelanggan, pelanggan.no_hp AS no_hp_pelanggan, pelanggan.alamat AS alamat_pelanggan, user.name AS nama_user')
            ->join('pelanggan', 'pelanggan.id = penjualan.pelanggan_id', 'left')
            ->join('user', 'user.id = penjualan.user_id', 'left')
            ->where('penjualan.id', $id)
            ->first();
    }

    private function pelangganOptions(): array
    {
        return $this->pelangganModel
            ->orderBy('nama', 'ASC')
            ->findAll();
    }

    private function generateTransactionNumber(string $tanggal, ?int $excludeId = null): string
    {
        $date = date_create($tanggal) ?: date_create();
        $prefix = $date->format('Ymd');
        $builder = $this->penjualanModel
            ->select('no_transaksi')
            ->like('no_transaksi', $prefix, 'after')
            ->orderBy('no_transaksi', 'DESC');

        if ($excludeId !== null) {
            $builder->where('id !=', $excludeId);
        }

        $last = $builder->first();
        $nextNumber = 1;

        if ($last && preg_match('/^' . preg_quote($prefix, '/') . '(\d+)$/', $last['no_transaksi'], $matches)) {
            $nextNumber = ((int) $matches[1]) + 1;
        }

        return $prefix . str_pad((string) $nextNumber, 3, '0', STR_PAD_LEFT);
    }

    private function isValidDate(string $date): bool
    {
        $parsed = date_create_from_format('Y-m-d', $date);

        return $parsed !== false && $parsed->format('Y-m-d') === $date;
    }

    private function isLoggedIn(): bool
    {
        return (bool) session()->get('isLoggedIn') && $this->isUserAccount();
    }
}
