<?php
/**
 * Assets — fixed asset register + depreciation + disposal.
 *
 * Depreciation methods:
 *   SLM = straight-line: monthly = (cost - salvage) / useful_life_months
 *   RB  = reducing-balance: monthly = book_value * (annual_rate / 12)
 *          annual_rate defaulted from useful_life (rough): 2 / (useful_life_years)
 *
 * A depreciation run for a given period Y-M:
 *   for each IN_SERVICE asset with depreciation_start_date <= period-end
 *   AND accumulated_depreciation < (cost - salvage)
 *   create ONE asset_depreciation_lines row + ONE journal_entries batch line
 *   (grouped by category so we post fewer, tidier JEs).
 *
 * Disposal:
 *   NBV = purchase_cost - accumulated_depreciation
 *   gain/loss = sale_price - NBV
 *   posts: Dr Bank (sale_price), Dr Accumulated Depreciation, Cr Asset Cost, Cr/Dr Gain/Loss
 */
class Assets extends Model
{
    /** All non-deleted assets, joined to category + custodian for the register screen. */
    public function listAll(array $filter = []): array
    {
        $db = new DB();
        $where = ["a.deleted = 0"];
        $params = [];
        if (!empty($filter['status']))      { $where[] = "a.status = :st";    $params[':st'] = $filter['status']; }
        if (!empty($filter['category_id'])) { $where[] = "a.category_id = :c"; $params[':c']  = (int)$filter['category_id']; }
        if (!empty($filter['q'])) {
            $where[] = "(a.asset_no LIKE :q OR a.name LIKE :q OR a.serial_no LIKE :q OR a.custodian_name LIKE :q OR a.location LIKE :q)";
            $params[':q'] = '%' . $filter['q'] . '%';
        }
        $sql = "SELECT a.*, c.name AS category_name
                  FROM assets a
             LEFT JOIN asset_categories c ON c.c0d = a.category_id
                 WHERE " . implode(' AND ', $where) . "
                 ORDER BY a.status = 'IN_SERVICE' DESC, a.purchase_date DESC, a.asset_no DESC";
        return $db->mySelect($sql, $params) ?: [];
    }

    public function find(int $id): ?array
    {
        $db = new DB();
        $r = $db->mySelect(
            "SELECT a.*, c.name AS category_name,
                    c.coa_asset_account_id, c.coa_accumulated_depreciation_account_id, c.coa_depreciation_expense_account_id,
                    c.default_depreciation_method, c.default_useful_life_months
               FROM assets a
          LEFT JOIN asset_categories c ON c.c0d = a.category_id
              WHERE a.c0d = :id AND a.deleted = 0",
            [':id' => $id]
        );
        return $r[0] ?? null;
    }

    public function categories(): array
    {
        $db = new DB();
        return $db->mySelect(
            "SELECT c.*, coa1.account_code AS asset_coa_code, coa2.account_code AS accdep_coa_code, coa3.account_code AS depexp_coa_code
               FROM asset_categories c
          LEFT JOIN chart_of_accounts coa1 ON coa1.c0d = c.coa_asset_account_id
          LEFT JOIN chart_of_accounts coa2 ON coa2.c0d = c.coa_accumulated_depreciation_account_id
          LEFT JOIN chart_of_accounts coa3 ON coa3.c0d = c.coa_depreciation_expense_account_id
              WHERE c.deleted = 0 AND c.is_active = 1
              ORDER BY c.name",
            []
        ) ?: [];
    }

    public function depreciationHistory(int $assetId): array
    {
        $db = new DB();
        return $db->mySelect(
            "SELECT dl.*, je.je_no, je.posted_on AS je_posted_on
               FROM asset_depreciation_lines dl
          LEFT JOIN journal_entries je ON je.c0d = dl.journal_entry_id
              WHERE dl.asset_id = :a
              ORDER BY dl.period_year, dl.period_month",
            [':a' => $assetId]
        ) ?: [];
    }

