<?php

namespace App\Controllers;

use App\Libraries\Code128Barcode;
use App\Libraries\BarangImportXlsx;
use App\Libraries\SimpleQrCode;
use App\Libraries\TemporaryImageUpload;
use App\Models\BarangModel;
use App\Models\KategoriModel;
use Config\Database;
use Dompdf\Dompdf;
use Dompdf\Options;
use InvalidArgumentException;
use RuntimeException;

class BarangController extends BaseController
{
    private BarangModel $barangModel;
    private KategoriModel $kategoriModel;
    private TemporaryImageUpload $temporaryImageUpload;
    private string $uploadPath;

    public function __construct()
    {
        $this->barangModel          = new BarangModel();
        $this->kategoriModel        = new KategoriModel();
        $this->temporaryImageUpload = new TemporaryImageUpload();
        $this->uploadPath           = FCPATH . 'uploads/barang';
    }

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

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

        $stockFilter = (string) $this->request->getGet('stok');
        $barangQuery = $this->barangModel
            ->select('barang.*, kategori.nama_kategori, kategori.icon_kategori')
            ->select('(SELECT COUNT(*) FROM pembelian_detail WHERE pembelian_detail.barang_id = barang.id) AS total_pembelian', false)
            ->select('(SELECT COUNT(*) FROM penjualan_detail WHERE penjualan_detail.barang_id = barang.id) AS total_penjualan', false)
            ->select('(SELECT COUNT(*) FROM service_barang WHERE service_barang.barang_id = barang.id) AS total_service', false)
            ->select('(SELECT COUNT(*) FROM stok_penyesuaian WHERE stok_penyesuaian.barang_id = barang.id) AS total_stok_penyesuaian', false)
            ->join('kategori', 'kategori.id = barang.kategori_id', 'left');

        if ($stockFilter === 'hampir-habis') {
            $barangQuery->where('barang.stok <= barang.stok_minimum');
        }

        $barang = $barangQuery
            ->orderBy('barang.id', 'DESC')
            ->findAll();

        return view('admin/barang/index', [
            'barang'      => $barang,
            'stockFilter' => $stockFilter,
            '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->requireAdministrator('/admin/barang')) {
            return $redirect;
        }

        return view('admin/barang/create', [
            'kategori'   => $this->kategoriOptions(),
            'kodeBarang' => $this->nextBarangCode(),
            'userName'   => session()->get('userName'),
            'userEmail'  => session()->get('userEmail'),
            'userRole'   => session()->get('userRole'),
        ]);
    }

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

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

