<?php
namespace App\Controllers;

use App\Core\Audit;
use App\Core\Auth;
use App\Core\Controller;
use App\Core\Database;
use App\Core\Dates;
use App\Core\Flash;
use App\Core\Num;
use App\Core\Request;
use App\Core\Response;
use App\Core\Settings;
use App\Models\Invoice;
use App\Services\ApprovalService;
use App\Services\ArCalculator;
use App\Services\DashboardService;
use App\Services\ExportService;
use App\Services\MasterResolver;

/** CRUD invoice beserta potongan, riwayat, dan lampiran. */
class InvoiceController extends Controller
{
    protected string $module = 'invoice';

    public function index(): void
    {
        $filters = PiutangController::readFilters(false);
        $sort = (string)Request::get('urut', 'invoice_date');
        $dir  = (string)Request::get('arah', 'desc');
        [$page, $per] = $this->pageParams();
        $m = new Invoice();
        $this->view('invoice/index', [
            'judul' => 'Invoice',
            'subjudul' => 'Seluruh invoice yang tercatat di sistem',
            'p' => $m->listing($filters, $sort, $dir, $page, $per),
            'tot' => $m->listingTotals($filters),
            'filters' => $filters, 'sort' => $sort, 'dir' => $dir,
            'opsi' => DashboardService::filterOptions(),
        ]);
    }

    public function show(array $params): void
    {
        $id = (int)($params['id'] ?? 0);
        $m = new Invoice();
        $inv = $m->findFull($id);
        if (!$inv) Response::abort(404, 'Invoice tidak ditemukan.');
        $this->guardScope($inv);

        $this->view('invoice/detail', [
            'judul' => 'Invoice ' . $inv['invoice_no'],
            'subjudul' => $inv['customer_name'] . ' · ' . tgl($inv['invoice_date']),
            'inv' => $inv,
            'deductions' => $m->deductions($id),
            'payments' => $m->payments($id),
            'revisions' => $m->revisions($id),
            'creditNotes' => $m->creditNotes($id),
            'followups' => $m->followups($id),
            'promises' => $m->promises($id),
            'attachments' => $m->attachments($id),
            'audit' => $m->auditTrail($id, 60),
            'attachmentTypes' => Database::i()->all('SELECT * FROM attachment_types WHERE is_active=1 ORDER BY sort_order'),
            'pengganti' => $inv['replaced_by_invoice_id'] ? $m->find((int)$inv['replaced_by_invoice_id']) : null,
            'digantikan' => $inv['replaces_invoice_id'] ? $m->find((int)$inv['replaces_invoice_id']) : null,
        ]);
    }

    private function guardScope(array $inv): void
    {
        [$sql, $prm] = \App\Core\Acl::invoiceScopeSql('i');
        if ($sql === '1=1') return;
        $ok = Database::i()->value(
            'SELECT COUNT(*) FROM invoices i WHERE i.id = :pk AND (' . $sql . ')',
            array_merge(['pk' => (int)$inv['id']], $prm)
        );
        if ((int)$ok === 0) Response::abort(403, 'Invoice ini berada di luar cakupan data akun Anda.');
    }

    public function create(): void
    {
        $this->view('invoice/form', array_merge($this->formData(), [
            'judul' => 'Tambah Invoice', 'inv' => null, 'deductions' => [],
            'nomorSaran' => (new Invoice())->nextInvoiceNo('FA'),
        ]));
    }

    public function edit(array $params): void
    {
        $id = (int)($params['id'] ?? 0);
        $m = new Invoice();
        $inv = $m->findFull($id);
        if (!$inv) Response::abort(404, 'Invoice tidak ditemukan.');
        $this->view('invoice/form', array_merge($this->formData(), [
            'judul' => 'Ubah Invoice ' . $inv['invoice_no'],
            'inv' => $inv, 'deductions' => $m->deductions($id),
        ]));
    }