    /**
     * Insert new asset. Returns new c0d.
     * $data expects: category_id, name, description, serial_no, purchase_date, purchase_cost,
     *   depreciation_method?, useful_life_months?, salvage_value?, depreciation_start_date?,
     *   custodian_name?, location?, notes?
     */
    public function create(array $data, int $userId): int
    {
        $db = new DB();
        // Auto asset_no
        $r = $db->mySelect("SELECT COUNT(*) n FROM assets", []);
        $seq = ((int)($r[0]['n'] ?? 0)) + 1;
        $assetNo = 'AST-' . date('Y') . '-' . str_pad((string)$seq, 4, '0', STR_PAD_LEFT);

        $cat = $this->categoryById((int)$data['category_id']);
        $method = $data['depreciation_method'] ?? ($cat['default_depreciation_method'] ?? 'SLM');
        $life   = (int)($data['useful_life_months'] ?? ($cat['default_useful_life_months'] ?? 60));
        $start  = $data['depreciation_start_date'] ?? $data['purchase_date'];
        $cost   = (float)($data['purchase_cost'] ?? 0);
        $salv   = (float)($data['salvage_value'] ?? 0);

        $fb = $db->insertUpdate(
            "INSERT INTO assets
                (asset_no, category_id, name, description, serial_no, purchase_date, purchase_cost,
                 depreciation_method, useful_life_months, salvage_value, depreciation_start_date,
                 accumulated_depreciation, current_book_value, status, custodian_name, location, notes,
                 created_by, created_on)
             VALUES (:no, :cat, :nm, :desc, :sn, :pd, :cost, :m, :life, :sv, :ds, 0, :bv, 'IN_SERVICE',
                     :cn, :loc, :nt, :u, NOW())",
            [
                ':no'   => $assetNo,
                ':cat'  => (int)$data['category_id'],
                ':nm'   => (string)($data['name'] ?? ''),
                ':desc' => (string)($data['description'] ?? ''),
                ':sn'   => (string)($data['serial_no'] ?? ''),
                ':pd'   => (string)$data['purchase_date'],
                ':cost' => $cost,
                ':m'    => $method,
                ':life' => $life,
                ':sv'   => $salv,
                ':ds'   => (string)$start,
                ':bv'   => $cost,
                ':cn'   => (string)($data['custodian_name'] ?? ''),
                ':loc'  => (string)($data['location'] ?? ''),
                ':nt'   => (string)($data['notes'] ?? ''),
                ':u'    => $userId,
            ]
        );
        return (int)($fb['last_id'] ?? 0);
    }

