<?php
/**
 * TenderServiceSpec — Slice 8S-10
 *
 * The "as bid" answers CRM captures on a Services tender to the 10 CRM checklist questions.
 * Storage is a single fat row per tender (with equipment_json for the flexible per-type
 * breakdown). Goods/Works tenders never touch this model.
 *
 * When the tender is WON → converted, TenderServiceSpec::toContractPayload() maps the answers
 * into the shape ServiceContracts::saveHeader() expects so PC doesn't re-type anything.
 */
class TenderServiceSpec extends Model
{
    /** Get the spec for a tender (empty array if not filled yet). */
    public function forTender(int $tenderId): array
    {
        $db = new DB();
        $r  = $db->mySelect(
            "SELECT * FROM tender_service_spec WHERE tender_id = :t",
            [':t' => $tenderId]
        );
        return $r[0] ?? [];
    }

    /**
     * Upsert the spec for a tender. Returns the row id.
     * Only writes fields that are actually set in $d (partial updates supported).
     */
    public function save(int $tenderId, array $d, int $uid, string $uname): int
    {
        if ($tenderId <= 0) throw new InvalidArgumentException('tender_id required.');

        // Normalise enums / booleans / equipment_json
        $existing = $this->forTender($tenderId);
        $isNew    = empty($existing);

        // Whitelist writable fields — anything else is ignored.
        $fields = [
            'no_of_sites'                 => fn($v) => $v === '' || $v === null ? null : (int)$v,
            'sites_notes'                 => fn($v) => trim((string)$v) ?: null,
            'equipment_json'              => fn($v) => $this->normaliseEquipmentJson($v),
            'pm_cadence'                  => fn($v) => $this->enum($v, ['MONTHLY','QUARTERLY','SEMI_ANNUAL','ANNUAL','ADHOC']),
            'pm_payment_structure'        => fn($v) => $this->enum($v, ['MONTHLY_RETAINER','PER_EVENT','BUNDLED_WITH_CALLOUTS']),
            'has_callouts'                => fn($v) => !empty($v) ? 1 : 0,
            'callout_payment_structure'   => fn($v) => $this->enum($v, ['PER_CALLOUT_FEE','T_AND_M','BUNDLED','FREE_QUOTA_PLUS_FEE']),
            'callout_fee'                 => fn($v) => $v === '' || $v === null ? null : (float)$v,
            'emergency_multiplier'        => fn($v) => $v === '' || $v === null ? null : (float)$v,
            'free_callouts_per_month'     => fn($v) => $v === '' || $v === null ? null : (int)$v,
            'sla_emergency_hrs'           => fn($v) => $v === '' || $v === null ? null : (int)$v,
            'sla_urgent_hrs'              => fn($v) => $v === '' || $v === null ? null : (int)$v,
            'sla_routine_hrs'             => fn($v) => $v === '' || $v === null ? null : (int)$v,
            'spare_policy'                => fn($v) => $this->enum($v, ['COMPANY','CLIENT','CONSIGNMENT','FIXED_PACK']),
            'spare_policy_notes'          => fn($v) => trim((string)$v) ?: null,
            'client_supplied_pack'        => fn($v) => !empty($v) ? 1 : 0,
            'service_pack_notes'          => fn($v) => trim((string)$v) ?: null,
            'invoicing_cadence'           => fn($v) => $this->enum($v, ['MONTHLY_RETAINER','PER_EVENT','MILESTONE','CONSUMPTION']),
            'payment_terms_days'          => fn($v) => $v === '' || $v === null ? null : (int)$v,
            'labour_rate_per_hr'          => fn($v) => $v === '' || $v === null ? null : (float)$v,
            'mileage_rate_per_km'         => fn($v) => $v === '' || $v === null ? null : (float)$v,
            'parts_markup_pct'            => fn($v) => $v === '' || $v === null ? null : (float)$v,
            'part_replacement_sop_agreed' => fn($v) => !empty($v) ? 1 : 0,
            'part_replacement_notes'      => fn($v) => trim((string)$v) ?: null,
            'general_notes'               => fn($v) => trim((string)$v) ?: null,
        ];

        $sets = [];
        $params = [':t' => $tenderId, ':u' => $uid, ':un' => $uname];
        foreach ($fields as $k => $normFn) {
            if (!array_key_exists($k, $d)) continue; // skip fields not in payload — partial update
            $sets[$k] = $normFn($d[$k]);
            $params[':' . $k] = $sets[$k];
        }

        $db = new DB();
        if ($isNew) {
            // Include tender_id + audit
            $cols = array_keys($sets);
            $colList  = implode(', ', array_merge(['tender_id','created_by','created_by_name'], $cols));
            $valList  = implode(', ', array_merge([':t',':u',':un'], array_map(fn($k) => ':' . $k, $cols)));
            $db->insertUpdate(
                "INSERT INTO tender_service_spec ($colList) VALUES ($valList)",
                $params
            );
            $r = $db->mySelect("SELECT c0d FROM tender_service_spec WHERE tender_id = :t", [':t' => $tenderId]);
            return (int)($r[0]['c0d'] ?? 0);
        }
        if (empty($sets)) return (int)$existing['c0d']; // nothing to update
        // UPDATE path uses no :un placeholder; drop it so PDO's strict param binding doesn't complain.
        unset($params[':un']);
        $setSql = implode(', ', array_map(fn($k) => "$k = :$k", array_keys($sets))) . ', edited_by = :u, edited_on = NOW()';
        $db->insertUpdate(
            "UPDATE tender_service_spec SET $setSql WHERE tender_id = :t",
            $params
        );
        return (int)$existing['c0d'];
    }