    private function formData(): array
    {
        $db = Database::i();
        return [
            'areas' => $db->all('SELECT id,name FROM areas WHERE deleted_at IS NULL AND is_active=1 ORDER BY name'),
            'bus' => $db->all('SELECT id,name FROM business_units WHERE deleted_at IS NULL AND is_active=1 ORDER BY name'),
            'customers' => $db->all('SELECT id,code,name FROM customers WHERE deleted_at IS NULL ORDER BY name LIMIT 3000'),
            'salespersons' => $db->all('SELECT id,name FROM salespersons WHERE deleted_at IS NULL AND is_active=1 ORDER BY name'),
            'officers' => $db->all('SELECT id,name FROM collection_officers WHERE deleted_at IS NULL AND is_active=1 ORDER BY name'),
            'jobCats' => $db->all('SELECT id,name FROM job_categories WHERE deleted_at IS NULL AND is_active=1 ORDER BY name'),
            'jobTypes' => $db->all('SELECT id,name,job_category_id FROM job_types WHERE deleted_at IS NULL AND is_active=1 ORDER BY name'),
            'revCats' => $db->all('SELECT id,name FROM revenue_categories WHERE deleted_at IS NULL AND is_active=1 ORDER BY name'),
            'terms' => $db->all('SELECT id,name,days,base_date_type FROM payment_terms WHERE deleted_at IS NULL AND is_active=1 ORDER BY days'),
            'dedTypes' => $db->all('SELECT * FROM deduction_types WHERE deleted_at IS NULL AND is_active=1 ORDER BY sort_order'),
        ];
    }

    public function store(): void
    {
        $data = $this->validate([
            'invoice_no'   => 'required|maxlen:60',
            'invoice_date' => 'required|date',
            'customer_id'  => 'required|integer|exists:customers,id',
            'gross_amount' => 'required|numeric|min:0.01',
        ], ['invoice_no' => 'Nomor invoice', 'invoice_date' => 'Tanggal invoice',
            'customer_id' => 'Customer', 'gross_amount' => 'Nominal'], '/invoice/tambah');

        $no = MasterResolver::key($data['invoice_no']);
        if (Database::i()->value('SELECT COUNT(*) FROM invoices WHERE invoice_no = ?', [$no])) {
            Flash::error("Nomor invoice $no sudah digunakan.");
            $this->redirect('/invoice/tambah');
        }
        $id = $this->persist(null, $data);
        Flash::success('Invoice ' . $no . ' berhasil disimpan.');
        $this->redirect('/invoice/' . $id);
    }

    public function update(array $params): void
    {
        $id = (int)($params['id'] ?? 0);
        $m = new Invoice();
        $before = $m->find($id);
        if (!$before) Response::abort(404, 'Invoice tidak ditemukan.');
        $data = $this->validate([
            'invoice_date' => 'required|date',
            'customer_id'  => 'required|integer',
            'gross_amount' => 'required|numeric|min:0.01',
        ], ['invoice_date' => 'Tanggal invoice', 'customer_id' => 'Customer', 'gross_amount' => 'Nominal']);
        $data['invoice_no'] = $before['invoice_no'];
        $this->persist($id, $data);
        Flash::success('Invoice berhasil diperbarui dan nilainya dihitung ulang.');
        $this->redirect('/invoice/' . $id);
    }