        return view('admin/barang/import', [
            'kategori'  => $this->kategoriOptions(),
            'userName'  => session()->get('userName'),
            'userEmail' => session()->get('userEmail'),
            'userRole'  => session()->get('userRole'),
        ]);
    }

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

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

        $content = (new BarangImportXlsx())->template($this->kategoriOptions());

        return $this->response
            ->setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
            ->setHeader('Content-Disposition', 'attachment; filename="format-import-barang.xlsx"')
            ->setBody($content);
    }

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

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

        $rules = [
            'file_import' => 'uploaded[file_import]|max_size[file_import,5120]|ext_in[file_import,xlsx]',
        ];

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

        $file = $this->request->getFile('file_import');

        if (! $file || ! $file->isValid()) {
            return redirect()->back()->with('error', 'File import tidak valid.');
        }

        try {
            $rows = (new BarangImportXlsx())->read($file->getTempName());
        } catch (RuntimeException $exception) {
            return redirect()->back()->with('error', $exception->getMessage());
        }

        [$data, $errors] = $this->validateImportRows($rows);

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

        if ($data === []) {
            return redirect()->back()->with('error', 'Tidak ada data barang yang dapat diimport.');
        }

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

        foreach ($data as $item) {
            $this->barangModel->insert($item);
        }

        $db->transComplete();

        if (! $db->transStatus()) {
            return redirect()->back()->with('error', 'Gagal mengimport data barang.');
        }

        return redirect()->to('/admin/barang')->with('success', count($data) . ' data barang berhasil diimport.');
    }

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

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

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

        if ($priceError = $this->sellingPriceError()) {
            return redirect()->back()->withInput()->with('errors', [
                'harga_jual' => $priceError,
            ]);
        }

        try {
            $foto = $this->moveTemporaryPhoto();
        } catch (RuntimeException $exception) {
            return redirect()->back()->withInput()->with('errors', [
                'foto' => $exception->getMessage(),
            ]);
        }

        $data = $this->formData($this->nextBarangCode());
        $data['harga_beli_awal'] = $data['harga_beli'];
        $data['foto'] = $foto;
        $data['stok'] = $data['stok_awal'];

        $this->barangModel->insert($data);

        return redirect()->to('/admin/barang')->with('success', 'Data barang 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/barang')) {
            return $redirect;
        }

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

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

        $usage = $this->barangUsage($id);

        return view('admin/barang/edit', [
            'barang'           => $barang,
            'kategori'         => $this->kategoriOptions(),
            'disableStokAwal'  => $usage['pembelian'] > 0 || $usage['penjualan'] > 0 || $usage['service'] > 0 || $usage['stok_penyesuaian'] > 0,
            '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/barang')) {
            return $redirect;
        }

        $barang = $this->barangModel
            ->select('barang.*, kategori.nama_kategori, kategori.icon_kategori')
            ->join('kategori', 'kategori.id = barang.kategori_id', 'left')
            ->where('barang.id', $id)
            ->first();

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

        return view('admin/barang/detail', [
            'barang'       => $barang,
            'stockHistory' => $this->stockHistory($barang),
            'qrcode'       => $this->svgDataUri((new SimpleQrCode())->toSvg($barang['qrcode'], 8, 4)),
            'barcode'      => $this->pngDataUri((new Code128Barcode())->toPng($barang['barcode'])),
            '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/barang')) {
            return $redirect;
        }

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

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

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

        if ($priceError = $this->sellingPriceError()) {
            return redirect()->back()->withInput()->with('errors', [
                'harga_jual' => $priceError,
            ]);
        }

        $usage = $this->barangUsage($id);
        $isUsedInTransaction = $usage['pembelian'] > 0 || $usage['penjualan'] > 0 || $usage['service'] > 0 || $usage['stok_penyesuaian'] > 0;
        $data = $this->formData();
        $oldStokAwal = (int) ($barang['stok_awal'] ?? 0);
        $currentStok = (int) ($barang['stok'] ?? 0);

        if ($isUsedInTransaction) {
            $data['stok_awal'] = $oldStokAwal;
            $data['stok'] = $currentStok;
            $data['harga_beli_awal'] = $barang['harga_beli_awal'] ?? $barang['harga_beli'] ?? 0;
            $data['harga_beli'] = $barang['harga_beli'] ?? 0;
        } else {
            $data['stok'] = max(0, $currentStok + ((int) $data['stok_awal'] - $oldStokAwal));
            $data['harga_beli_awal'] = $data['harga_beli'];
        }

        try {
            $foto = $this->moveTemporaryPhoto();
        } catch (RuntimeException $exception) {
            return redirect()->back()->withInput()->with('errors', [
                'foto' => $exception->getMessage(),
            ]);
        }

        if ($foto !== null) {
            $data['foto'] = $foto;
            $this->deletePhoto($barang['foto'] ?? null);
        }

        $this->barangModel->update($id, $data);

        return redirect()->to('/admin/barang')->with('success', 'Data barang 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/barang')) {
            return $redirect;
        }

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

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

        $usage = $this->barangUsage($id);

        if ($usage['pembelian'] > 0 || $usage['penjualan'] > 0 || $usage['service'] > 0) {
            return redirect()->to('/admin/barang')->with(
                'error',
                'Data barang tidak dapat dihapus karena sudah digunakan pada transaksi pembelian, penjualan, atau service. Hapus transaksi terkait terlebih dahulu.'
            );
        }

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

        $db->table('stok_penyesuaian')->where('barang_id', $id)->delete();
        $this->barangModel->delete($id);

        $db->transComplete();

        if (! $db->transStatus()) {
            return redirect()->to('/admin/barang')->with('error', 'Data barang gagal dihapus.');
        }

        $this->deletePhoto($barang['foto'] ?? null);

        $message = 'Data barang berhasil dihapus.';

        if ($usage['stok_penyesuaian'] > 0) {
            $message .= ' Data penyesuaian stok terkait juga ikut dihapus.';
        }

        return redirect()->to('/admin/barang')->with('success', $message);
    }

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

        $kodeBarang = strtoupper(trim((string) $this->request->getGet('kode_barang')));
        $excludeId = (int) ($this->request->getGet('exclude_id') ?? 0);

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

        $builder = $this->barangModel->where('kode_barang', $kodeBarang);

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

        $exists = $builder->first() !== null;

        return $this->response->setJSON([
            'available' => ! $exists,
            'message'   => $exists
                ? 'Kode barang sudah digunakan. Gunakan kode lain.'
                : 'Kode barang tersedia.',
        ]);
    }

    public function downloadQrCode(int $id)
    {
        if ($redirect = $this->guardDownload()) {
            return $redirect;
        }

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

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

        try {
            $svg = (new SimpleQrCode())->toSvg($barang['qrcode']);
        } catch (InvalidArgumentException $exception) {
            return redirect()->to('/admin/barang')->with('error', $exception->getMessage());
        }

        return $this->downloadSvg($this->labeledQrSvg($svg, $barang), 'qrcode-' . $barang['kode_barang'] . '.svg');
    }

    public function downloadBarcode(int $id)
    {
        if ($redirect = $this->guardDownload()) {
            return $redirect;
        }

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

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

        try {
            $png = (new Code128Barcode())->toPng($barang['barcode']);
        } catch (InvalidArgumentException $exception) {
            return redirect()->to('/admin/barang')->with('error', $exception->getMessage());
        }

        return $this->downloadPng($this->labeledBarcodePng($png, $barang), 'barcode-' . $barang['barcode'] . '.png');
    }

    public function printMassQrCode()
    {
        if ($redirect = $this->guardDownload()) {
            return $redirect;
        }

        $barang = $this->barangModel
            ->orderBy('nama_barang', 'ASC')
            ->findAll();

        $labels = [];
        $qrCode = new SimpleQrCode();

        foreach ($barang as $item) {
            try {
                $svg = $qrCode->toSvg($item['qrcode'], 8, 4);
            } catch (InvalidArgumentException $exception) {
                continue;
            }

            $labels[] = [
                'kode_barang' => $item['kode_barang'],
                'nama_barang' => $item['nama_barang'],
                'image'       => $this->svgDataUri($svg),
            ];
        }

        return $this->streamLabelsPdf('qrcode', 'Cetak Massal QR Code', $labels, 'qrcode-barang.pdf');
    }

    public function printMassBarcode()
    {
        if ($redirect = $this->guardDownload()) {
            return $redirect;
        }

        $barang = $this->barangModel
            ->orderBy('nama_barang', 'ASC')
            ->findAll();

        $labels = [];
        $barcode = new Code128Barcode();

        foreach ($barang as $item) {
            try {
                $png = $barcode->toPng($item['barcode']);
            } catch (InvalidArgumentException $exception) {
                continue;
            }

            $labels[] = [
                'kode_barang' => $item['kode_barang'],
                'nama_barang' => $item['nama_barang'],
                'image'       => $this->pngDataUri($png),
            ];
        }

        return $this->streamLabelsPdf('barcode', 'Cetak Massal Barcode', $labels, 'barcode-barang.pdf');
    }

    private function validationRules(?int $id = null, bool $validateKode = true): array
    {
        $uniqueRule = $id === null
            ? 'is_unique[barang.kode_barang]'
            : 'is_unique[barang.kode_barang,id,' . $id . ']';

        $rules = [
            'nama_barang'  => 'required|min_length[2]|max_length[150]',
            'deskripsi_barang' => 'permit_empty|max_length[1000]',
            'kategori_id'  => 'required|is_not_unique[kategori.id]',
            'satuan'       => 'required|min_length[1]|max_length[50]',
            'lokasi_barang' => 'permit_empty|max_length[150]',
            'barcode'      => 'permit_empty|max_length[120]|regex_match[/^[\\x20-\\x7E]+$/]',
            'harga_beli'   => 'required|integer|greater_than_equal_to[0]',
            'harga_jual'   => 'required|integer|greater_than_equal_to[0]',
            'stok_awal'    => 'required|integer|greater_than_equal_to[0]',
            'stok_minimum' => 'required|integer|greater_than_equal_to[0]',
        ];

        if ($validateKode) {
            $rules = ['kode_barang' => 'required|min_length[2]|max_length[50]|regex_match[/^[\\x20-\\x7E]+$/]|' . $uniqueRule] + $rules;
        }

        return $rules;
    }

    private function formData(?string $generatedCode = null): array
    {
        $kodeBarang = strtoupper(trim((string) ($generatedCode ?? $this->request->getPost('kode_barang'))));
        $barcode = trim((string) $this->request->getPost('barcode'));

        if ($barcode === '') {
            $barcode = $kodeBarang;
        }

        return [
            'kode_barang'  => $kodeBarang,
            'nama_barang'  => $this->request->getPost('nama_barang'),
            'deskripsi_barang' => $this->request->getPost('deskripsi_barang') ?: null,
            'kategori_id'  => $this->request->getPost('kategori_id'),
            'satuan'       => $this->request->getPost('satuan'),
            'lokasi_barang' => $this->request->getPost('lokasi_barang') ?: null,
            'harga_beli'   => (int) $this->request->getPost('harga_beli'),
            'harga_jual'   => (int) $this->request->getPost('harga_jual'),
            'stok_awal'    => $this->request->getPost('stok_awal'),
            'stok_minimum' => $this->request->getPost('stok_minimum'),
            'qrcode'       => $kodeBarang,
            'barcode'      => $barcode,
        ];
    }

    private function sellingPriceError(): ?string
    {
        $hargaBeli = (int) $this->request->getPost('harga_beli');
        $hargaJual = (int) $this->request->getPost('harga_jual');

        if ($hargaJual < $hargaBeli) {
            return 'Harga jual tidak boleh lebih kecil dari harga beli.';
        }

        return null;
    }

    private function nextBarangCode(): string
    {
        $row = Database::connect()
            ->table('barang')
            ->select("MAX(CAST(SUBSTRING(kode_barang, 4) AS UNSIGNED)) AS last_number", false)
            ->like('kode_barang', 'BRG', 'after')
            ->get()
            ->getRowArray();

        return 'BRG' . str_pad((string) (((int) ($row['last_number'] ?? 0)) + 1), 4, '0', STR_PAD_LEFT);
    }

    private function kategoriOptions(): array
    {
        return $this->kategoriModel
            ->orderBy('nama_kategori', 'ASC')
            ->findAll();
    }

    private function validateImportRows(array $rows): array
    {
        $kategoriMap = [];

        foreach ($this->kategoriOptions() as $kategori) {
            $kategoriMap[strtolower(trim($kategori['nama_kategori']))] = (int) $kategori['id'];
        }

        $existingCodes = [];

        foreach ($this->barangModel->select('kode_barang')->findAll() as $barang) {
            $existingCodes[strtoupper($barang['kode_barang'])] = true;
        }

        $seenCodes = [];
        $data = [];
        $errors = [];

        foreach ($rows as $row) {
            $rowNumber = (int) ($row['_row'] ?? 0);
            $kodeBarang = strtoupper(trim((string) ($row['kode_barang'] ?? '')));
            $barcode = trim((string) ($row['barcode'] ?? ''));
            $namaBarang = trim((string) ($row['nama_barang'] ?? ''));
            $kategoriName = trim((string) ($row['kategori'] ?? ''));
            $satuan = trim((string) ($row['satuan'] ?? ''));
            $stokAwal = $this->importIntegerValue($row['stok_awal'] ?? '');
            $stokMinimum = $this->importIntegerValue($row['stok_minimum'] ?? '');
            $lokasiBarang = trim((string) ($row['lokasi_barang'] ?? ''));
            $hargaBeli = $this->importIntegerValue($row['harga_beli'] ?? '');
            $hargaJual = $this->importIntegerValue($row['harga_jual'] ?? '');
            $deskripsiBarang = trim((string) ($row['deskripsi_barang'] ?? ''));
            $rowErrors = [];

            if ($kodeBarang === '') {
                $rowErrors[] = 'kode barang wajib diisi';
            } elseif (! preg_match('/^[\x20-\x7E]+$/', $kodeBarang)) {
                $rowErrors[] = 'kode barang mengandung karakter tidak valid';
            } elseif (strlen($kodeBarang) > 50) {
                $rowErrors[] = 'kode barang maksimal 50 karakter';
            } elseif (isset($existingCodes[$kodeBarang])) {
                $rowErrors[] = 'kode barang sudah ada di database';
            } elseif (isset($seenCodes[$kodeBarang])) {
                $rowErrors[] = 'kode barang duplikat di file import';
            }

            if ($barcode !== '') {
                if (! preg_match('/^[\x20-\x7E]+$/', $barcode)) {
                    $rowErrors[] = 'barcode mengandung karakter tidak valid';
                } elseif (strlen($barcode) > 120) {
                    $rowErrors[] = 'barcode maksimal 120 karakter';
                }
            }

            if ($namaBarang === '') {
                $rowErrors[] = 'nama barang wajib diisi';
            } elseif (strlen($namaBarang) > 150) {
                $rowErrors[] = 'nama barang maksimal 150 karakter';
            }

            if ($kategoriName === '') {
                $rowErrors[] = 'kategori wajib diisi';
            }

            $kategoriId = $kategoriMap[strtolower($kategoriName)] ?? null;

            if ($kategoriName !== '' && $kategoriId === null) {
                $rowErrors[] = 'kategori tidak ditemukan';
            }

            if ($satuan === '') {
                $rowErrors[] = 'satuan wajib diisi';
            } elseif (strlen($satuan) > 50) {
                $rowErrors[] = 'satuan maksimal 50 karakter';
            }

            if ($stokAwal === null) {
                $rowErrors[] = 'stok awal wajib angka 0 atau lebih';
            }

            if ($stokMinimum === null) {
                $rowErrors[] = 'stok minimum wajib angka 0 atau lebih';
            }

            if (strlen($lokasiBarang) > 150) {
                $rowErrors[] = 'lokasi barang maksimal 150 karakter';
            }

            if ($hargaBeli === null) {
                $rowErrors[] = 'harga beli wajib angka 0 atau lebih';
            }

            if ($hargaJual === null) {
                $rowErrors[] = 'harga jual wajib angka 0 atau lebih';
            }

            if ($hargaBeli !== null && $hargaJual !== null && $hargaJual < $hargaBeli) {
                $rowErrors[] = 'harga jual tidak boleh lebih kecil dari harga beli';
            }

            if (strlen($deskripsiBarang) > 1000) {
                $rowErrors[] = 'deskripsi barang maksimal 1000 karakter';
            }

            if ($rowErrors !== []) {
                $errors[] = 'Baris ' . $rowNumber . ': ' . implode(', ', $rowErrors) . '.';
                continue;
            }

            $seenCodes[$kodeBarang] = true;
            $data[] = [
                'kode_barang'      => $kodeBarang,
                'nama_barang'      => $namaBarang,
                'deskripsi_barang' => $deskripsiBarang !== '' ? $deskripsiBarang : null,
                'kategori_id'      => $kategoriId,
                'satuan'           => $satuan,
                'lokasi_barang'    => $lokasiBarang !== '' ? $lokasiBarang : null,
                'harga_beli'       => $hargaBeli,
                'harga_beli_awal'  => $hargaBeli,
                'harga_jual'       => $hargaJual,
                'stok_awal'        => $stokAwal,
                'stok_minimum'     => $stokMinimum,
                'stok'             => $stokAwal,
                'foto'             => null,
                'qrcode'           => $kodeBarang,
                'barcode'          => $barcode !== '' ? $barcode : $kodeBarang,
            ];
        }

        return [$data, $errors];
    }

    private function importIntegerValue($value): ?int
    {
        $value = trim((string) $value);

        if ($value === '') {
            return null;
        }

        $compact = preg_replace('/\s+/', '', $value) ?? '';

        if (ctype_digit($compact)) {
            return (int) $compact;
        }

        if (preg_match('/^\d{1,3}(\.\d{3})+$/', $compact)) {
            return (int) str_replace('.', '', $compact);
        }

        if (preg_match('/^\d{1,3}(,\d{3})+$/', $compact)) {
            return (int) str_replace(',', '', $compact);
        }

        if (preg_match('/^(\d+)[\.,]0+$/', $compact, $matches)) {
            return (int) $matches[1];
        }

        return null;
    }

    private function moveTemporaryPhoto(): ?string
    {
        return $this->temporaryImageUpload->moveToPermanent(
            $this->request->getPost('foto_temp'),
            $this->uploadPath,
            ['jpg', 'jpeg', 'png', 'webp']
        );
    }

    private function deletePhoto(?string $photoName): void
    {
        if (! $photoName) {
            return;
        }

        $photoPath = $this->uploadPath . DIRECTORY_SEPARATOR . $photoName;

        if (is_file($photoPath)) {
            unlink($photoPath);
        }
    }

    private function barangUsage(int $barangId): array
    {
        $db = Database::connect();

        return [
            'pembelian'       => $db->table('pembelian_detail')->where('barang_id', $barangId)->countAllResults(),
            'penjualan'       => $db->table('penjualan_detail')->where('barang_id', $barangId)->countAllResults(),
            'service'         => $db->table('service_barang')->where('barang_id', $barangId)->countAllResults(),
            'stok_penyesuaian'   => $db->table('stok_penyesuaian')->where('barang_id', $barangId)->countAllResults(),
        ];
    }

    private function stockHistory(array $barang): array
    {
        $barangId = (int) $barang['id'];
        $saldo = (int) ($barang['stok_awal'] ?? 0);
        $history = [[
            'tanggal'    => $barang['created_at'] ?? null,
            'jenis'      => 'awal',
            'label'      => 'Stok Awal',
            'referensi'  => 'Master barang',
            'keterangan' => 'Stok awal saat barang dibuat.',
            'masuk'      => $saldo,
            'keluar'     => null,
            'saldo'      => $saldo,
            'sort_date'  => $barang['created_at'] ?? '0000-00-00 00:00:00',
            'sort_order' => 0,
        ]];

        $db = Database::connect();
        $events = [];

        $pembelian = $db->table('pembelian_detail bmd')
            ->select('bmd.id AS detail_id, bmd.jumlah, bm.id AS transaksi_id, bm.no_transaksi, bm.tanggal_pembelian AS tanggal, bm.created_at, suplier.nama AS nama_suplier')
            ->join('pembelian bm', 'bm.id = bmd.pembelian_id', 'inner')
            ->join('suplier', 'suplier.id = bm.suplier_id', 'left')
            ->where('bmd.barang_id', $barangId)
            ->get()
            ->getResultArray();

        foreach ($pembelian as $item) {
            $events[] = [
                'tanggal'    => $item['tanggal'],
                'jenis'      => 'masuk',
                'label'      => 'Pembelian',
                'referensi'  => $item['no_transaksi'],
                'keterangan' => ! empty($item['nama_suplier']) ? 'Supplier: ' . $item['nama_suplier'] : '-',
                'jumlah'     => (int) $item['jumlah'],
                'sort_date'  => ($item['tanggal'] ?? '') . ' ' . date('H:i:s', strtotime($item['created_at'] ?? '00:00:00')),
                'sort_order' => 1,
                'detail_id'  => (int) $item['detail_id'],
            ];
        }

        $penjualan = $db->table('penjualan_detail pd')
            ->select('pd.id AS detail_id, pd.jumlah, p.id AS transaksi_id, p.no_transaksi, p.tanggal_penjualan AS tanggal, p.created_at, pelanggan.nama AS nama_pelanggan')
            ->join('penjualan p', 'p.id = pd.penjualan_id', 'inner')
            ->join('pelanggan', 'pelanggan.id = p.pelanggan_id', 'left')
            ->where('pd.barang_id', $barangId)
            ->get()
            ->getResultArray();

        foreach ($penjualan as $item) {
            $events[] = [
                'tanggal'    => $item['tanggal'],
                'jenis'      => 'keluar',
                'label'      => 'Penjualan',
                'referensi'  => $item['no_transaksi'],
                'keterangan' => ! empty($item['nama_pelanggan']) ? 'Pelanggan: ' . $item['nama_pelanggan'] : '-',
                'jumlah'     => -((int) $item['jumlah']),
                'sort_date'  => ($item['tanggal'] ?? '') . ' ' . date('H:i:s', strtotime($item['created_at'] ?? '00:00:00')),
                'sort_order' => 2,
                'detail_id'  => (int) $item['detail_id'],
            ];
        }

        $service = $db->table('service_barang sb')
            ->select('sb.id AS detail_id, sb.jumlah, s.id AS transaksi_id, s.no_transaksi, s.tanggal_service AS tanggal, s.created_at, pelanggan.nama AS nama_pelanggan, teknisi.nama AS nama_teknisi')
            ->join('service s', 's.id = sb.service_id', 'inner')
            ->join('pelanggan', 'pelanggan.id = s.pelanggan_id', 'left')
            ->join('teknisi', 'teknisi.id = s.teknisi_id', 'left')
            ->where('sb.barang_id', $barangId)
            ->get()
            ->getResultArray();

        foreach ($service as $item) {
            $info = [];

            if (! empty($item['nama_pelanggan'])) {
                $info[] = 'Pelanggan: ' . $item['nama_pelanggan'];
            }

            if (! empty($item['nama_teknisi'])) {
                $info[] = 'Teknisi: ' . $item['nama_teknisi'];
            }

            $events[] = [
                'tanggal'    => $item['tanggal'],
                'jenis'      => 'keluar',
                'label'      => 'Service',
                'referensi'  => $item['no_transaksi'],
                'keterangan' => $info !== [] ? implode(', ', $info) : '-',
                'jumlah'     => -((int) $item['jumlah']),
                'sort_date'  => ($item['tanggal'] ?? '') . ' ' . date('H:i:s', strtotime($item['created_at'] ?? '00:00:00')),
                'sort_order' => 3,
                'detail_id'  => (int) $item['detail_id'],
            ];
        }

        $stokPenyesuaian = $db->table('stok_penyesuaian')
            ->select('id AS detail_id, no_penyesuaian, tanggal_penyesuaian AS tanggal, stok_sistem, stok_fisik, selisih, alasan, catatan, status, dibatalkan_pada, catatan_pembatalan, created_at')
            ->where('barang_id', $barangId)
            ->get()
            ->getResultArray();

        foreach ($stokPenyesuaian as $item) {
            $selisih = (int) $item['selisih'];
            $keterangan = trim(($item['alasan'] ?? '') . ' - Stok sistem ' . ($item['stok_sistem'] ?? 0) . ', stok fisik ' . ($item['stok_fisik'] ?? 0), ' -');

            $events[] = [
                'tanggal'    => $item['tanggal'],
                'jenis'      => 'penyesuaian',
                'label'      => 'Penyesuaian',
                'referensi'  => $item['no_penyesuaian'],
                'keterangan' => $keterangan,
                'jumlah'     => $selisih,
                'sort_date'  => ($item['tanggal'] ?? '') . ' ' . date('H:i:s', strtotime($item['created_at'] ?? '00:00:00')),
                'sort_order' => 4,
                'detail_id'  => (int) $item['detail_id'],
            ];

            if (($item['status'] ?? '') === 'dibatalkan') {
                $events[] = [
                    'tanggal'    => ! empty($item['dibatalkan_pada']) ? date('Y-m-d', strtotime($item['dibatalkan_pada'])) : $item['tanggal'],
                    'jenis'      => 'pembatalan',
                    'label'      => 'Pembatalan',
                    'referensi'  => $item['no_penyesuaian'],
                    'keterangan' => ! empty($item['catatan_pembatalan']) ? $item['catatan_pembatalan'] : 'Pembatalan penyesuaian stok.',
                    'jumlah'     => -$selisih,
                    'sort_date'  => $item['dibatalkan_pada'] ?? (($item['tanggal'] ?? '') . ' 23:59:59'),
                    'sort_order' => 5,
                    'detail_id'  => (int) $item['detail_id'],
                ];
            }
        }

        usort($events, static function (array $left, array $right): int {
            return [$left['sort_date'], $left['sort_order'], $left['detail_id']]
                <=> [$right['sort_date'], $right['sort_order'], $right['detail_id']];
        });

        foreach ($events as $event) {
            $delta = (int) $event['jumlah'];
            $saldo += $delta;
            $masuk = $delta > 0 ? $delta : null;
            $keluar = $delta < 0 ? abs($delta) : null;

            $history[] = [
                'tanggal'    => $event['tanggal'],
                'jenis'      => $event['jenis'],
                'label'      => $event['label'],
                'referensi'  => $event['referensi'],
                'keterangan' => $event['keterangan'] !== '' ? $event['keterangan'] : '-',
                'masuk'      => $masuk,
                'keluar'     => $keluar,
                'saldo'      => $saldo,
            ];
        }

        return $history;
    }

    private function downloadSvg(string $svg, string $fileName)
    {
        $fileName = preg_replace('/[^A-Za-z0-9_.-]/', '-', $fileName);

        return $this->response
            ->setHeader('Content-Type', 'image/svg+xml')
            ->setHeader('Content-Disposition', 'attachment; filename="' . $fileName . '"')
            ->setBody($svg);
    }

    private function downloadPng(string $png, string $fileName)
    {
        $fileName = preg_replace('/[^A-Za-z0-9_.-]/', '-', $fileName);

        return $this->response
            ->setHeader('Content-Type', 'image/png')
            ->setHeader('Content-Disposition', 'attachment; filename="' . $fileName . '"')
            ->setBody($png);
    }

    private function labeledQrSvg(string $svg, array $barang): string
    {
        $width = 420;
        $height = 500;
        $qrSize = 330;
        $qrX = (int) (($width - $qrSize) / 2);
        $lines = $this->wrapLabelText((string) ($barang['nama_barang'] ?? ''), 34, 3);
        $nameText = '';
        $y = 415;

        foreach ($lines as $line) {
            $nameText .= '<text x="210" y="' . $y . '" text-anchor="middle" font-family="Arial, DejaVu Sans, sans-serif" font-size="18" fill="#111827">'
                . $this->xmlEscape($line)
                . '</text>';
            $y += 24;
        }

        $imageData = 'data:image/svg+xml;base64,' . base64_encode($svg);

        return '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="' . $width . '" height="' . $height . '" viewBox="0 0 ' . $width . ' ' . $height . '">'
            . '<rect width="100%" height="100%" fill="#fff"/>'
            . '<image href="' . $imageData . '" xlink:href="' . $imageData . '" x="' . $qrX . '" y="24" width="' . $qrSize . '" height="' . $qrSize . '"/>'
            . '<text x="210" y="385" text-anchor="middle" font-family="Arial, DejaVu Sans, sans-serif" font-size="20" font-weight="700" fill="#111827">'
            . $this->xmlEscape((string) ($barang['kode_barang'] ?? ''))
            . '</text>'
            . $nameText
            . '</svg>';
    }

    private function labeledBarcodePng(string $png, array $barang): string
    {
        $source = imagecreatefromstring($png);

        if (! $source) {
            return $png;
        }

        $sourceWidth = imagesx($source);
        $sourceHeight = imagesy($source);
        $paddingX = 80;
        $paddingTop = 45;
        $barcodeGap = 32;
        $font = $this->labelFontPath();
        $nameLines = $this->wrapLabelText((string) ($barang['nama_barang'] ?? ''), 58, 3);
        $canvasWidth = $sourceWidth + ($paddingX * 2);
        $canvasHeight = $paddingTop + $sourceHeight + $barcodeGap + 50 + (count($nameLines) * 36) + 35;

        $canvas = imagecreatetruecolor($canvasWidth, $canvasHeight);
        $white = imagecolorallocate($canvas, 255, 255, 255);
        $black = imagecolorallocate($canvas, 17, 24, 39);

        imagefilledrectangle($canvas, 0, 0, $canvasWidth, $canvasHeight, $white);
        imagecopy($canvas, $source, $paddingX, $paddingTop, 0, 0, $sourceWidth, $sourceHeight);

        $y = $paddingTop + $sourceHeight + $barcodeGap;
        $y = $this->drawCenteredPngText($canvas, (string) ($barang['barcode'] ?? $barang['kode_barang'] ?? ''), $canvasWidth, $y, 30, $font, $black, true);
        $y += 14;

        foreach ($nameLines as $line) {
            $y = $this->drawCenteredPngText($canvas, $line, $canvasWidth, $y, 24, $font, $black);
        }

        ob_start();
        imagepng($canvas);
        $labelPng = (string) ob_get_clean();

        imagedestroy($source);
        imagedestroy($canvas);

        return $labelPng;
    }

    private function drawCenteredPngText($image, string $text, int $canvasWidth, int $y, int $fontSize, ?string $font, int $color, bool $bold = false): int
    {
        if ($font !== null && function_exists('imagettftext')) {
            $box = imagettfbbox($fontSize, 0, $font, $text);
            $textWidth = abs(($box[2] ?? 0) - ($box[0] ?? 0));
            $x = (int) (($canvasWidth - $textWidth) / 2);
            $baseline = $y + $fontSize;

            imagettftext($image, $fontSize, 0, $x, $baseline, $color, $font, $text);

            if ($bold) {
                imagettftext($image, $fontSize, 0, $x + 1, $baseline, $color, $font, $text);
            }

            return $baseline + 12;
        }

        $fontId = 5;
        $textWidth = imagefontwidth($fontId) * strlen($text);
        $x = (int) (($canvasWidth - $textWidth) / 2);
        imagestring($image, $fontId, $x, $y, $text, $color);

        if ($bold) {
            imagestring($image, $fontId, $x + 1, $y, $text, $color);
        }

        return $y + imagefontheight($fontId) + 8;
    }

    private function labelFontPath(): ?string
    {
        $font = ROOTPATH . 'vendor/dompdf/dompdf/lib/fonts/DejaVuSans.ttf';

        return is_file($font) ? $font : null;
    }

    private function wrapLabelText(string $text, int $maxLength, int $maxLines): array
    {
        $text = trim(preg_replace('/\s+/', ' ', $text) ?? '');

        if ($text === '') {
            return ['-'];
        }

        $lines = explode("\n", wordwrap($text, $maxLength, "\n", true));

        if (count($lines) > $maxLines) {
            $lines = array_slice($lines, 0, $maxLines);
            $last = rtrim($lines[$maxLines - 1]);
            $lines[$maxLines - 1] = rtrim(substr($last, 0, max(0, $maxLength - 3))) . '...';
        }

        return $lines;
    }

    private function xmlEscape(string $value): string
    {
        return htmlspecialchars($value, ENT_QUOTES | ENT_XML1, 'UTF-8');
    }

    private function streamLabelsPdf(string $type, string $title, array $labels, string $fileName)
    {
        $options = new Options();
        $options->set('isHtml5ParserEnabled', true);
        $options->set('isRemoteEnabled', false);

        $dompdf = new Dompdf($options);
        $dompdf->loadHtml(view('admin/barang/print_labels', [
            'type'   => $type,
            'title'  => $title,
            'labels' => $labels,
        ]));
        $dompdf->setPaper('A4', 'portrait');
        $dompdf->render();

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

    private function svgDataUri(string $svg): string
    {
        return 'data:image/svg+xml;base64,' . base64_encode($svg);
    }

    private function pngDataUri(string $png): string
    {
        return 'data:image/png;base64,' . base64_encode($png);
    }

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

        return $this->requireInventoryAccess('/admin/barang');
    }

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