<?php
/**
 * TenderContractBonds · Slice 8Z-q.
 *
 * Tracks each bond / receipt / certificate attached to a Won tender through
 * its lifecycle (PENDING → RECEIVED → EXPIRED). Distinct from tender_contract_terms
 * (which holds one row of contract-level %s and refs) because each bond has its
 * own document, its own validity window, and its own status. This is what makes
 * FM's job on Tanzania tenders survivable — a single expiring PG mid-contract is
 * a real risk and the tracker drives the renewal alerts.
 *
 * File handling mirrors TenderSubmissionAttachments — same MIME whitelist, same
 * 10 MB cap, files under ROOT/uploads/tenders/{tenderId}/bonds/.
 */
class TenderContractBonds extends Model
{
    public const BOND_TYPES = [
        'CLIENT_ADVANCE'      => 'Client advance received',
        'APG'                 => 'Advance Payment Guarantee',
        'PERFORMANCE_BOND'    => 'Performance bond',
        'RETENTION_BOND'      => 'Retention bond',
        'INSURANCE_CERT'      => 'Insurance certificate',
        'TAX_CLEARANCE'       => 'TRA tax clearance',
        'VAT_EXEMPTION_CERT'  => 'VAT exemption certificate',
        'OTHER_GUARANTEE'     => 'Other guarantee',
    ];

    public const STATUSES = ['PENDING','RECEIVED','NA','EXPIRED'];

    public const MAX_BYTES = 10 * 1024 * 1024;

    public const ALLOWED_MIMES = [
        'application/pdf',
        'image/jpeg', 'image/png', 'image/webp', 'image/heic', 'image/gif',
        'application/msword',
        'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
        'application/vnd.ms-excel',
        'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
        'application/zip', 'application/x-zip-compressed',
    ];

    public function listFor(int $tenderId): array
    {
        $db = new DB();
        return $db->mySelect(
            "SELECT * FROM tender_contract_bonds
              WHERE tender_id = :t AND deleted = 0
              ORDER BY FIELD(bond_type,'CLIENT_ADVANCE','APG','PERFORMANCE_BOND','RETENTION_BOND','INSURANCE_CERT','TAX_CLEARANCE','VAT_EXEMPTION_CERT','OTHER_GUARANTEE'), c0d ASC",
            [':t' => $tenderId]
        ) ?: [];
    }

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

    /**
     * Upsert a bond row (no file — that's uploadDoc()).
     * If $data['c0d'] is set → UPDATE; else INSERT.
     */
    public function save(int $tenderId, array $d, int $userId, string $userName): array
    {
        if ($tenderId <= 0) return ['ok' => false, 'msg' => 'tender_id required.'];
        $type = strtoupper((string)($d['bond_type'] ?? ''));
        if (!isset(self::BOND_TYPES[$type])) return ['ok' => false, 'msg' => 'Unknown bond type.'];
        $status = strtoupper((string)($d['status'] ?? 'PENDING'));
        if (!in_array($status, self::STATUSES, true)) $status = 'PENDING';

        $params = [
            ':t'   => $tenderId,
            ':bt'  => $type,
            ':s'   => $status,
            ':rn'  => trim((string)($d['reference_no']   ?? '')) ?: null,
            ':is'  => trim((string)($d['issuer']         ?? '')) ?: null,
            ':am'  => isset($d['amount']) && $d['amount'] !== '' ? (float)$d['amount'] : null,
            ':cur' => strtoupper(trim((string)($d['currency'] ?? 'TZS'))) ?: 'TZS',
            ':rd'  => !empty($d['received_date'])  ? (string)$d['received_date']  : null,
            ':vu'  => !empty($d['validity_until']) ? (string)$d['validity_until'] : null,
            ':rem' => trim((string)($d['remarks'] ?? '')) ?: null,
        ];

        $db = new DB();
        $id = (int)($d['c0d'] ?? 0);
        if ($id > 0) {
            // Verify tender ownership before mutating
            $cur = $this->find($id);
            if (!$cur || (int)$cur['tender_id'] !== $tenderId) return ['ok' => false, 'msg' => 'Bond not found on this tender.'];
            // UPDATE doesn't reference :t (tender_id) — drop it so PDO's strict param binding is happy.
            unset($params[':t']);
            $params[':id'] = $id;
            $params[':eb'] = $userId;
            $db->insertUpdate(
                "UPDATE tender_contract_bonds SET
                    bond_type=:bt, status=:s, reference_no=:rn, issuer=:is,
                    amount=:am, currency=:cur, received_date=:rd, validity_until=:vu,
                    remarks=:rem, edited_by=:eb, edited_on=NOW()
                  WHERE c0d=:id",
                $params
            );
            return ['ok' => true, 'id' => $id];
        }
        $params[':cb']  = $userId;
        $params[':cbn'] = $userName;
        $r = $db->insertUpdate(
            "INSERT INTO tender_contract_bonds
                (tender_id, bond_type, status, reference_no, issuer, amount, currency,
                 received_date, validity_until, remarks,
                 created_by, created_by_name, created_on)
             VALUES (:t, :bt, :s, :rn, :is, :am, :cur, :rd, :vu, :rem, :cb, :cbn, NOW())",
            $params
        );
        return ['ok' => true, 'id' => (int)($r['last_id'] ?? 0)];
    }