    /** Menyimpan invoice + potongan dalam satu transaksi. */
    private function persist(?int $id, array $data): int
    {
        $db = Database::i();
        return $db->transaction(function () use ($db, $id, $data) {
            $customerId = (int)$data['customer_id'];
            $cust = $db->one('SELECT * FROM customers WHERE id = ?', [$customerId]);

            $fields = [
                'invoice_no' => MasterResolver::key($data['invoice_no']),
                'tax_invoice_no' => MasterResolver::norm((string)Request::post('tax_invoice_no', '')) ?: null,
                'so_no' => MasterResolver::key((string)Request::post('so_no', '')) ?: null,
                'invoice_date' => Dates::parse($data['invoice_date']),
                'wr_date' => Request::date('wr_date'),
                'print_date' => Request::date('print_date'),
                'input_date' => Request::date('input_date'),
                'sent_date' => Request::date('sent_date'),
                'received_date' => Request::date('received_date'),
                'tax_invoice_date' => Request::date('tax_invoice_date'),
                'customer_id' => $customerId,
                'outlet_id' => Request::int('outlet_id') ?: null,
                'area_id' => Request::int('area_id') ?: ($cust['area_id'] ?? null),
                'business_unit_id' => Request::int('business_unit_id') ?: ($cust['business_unit_id'] ?? null),
                'salesperson_id' => Request::int('salesperson_id') ?: ($cust['salesperson_id'] ?? null),
                'collection_officer_id' => Request::int('collection_officer_id') ?: ($cust['collection_officer_id'] ?? null),
                'job_category_id' => Request::int('job_category_id') ?: null,
                'job_type_id' => Request::int('job_type_id') ?: null,
                'revenue_category_id' => Request::int('revenue_category_id') ?: null,
                'payment_term_id' => Request::int('payment_term_id') ?: null,
                'send_channel' => MasterResolver::key((string)Request::post('send_channel', '')) ?: null,
                'send_pic' => MasterResolver::norm((string)Request::post('send_pic', '')) ?: null,
                'notes' => mb_substr((string)Request::post('notes', ''), 0, 2000) ?: null,
                'gross_amount' => Num::parse($data['gross_amount']),
            ];

            $termManual = Request::get('payment_term_days', '');
            $dueManual  = Request::date('due_date');
            $useManual  = Request::bool('due_date_manual');
            if ($useManual && $dueManual) {
                $fields['due_date'] = $dueManual;
                $fields['due_date_manual'] = 1;
                $fields['due_date_basis'] = 'manual';
                $fields['payment_term_days'] = (int)(Dates::diffDays($fields['invoice_date'], $dueManual) ?? 30);
            } else {
                $term = $termManual !== '' ? (int)$termManual : null;
                $r = ArCalculator::computeDueDate($fields, null, $term);
                $fields['due_date'] = $r['due_date'] ?: $fields['invoice_date'];
                $fields['due_date_basis'] = $r['basis'];
                $fields['payment_term_days'] = $r['term_days'];
                $fields['due_date_manual'] = 0;
            }

            if ($id) {
                $fields['updated_by'] = Auth::id() ?: null;
                $before = $db->one('SELECT * FROM invoices WHERE id = ?', [$id]);
                $db->update('invoices', $fields, 'id = :pk', ['pk' => $id]);
                Audit::logDiff('invoice', 'update', 'invoice', $id, $before ?? [], $fields, 'Invoice diubah');
            } else {
                $fields['source'] = 'manual';
                $fields['created_by'] = Auth::id() ?: null;
                $id = $db->insert('invoices', $fields);
                Audit::log('invoice', 'create', 'invoice', $id, 'Invoice ' . $fields['invoice_no'] . ' dibuat', null, $fields);
            }

            // Potongan
            $db->exec('UPDATE invoice_deductions SET deleted_at = NOW() WHERE invoice_id = ? AND deleted_at IS NULL', [$id]);
            $types = Request::array('ded_type');
            $amounts = Request::array('ded_amount');
            $notes = Request::array('ded_note');
            $dpp = ArCalculator::dpp($fields['gross_amount']);
            foreach ($types as $i => $typeId) {
                $typeId = (int)$typeId;
                $amt = Num::parse((string)($amounts[$i] ?? 0));
                if ($typeId <= 0 || abs($amt) < 0.005) continue;
                $db->insert('invoice_deductions', [
                    'invoice_id' => $id, 'deduction_type_id' => $typeId,
                    'base_amount' => $dpp, 'rate' => $dpp > 0 ? round($amt / $dpp * 100, 4) : 0,
                    'amount' => round($amt, 2), 'is_manual' => 1,
                    'notes' => mb_substr((string)($notes[$i] ?? ''), 0, 255) ?: null,
                    'created_by' => Auth::id() ?: null,
                ]);
            }
            ArCalculator::recalculate($id);
            return $id;
        });
    }

    public function destroy(array $params): void
    {
        $id = (int)($params['id'] ?? 0);
        $m = new Invoice();
        $inv = $m->find($id);
        if (!$inv) Response::abort(404, 'Invoice tidak ditemukan.');
        if ((float)$inv['paid_amount'] > 0) {
            Flash::error('Invoice yang sudah menerima pembayaran tidak dapat dihapus. Gunakan pembatalan invoice.');
            $this->redirect('/invoice/' . $id);
        }
        $m->delete($id);
        Audit::log('invoice', 'delete', 'invoice', $id, 'Invoice ' . $inv['invoice_no'] . ' dihapus (soft delete)', $inv, null);
        Flash::success('Invoice ' . $inv['invoice_no'] . ' dihapus. Data tetap tersimpan di riwayat.');
        $this->redirect('/invoice');
    }