    /**
     * Dispose an asset. Computes NBV, gain/loss, posts JE, flips status = DISPOSED.
     * Journal shape (using the asset's category-linked COAs):
     *   Dr Bank (sale_price)                             — if sale > 0
     *   Dr Accumulated Depreciation (all of it)          — zeroes the contra
     *   Cr Asset Cost (all of it)                        — zeroes the asset
     *   Cr Gain on Disposal (7910) if sale > NBV
     *   Dr Loss on Disposal (6910) if sale < NBV
     * Returns ['disposal_id' => int, 'je_id' => int, 'nbv' => float, 'gain_or_loss' => float].
     */
    public function disposeAsset(int $assetId, string $disposalDate, string $method, float $salePrice, string $buyerName, ?int $bankAccountId, string $notes, int $userId, string $userName): array
    {
        $a = $this->find($assetId);
        if (!$a) throw new RuntimeException('Asset not found.');
        if ($a['status'] === 'DISPOSED') throw new RuntimeException('Asset already disposed.');

        $cat = $this->categoryById((int)$a['category_id']);
        if (!$cat || !$cat['coa_asset_account_id'] || !$cat['coa_accumulated_depreciation_account_id']) {
            throw new RuntimeException('Category is missing COA setup.');
        }

        $db = new DB();
        $cost = (float)$a['purchase_cost'];
        $acc  = (float)$a['accumulated_depreciation'];
        $nbv  = round($cost - $acc, 2);
        $gain = round($salePrice - $nbv, 2);

        // Build journal lines
        $lines = [];
        if ($salePrice > 0.01) {
            if ($bankAccountId === null || $bankAccountId <= 0) {
                // Fall back to first bank COA
                $r = $db->mySelect("SELECT c0d FROM chart_of_accounts WHERE is_bank = 1 AND is_active = 1 ORDER BY c0d LIMIT 1", []);
                if (empty($r)) throw new RuntimeException('No bank COA configured.');
                $bankAccountId = (int)$r[0]['c0d'];
            }
            $lines[] = ['account_id' => (int)$bankAccountId, 'debit' => $salePrice, 'credit' => 0, 'narrative' => 'Sale proceeds — ' . $a['asset_no']];
        }
        if ($acc > 0.01) {
            $lines[] = ['account_id' => (int)$cat['coa_accumulated_depreciation_account_id'], 'debit' => $acc, 'credit' => 0, 'narrative' => 'Reversal accumulated depreciation — ' . $a['asset_no']];
        }
        $lines[] = ['account_id' => (int)$cat['coa_asset_account_id'], 'debit' => 0, 'credit' => $cost, 'narrative' => 'Retire asset cost — ' . $a['asset_no']];

        // Gain/loss balancing entry
        if ($gain > 0.01) {
            $gainCoa = $db->mySelect("SELECT c0d FROM chart_of_accounts WHERE account_code = '7910' AND is_active = 1", []);
            if (empty($gainCoa)) throw new RuntimeException('Gain on Disposal COA (7910) missing.');
            $lines[] = ['account_id' => (int)$gainCoa[0]['c0d'], 'debit' => 0, 'credit' => $gain, 'narrative' => 'Gain on disposal — ' . $a['asset_no']];
        } elseif ($gain < -0.01) {
            $lossCoa = $db->mySelect("SELECT c0d FROM chart_of_accounts WHERE account_code = '6910' AND is_active = 1", []);
            if (empty($lossCoa)) throw new RuntimeException('Loss on Disposal COA (6910) missing.');
            $lines[] = ['account_id' => (int)$lossCoa[0]['c0d'], 'debit' => abs($gain), 'credit' => 0, 'narrative' => 'Loss on disposal — ' . $a['asset_no']];
        }

        if (!class_exists('JournalPosting')) require ROOT . '/app/models/JournalPosting.php';
        $jp = new JournalPosting();
        $res = $jp->post([
            'entry_date'     => $disposalDate,
            'source_type'    => 'ASSET_DISPOSAL',
            'source_id'      => $assetId,
            'source_ref'     => 'DISP-' . $a['asset_no'],
            'narrative'      => 'Asset disposal — ' . $a['asset_no'] . ' (' . $a['name'] . ') · ' . $method . ($salePrice > 0 ? ' · sale ' . number_format($salePrice) : ''),
            'currency'       => 'TZS',
            'posted_by'      => $userId,
            'posted_by_name' => $userName,
            'lines'          => $lines,
        ]);
        if (empty($res['ok'])) throw new RuntimeException('Journal post failed: ' . ($res['msg'] ?? 'unknown'));
        $jeId = (int)($res['id'] ?? 0);

        // Persist disposal + flip asset status
        $fb = $db->insertUpdate(
            "INSERT INTO asset_disposals
                (asset_id, disposal_date, disposal_method, sale_price, buyer_name,
                 net_book_value_at_disposal, gain_or_loss, journal_entry_id, notes, created_by, created_on)
             VALUES (:a, :d, :m, :sp, :b, :nbv, :gl, :je, :nt, :u, NOW())",
            [':a' => $assetId, ':d' => $disposalDate, ':m' => $method, ':sp' => $salePrice, ':b' => $buyerName,
             ':nbv' => $nbv, ':gl' => $gain, ':je' => $jeId, ':nt' => $notes, ':u' => $userId]
        );
        $disposalId = (int)($fb['last_id'] ?? 0);

        $db->insertUpdate(
            "UPDATE assets SET status = 'DISPOSED', current_book_value = 0, edited_by = :u, edited_on = NOW() WHERE c0d = :id",
            [':u' => $userId, ':id' => $assetId]
        );

        return ['disposal_id' => $disposalId, 'je_id' => $jeId, 'nbv' => $nbv, 'gain_or_loss' => $gain];
    }