    /**
     * Map the tender spec into the shape ServiceContracts::saveHeader() expects.
     * Called by Tenders::convertToProject when converting a WON Services tender.
     */
    public function toContractPayload(int $tenderId, int $projectId, int $customerId, string $customerName): array
    {
        $s = $this->forTender($tenderId);
        if (empty($s)) return [];
        return [
            'project_id'              => $projectId,
            'customer_id'             => $customerId,
            'customer_name'           => $customerName,
            'currency'                => 'TZS',
            'spare_policy'            => $s['spare_policy']       ?: null,
            'invoice_cadence'         => $s['invoicing_cadence']  ?: null,
            'payment_terms_days'      => $s['payment_terms_days'] ?: 30,
            'callout_fee'             => $s['callout_fee'],
            'emergency_multiplier'    => $s['emergency_multiplier'],
            'free_callouts_per_month' => $s['free_callouts_per_month'] ?? 0,
            'labour_rate_per_hr'      => $s['labour_rate_per_hr'],
            'mileage_rate_per_km'     => $s['mileage_rate_per_km'],
            'parts_markup_pct'        => $s['parts_markup_pct'],
            'notes'                   => trim(($s['general_notes'] ?? '') . "\n\n" . ($s['spare_policy_notes'] ?? '')),
        ];
    }

    /** Decoded equipment list for the view + convert step. */
    public function decodedEquipment(array $spec): array
    {
        if (empty($spec['equipment_json'])) return [];
        $r = json_decode((string)$spec['equipment_json'], true);
        return is_array($r) ? $r : [];
    }

    /* ── Internals ─────────────────────────────────────────────────────── */

    private function enum($v, array $allowed): ?string
    {
        $s = trim((string)$v);
        return in_array($s, $allowed, true) ? $s : null;
    }

    private function normaliseEquipmentJson($v): ?string
    {
        if ($v === null || $v === '') return null;
        // Accept either a JSON string or an array (from AJAX json_encode-once)
        $arr = is_string($v) ? json_decode($v, true) : (is_array($v) ? $v : null);
        if (!is_array($arr)) return null;
        $clean = [];
        foreach ($arr as $row) {
            if (!is_array($row)) continue;
            $type  = trim((string)($row['type']  ?? ''));
            $size  = trim((string)($row['size']  ?? ''));
            $count = (int)($row['count'] ?? 0);
            if ($type === '' && $count <= 0) continue;
            $clean[] = ['type' => $type, 'size' => $size, 'count' => max(0, $count)];
        }
        return json_encode($clean, JSON_UNESCAPED_UNICODE);
    }
}