    public function cancel(array $params): void
    {
        $id = (int)($params['id'] ?? 0);
        $inv = (new Invoice())->find($id);
        if (!$inv) Response::abort(404, 'Invoice tidak ditemukan.');
        $alasan = trim((string)Request::post('alasan', ''));
        if ($alasan === '') { Flash::error('Alasan pembatalan wajib diisi.'); $this->redirect('/invoice/' . $id); }

        $reqId = ApprovalService::request('invoice', 'cancel', 'invoice', $id,
            'Pembatalan invoice ' . $inv['invoice_no'] . ' — ' . $alasan, (float)$inv['net_amount'],
            ['alasan' => $alasan]);
        Flash::info('Permintaan pembatalan invoice dikirim untuk persetujuan (nomor permintaan telah dibuat).');
        $this->redirect('/invoice/' . $id);
    }

    public function recalc(array $params): void
    {
        $id = (int)($params['id'] ?? 0);
        ArCalculator::recalculate($id, Dates::today(), true);
        Audit::log('invoice', 'recalculate', 'invoice', $id, 'Nilai invoice dihitung ulang');
        Flash::success('Nilai invoice berhasil dihitung ulang oleh server.');
        $this->redirect('/invoice/' . $id);
    }

    public function bulk(): void
    {
        $ids = array_map('intval', Request::array('ids'));
        $aksi = (string)Request::post('aksi', '');
        if (!$ids) { Flash::warning('Tidak ada invoice yang dipilih.'); $this->back('/invoice'); }
        $n = 0;
        switch ($aksi) {
            case 'hitung_ulang':
                foreach ($ids as $id) { ArCalculator::recalculate($id, Dates::today(), true); $n++; }
                Flash::success("$n invoice berhasil dihitung ulang.");
                break;
            case 'tugaskan_penagih':
                $co = Request::int('collection_officer_id');
                if ($co <= 0) { Flash::error('Pilih petugas penagihan terlebih dahulu.'); break; }
                foreach ($ids as $id) {
                    Database::i()->exec('UPDATE invoices SET collection_officer_id = ? WHERE id = ?', [$co, $id]);
                    $n++;
                }
                Audit::log('invoice', 'update', 'invoice', null, "Penugasan petugas penagihan massal untuk $n invoice");
                Flash::success("$n invoice ditugaskan ke petugas penagihan.");
                break;
            default:
                Flash::warning('Aksi massal tidak dikenali.');
        }
        $this->back('/invoice');
    }

    /** Pencarian invoice untuk autocomplete (JSON). */
    public function search(): void
    {
        $q = trim((string)Request::get('q', ''));
        $onlyOutstanding = Request::get('outstanding') === '1';
        if (mb_strlen($q) < 2) $this->json(['sukses' => true, 'data' => []]);
        [$scope, $sp] = \App\Core\Acl::invoiceScopeSql('i');
        $sql = 'SELECT i.id, i.invoice_no, i.invoice_date, i.due_date, i.net_amount, i.outstanding_amount,
                       i.payment_status_code, c.name AS customer_name, c.id AS customer_id
                  FROM invoices i LEFT JOIN customers c ON c.id = i.customer_id
                 WHERE i.deleted_at IS NULL AND i.doc_status = "active"
                   AND (i.invoice_no LIKE :q OR i.tax_invoice_no LIKE :q2 OR c.name LIKE :q3)
                   AND (' . $scope . ')';
        if ($onlyOutstanding) $sql .= ' AND i.outstanding_amount > 0';
        $sql .= ' ORDER BY i.due_date LIMIT 25';
        $rows = Database::i()->all($sql, array_merge(['q' => "%$q%", 'q2' => "%$q%", 'q3' => "%$q%"], $sp));
        foreach ($rows as &$r) {
            $r['due_date_fmt'] = tgl($r['due_date']);
            $r['outstanding_amount'] = (float)$r['outstanding_amount'];
            $r['net_amount'] = (float)$r['net_amount'];
        }
        $this->json(['sukses' => true, 'data' => $rows]);
    }