    public function findDisposal(int $assetId): ?array
    {
        $db = new DB();
        $r = $db->mySelect(
            "SELECT d.*, je.je_no FROM asset_disposals d
        LEFT JOIN journal_entries je ON je.c0d = d.journal_entry_id
             WHERE d.asset_id = :a ORDER BY d.c0d DESC LIMIT 1",
            [':a' => $assetId]
        );
        return $r[0] ?? null;
    }

    private function categoryById(int $id): ?array
    {
        $db = new DB();
        $r = $db->mySelect("SELECT * FROM asset_categories WHERE c0d = :id", [':id' => $id]);
        return $r[0] ?? null;
    }

    /* ── Depreciation ─────────────────────────────────────────────────── */

    /**
     * Preview (no writes) — what this period's run would post per asset.
     * Returns rows: [asset_id, asset_no, category_id, cost, accumulated, book_value_before, depreciation_amount, book_value_after]
     */
    public function previewDepreciation(int $year, int $month): array
    {
        $periodEnd = date('Y-m-t', strtotime(sprintf('%04d-%02d-01', $year, $month)));
        $db = new DB();
        $assets = $db->mySelect(
            "SELECT a.*, c.default_useful_life_months, c.default_depreciation_method
               FROM assets a
          LEFT JOIN asset_categories c ON c.c0d = a.category_id
              WHERE a.deleted = 0 AND a.status = 'IN_SERVICE'
                AND a.depreciation_start_date <= :pe
                AND a.accumulated_depreciation < (a.purchase_cost - a.salvage_value)
                AND NOT EXISTS (SELECT 1 FROM asset_depreciation_lines dl
                                 WHERE dl.asset_id = a.c0d AND dl.period_year = :y AND dl.period_month = :m)",
            [':pe' => $periodEnd, ':y' => $year, ':m' => $month]
        ) ?: [];

        $out = [];
        foreach ($assets as $a) {
            $bookBefore = (float)$a['purchase_cost'] - (float)$a['accumulated_depreciation'];
            $remain     = (float)$a['purchase_cost'] - (float)$a['salvage_value'] - (float)$a['accumulated_depreciation'];
            $dep = 0.0;

            if ($a['depreciation_method'] === 'RB') {
                // Reducing balance: annual rate ~= 2 / years, monthly = book * (rate/12)
                $years = max(1, (int)$a['useful_life_months'] / 12);
                $annual = 2.0 / $years;
                $dep = round($bookBefore * ($annual / 12), 2);
            } else {
                // Straight-line
                $dep = round(((float)$a['purchase_cost'] - (float)$a['salvage_value']) / max(1, (int)$a['useful_life_months']), 2);
            }
            // Cap so we never depreciate below salvage
            if ($dep > $remain) $dep = round($remain, 2);
            if ($dep < 0.01) continue;

            $out[] = [
                'asset_id'            => (int)$a['c0d'],
                'asset_no'            => $a['asset_no'],
                'name'                => $a['name'],
                'category_id'         => (int)$a['category_id'],
                'cost'                => (float)$a['purchase_cost'],
                'accumulated'         => (float)$a['accumulated_depreciation'],
                'book_value_before'   => $bookBefore,
                'depreciation_amount' => $dep,
                'book_value_after'    => $bookBefore - $dep,
            ];
        }
        return $out;
    }