    /** Attach / replace the supporting document on a bond row. $file = $_FILES entry. */
    public function uploadDoc(int $bondId, array $file, int $userId): array
    {
        $cur = $this->find($bondId);
        if (!$cur) return ['ok' => false, 'msg' => 'Bond not found.'];
        if (empty($file) || !isset($file['tmp_name']) || $file['error'] !== UPLOAD_ERR_OK) {
            return ['ok' => false, 'msg' => 'No file received (upload error ' . ($file['error'] ?? '?') . ').'];
        }
        $size = (int)$file['size'];
        if ($size <= 0) return ['ok' => false, 'msg' => 'File is empty.'];
        if ($size > self::MAX_BYTES) return ['ok' => false, 'msg' => 'File too large (max ' . (self::MAX_BYTES / 1024 / 1024) . ' MB).'];

        $probed = null;
        if (function_exists('finfo_open')) {
            $fi = finfo_open(FILEINFO_MIME_TYPE);
            if ($fi) { $probed = finfo_file($fi, $file['tmp_name']) ?: null; finfo_close($fi); }
        }
        $mime = $probed ?: (string)($file['type'] ?? '');
        if (!in_array($mime, self::ALLOWED_MIMES, true)) {
            return ['ok' => false, 'msg' => 'File type "' . htmlspecialchars($mime) . '" not allowed. Use PDF, image, DOC/DOCX, XLS/XLSX or ZIP.'];
        }

        $tid  = (int)$cur['tender_id'];
        $origName = basename((string)$file['name']);
        $safe = preg_replace('/[^A-Za-z0-9._-]+/', '_', $origName);
        if (strlen($safe) > 120) $safe = substr($safe, -120);
        $dir = ROOT . '/uploads/tenders/' . $tid . '/bonds';
        if (!is_dir($dir) && !@mkdir($dir, 0775, true)) return ['ok' => false, 'msg' => 'Server could not create upload directory.'];
        $fname = uniqid('bond', true) . '_' . $safe;
        $abs   = $dir . '/' . $fname;
        $rel   = 'uploads/tenders/' . $tid . '/bonds/' . $fname;
        if (!@move_uploaded_file($file['tmp_name'], $abs)) return ['ok' => false, 'msg' => 'Server could not save the file.'];

        // Delete the old file if there was one (audit row survives; only the file is replaced).
        if (!empty($cur['document_path'])) {
            $old = ROOT . '/' . ltrim((string)$cur['document_path'], '/');
            if (is_file($old)) @unlink($old);
        }

        $db = new DB();
        $db->insertUpdate(
            "UPDATE tender_contract_bonds SET
                document_path=:sp, document_original_filename=:of, document_mime=:m, document_size_bytes=:sz,
                edited_by=:eb, edited_on=NOW()
              WHERE c0d=:id",
            [':sp' => $rel, ':of' => $origName, ':m' => $mime, ':sz' => $size, ':eb' => $userId, ':id' => $bondId]
        );
        return ['ok' => true];
    }

    /** Soft-delete + best-effort unlink. */
    public function remove(int $id, int $userId): array
    {
        $cur = $this->find($id);
        if (!$cur) return ['ok' => false, 'msg' => 'Bond not found.'];
        $db = new DB();
        $db->insertUpdate(
            "UPDATE tender_contract_bonds SET deleted=1, edited_by=:u, edited_on=NOW() WHERE c0d=:id",
            [':u' => $userId, ':id' => $id]
        );
        if (!empty($cur['document_path'])) {
            $abs = ROOT . '/' . ltrim((string)$cur['document_path'], '/');
            if (is_file($abs)) @unlink($abs);
        }
        return ['ok' => true];
    }

    /** Absolute filesystem path for download, or null if missing on disk. */
    public function absoluteDocPath(array $row): ?string
    {
        if (empty($row['document_path'])) return null;
        $abs = ROOT . '/' . ltrim((string)$row['document_path'], '/');
        return is_file($abs) ? $abs : null;
    }

    /**
     * Roll-up view used by FM: for each bond row, decorate with an expiry chip so
     * the view doesn't have to repeat the date math.
     *   OK        · > 30 days to expiry, or no expiry required
     *   DUE_SOON  · 0–30 days to expiry
     *   OVERDUE   · past validity_until (auto-flipped to EXPIRED for FM to confirm)
     */
    public function withExpiry(array $bonds): array
    {
        $today = new DateTime(date('Y-m-d'));
        foreach ($bonds as &$b) {
            $b['expiry_chip'] = 'OK';
            if (!empty($b['validity_until'])) {
                $exp = DateTime::createFromFormat('Y-m-d', substr((string)$b['validity_until'], 0, 10));
                if ($exp) {
                    $diff = (int)$today->diff($exp)->format('%r%a');
                    if      ($diff < 0)  $b['expiry_chip'] = 'OVERDUE';
                    elseif  ($diff <= 30) $b['expiry_chip'] = 'DUE_SOON';
                }
            }
        }
        unset($b);
        return $bonds;
    }
}