    public function print(array $params): void
    {
        $id = (int)($params['id'] ?? 0);
        $m = new Invoice();
        $inv = $m->findFull($id);
        if (!$inv) Response::abort(404, 'Invoice tidak ditemukan.');
        \App\Core\View::render('invoice/cetak', [
            'inv' => $inv, 'deductions' => $m->deductions($id), 'payments' => $m->payments($id),
        ], '');
    }

    public function export(): void
    {
        $filters = PiutangController::readFilters(false);
        $m = new Invoice();
        $p = $m->listing($filters, (string)Request::get('urut', 'invoice_date'), (string)Request::get('arah', 'desc'),
                         1, Settings::int('laporan.maks_baris_export', 50000));
        $tot = $m->listingTotals($filters);
        ExportService::send((string)Request::get('format', 'xlsx'), [
            'judul' => 'DAFTAR INVOICE',
            'tanggal_lapor' => Dates::fmt(Dates::today()),
            'filter' => PiutangController::filterLabels($filters),
            'nama_berkas' => 'daftar_invoice', 'sheet' => 'Invoice', 'orientasi' => 'landscape',
        ], [
            ['label' => 'No. Invoice',      'key' => 'invoice_no',        'type' => 'text'],
            ['label' => 'No. Faktur Pajak', 'key' => 'tax_invoice_no',    'type' => 'text'],
            ['label' => 'No. SO',           'key' => 'so_no',             'type' => 'text'],
            ['label' => 'Tgl Invoice',      'key' => 'invoice_date',      'type' => 'date'],
            ['label' => 'Tgl Kirim',        'key' => 'sent_date',         'type' => 'date'],
            ['label' => 'Jatuh Tempo',      'key' => 'due_date',          'type' => 'date'],
            ['label' => 'Customer',         'key' => 'customer_name',     'type' => 'text'],
            ['label' => 'Outlet',           'key' => 'outlet_name',       'type' => 'text'],
            ['label' => 'Area',             'key' => 'area_name',         'type' => 'text'],
            ['label' => 'Sales',            'key' => 'salesperson_name',  'type' => 'text'],
            ['label' => 'Jenis Pekerjaan',  'key' => 'job_type_name',     'type' => 'text'],
            ['label' => 'PMSC/By Case',     'key' => 'revenue_category_name', 'type' => 'text'],
            ['label' => 'Nominal',          'key' => 'gross_amount',      'type' => 'money'],
            ['label' => 'DPP',              'key' => 'dpp_amount',        'type' => 'money'],
            ['label' => 'PPN',              'key' => 'vat_amount',        'type' => 'money'],
            ['label' => 'PPh 23',           'key' => 'pph23_amount',      'type' => 'money'],
            ['label' => 'Pot. Adm',         'key' => 'admin_fee_amount',  'type' => 'money'],
            ['label' => 'Pot. Lain',        'key' => 'other_deduction_amount', 'type' => 'money'],
            ['label' => 'Total Tagihan',    'key' => 'net_amount',        'type' => 'money'],
            ['label' => 'Pembayaran',       'key' => 'paid_amount',       'type' => 'money'],
            ['label' => 'Saldo',            'key' => 'outstanding_amount','type' => 'money'],
            ['label' => 'Status Bayar',     'key' => 'payment_status_name','type' => 'text'],
            ['label' => 'Status Dokumen',   'key' => 'doc_status',        'type' => 'text'],
            ['label' => 'Umur (hari)',      'key' => 'aging_days',        'type' => 'int'],
            ['label' => 'Hari Penagihan',   'key' => 'collection_days',   'type' => 'int'],
        ], $p['data'], [
            'invoice_no' => 'TOTAL', 'gross_amount' => $tot['bruto'], 'pph23_amount' => $tot['pph23'],
            'net_amount' => $tot['bersih'], 'paid_amount' => $tot['dibayar'], 'outstanding_amount' => $tot['saldo'],
        ]);
    }
}