    /**
     * Actually run the depreciation for Y-M:
     *  - create one journal_entries per category (Dr Dep Exp, Cr Acc Dep)
     *  - one asset_depreciation_lines row per asset
     *  - update assets.accumulated_depreciation + current_book_value
     * Returns [je_ids => [...], line_count => n, total_amount => x].
     */
    public function runDepreciation(int $year, int $month, int $userId, string $userName): array
    {
        $preview = $this->previewDepreciation($year, $month);
        if (empty($preview)) return ['je_ids' => [], 'line_count' => 0, 'total_amount' => 0];

        // Group by category
        $byCat = [];
        foreach ($preview as $p) { $byCat[$p['category_id']][] = $p; }

        $db = new DB();
        $jeIds = [];
        $totalCount = 0; $totalAmt = 0.0;
        $periodEnd = date('Y-m-t', strtotime(sprintf('%04d-%02d-01', $year, $month)));

        // Require JournalPosting model
        if (!class_exists('JournalPosting')) require ROOT . '/app/models/JournalPosting.php';

        foreach ($byCat as $catId => $items) {
            $cat = $this->categoryById((int)$catId);
            if (!$cat || !$cat['coa_depreciation_expense_account_id'] || !$cat['coa_accumulated_depreciation_account_id']) {
                continue; // skip category with missing COA setup
            }
            $sumDep = 0.0;
            foreach ($items as $it) $sumDep += (float)$it['depreciation_amount'];

            $jp = new JournalPosting();
            $res = $jp->post([
                'entry_date'  => $periodEnd,
                'source_type' => 'DEPRECIATION',
                'source_id'   => 0,
                'source_ref'  => sprintf('DEP-%04d-%02d-CAT%d', $year, $month, (int)$catId),
                'narrative'   => 'Monthly depreciation run · ' . $cat['name'] . ' · ' . sprintf('%04d-%02d', $year, $month) . ' · ' . count($items) . ' asset(s)',
                'currency'    => 'TZS',
                'posted_by'   => $userId,
                'posted_by_name' => $userName,
                'lines' => [
                    ['account_id' => (int)$cat['coa_depreciation_expense_account_id'],       'debit' => $sumDep, 'credit' => 0, 'narrative' => 'Depreciation expense · ' . $cat['name']],
                    ['account_id' => (int)$cat['coa_accumulated_depreciation_account_id'],   'debit' => 0, 'credit' => $sumDep, 'narrative' => 'Accumulated depreciation · ' . $cat['name']],
                ],
            ]);
            if (empty($res['ok'])) {
                throw new RuntimeException('Journal post failed for category ' . $cat['name'] . ': ' . ($res['msg'] ?? 'unknown'));
            }
            $jeId = (int)($res['id'] ?? 0);
            $jeIds[] = $jeId;

            // Write per-asset dep line + update balances
            foreach ($items as $it) {
                $db->insertUpdate(
                    "INSERT INTO asset_depreciation_lines
                        (asset_id, period_year, period_month, depreciation_amount, book_value_after, journal_entry_id, posted_on)
                     VALUES (:a, :y, :m, :da, :bva, :je, NOW())",
                    [':a' => $it['asset_id'], ':y' => $year, ':m' => $month, ':da' => $it['depreciation_amount'],
                     ':bva' => $it['book_value_after'], ':je' => $jeId]
                );
                // NOTE: MySQL evaluates SET clauses left-to-right using already-updated values,
                // so referencing `accumulated_depreciation` in the 2nd clause reads the NEW value.
                // That's fine — we simply set book_value from the just-updated accumulated.
                $db->insertUpdate(
                    "UPDATE assets
                        SET accumulated_depreciation = accumulated_depreciation + :da,
                            current_book_value       = purchase_cost - accumulated_depreciation,
                            edited_by = :u, edited_on = NOW()
                      WHERE c0d = :id",
                    [':da' => $it['depreciation_amount'], ':u' => $userId, ':id' => $it['asset_id']]
                );
                $totalCount++;
                $totalAmt += (float)$it['depreciation_amount'];
            }
        }

        return ['je_ids' => $jeIds, 'line_count' => $totalCount, 'total_amount' => $totalAmt];
    }
}
