<?php
/**
 * WorkInstances — live executions of a work template.
 *
 * State machine:
 *   DRAFT  →  SUBMITTED  →  APPROVED   (terminal)
 *          ↘              ↗
 *            REJECTED  →  reopen() → DRAFT
 *   any → CANCELLED (terminal, aborted)
 *
 * Per-item results are UPSERTED so a mobile client can retry POST safely.
 * Attachments dedup on sha256 within the instance so a mobile burst-upload of the same
 * photo can't create duplicates.
 */
class WorkInstances extends Model
{
    /**
     * Create a fresh instance targeting (entity_type, entity_id). Stamps the template version
     * so that later template edits (via forkNewVersion) don't retroactively change what the
     * assignee sees.
     */
    public function create(int $templateId, string $targetEntityType, int $targetEntityId, array $d, int $uid, string $uname): int
    {
        if (!class_exists('WorkTemplates')) require_once ROOT . '/app/models/WorkTemplates.php';
        $wt = new WorkTemplates();
        $tpl = $wt->findById($templateId);
        if (!$tpl) throw new RuntimeException('Template not found.');
        if ((int)$tpl['is_active'] !== 1 && empty($d['force_inactive'])) throw new RuntimeException('Template is inactive — pick the current active version.');
        if ($targetEntityId <= 0) throw new RuntimeException('Target entity id required.');

        $db = new DB();
        $fb = $db->insertUpdate(
            "INSERT INTO work_instances
                (template_id, template_version, target_entity_type, target_entity_id, ref_label,
                 scheduled_for, assignee_id, assignee_name, status, context_json, created_by, created_on)
             VALUES (:t, :tv, :tet, :tei, :rl,
                     :sf, :aid, :anm, 'DRAFT', :cx, :u, NOW())",
            [
                ':t' => $templateId, ':tv' => (int)$tpl['version'],
                ':tet' => $targetEntityType, ':tei' => $targetEntityId,
                ':rl' => (string)($d['ref_label'] ?? ($tpl['title'] . ' · ' . $targetEntityType . ' #' . $targetEntityId)),
                ':sf' => !empty($d['scheduled_for']) ? (string)$d['scheduled_for'] : null,
                ':aid' => (int)($d['assignee_id'] ?? 0),
                ':anm' => (string)($d['assignee_name'] ?? ''),
                ':cx' => !empty($d['context_json']) ? (string)$d['context_json'] : null,
                ':u' => $uid,
            ]
        );
        return (int)($fb['last_id'] ?? 0);
    }

    public function findById(int $id): ?array
    {
        $db = new DB();
        $r = $db->mySelect(
            "SELECT i.*, t.code AS template_code, t.title AS template_title, t.scope, t.target_entity_type AS tpl_target_entity_type
               FROM work_instances i
               JOIN work_templates t ON t.c0d = i.template_id
              WHERE i.c0d = :id AND i.deleted = 0",
            [':id' => $id]
        );
        return $r[0] ?? null;
    }

    public function forTarget(string $targetEntityType, int $targetEntityId): array
    {
        $db = new DB();
        return $db->mySelect(
            "SELECT i.*, t.code AS template_code, t.title AS template_title, t.scope
               FROM work_instances i
               JOIN work_templates t ON t.c0d = i.template_id
              WHERE i.target_entity_type = :tet AND i.target_entity_id = :tei AND i.deleted = 0
              ORDER BY i.c0d DESC",
            [':tet' => $targetEntityType, ':tei' => $targetEntityId]
        ) ?: [];
    }

    /** Assignee-facing queue — all open instances I own or that are unclaimed and match my role hint. */
    public function myQueue(int $userId, string $status = ''): array
    {
        if ($userId <= 0) return [];
        $db = new DB();
        $sql = "SELECT i.*, t.code AS template_code, t.title AS template_title, t.scope
                  FROM work_instances i
                  JOIN work_templates t ON t.c0d = i.template_id
                 WHERE i.assignee_id = :u AND i.deleted = 0";
        $p = [':u' => $userId];
        if ($status !== '') { $sql .= " AND i.status = :s"; $p[':s'] = $status; }
        $sql .= " ORDER BY i.scheduled_for ASC, i.c0d DESC";
        return $db->mySelect($sql, $p) ?: [];
    }

    public function unclaimedForRole(string $roleHint): array
    {
        // Unclaimed pool. Consumer filters by scope/target if desired.
        $db = new DB();
        return $db->mySelect(
            "SELECT i.*, t.code AS template_code, t.title AS template_title, t.scope
               FROM work_instances i
               JOIN work_templates t ON t.c0d = i.template_id
              WHERE i.assignee_id = 0 AND i.status = 'DRAFT' AND i.deleted = 0
              ORDER BY i.scheduled_for ASC, i.c0d DESC",
            []
        ) ?: [];
    }

    /* ── Claim / assign ───────────────────────────────────────────────── */

    public function claim(int $instanceId, int $userId, string $userName): void
    {
        $inst = $this->findById($instanceId);
        if (!$inst) throw new RuntimeException('Instance not found.');
        if ((int)$inst['assignee_id'] > 0 && (int)$inst['assignee_id'] !== $userId) {
            throw new RuntimeException('Already claimed by ' . ($inst['assignee_name'] ?: '#' . $inst['assignee_id']));
        }
        if (!in_array($inst['status'], ['DRAFT'], true)) throw new RuntimeException('Only DRAFT instances can be claimed.');
        $db = new DB();
        $db->insertUpdate(
            "UPDATE work_instances SET assignee_id = :u, assignee_name = :un, claimed_at = NOW(),
                                       edited_by = :u, edited_on = NOW()
              WHERE c0d = :id",
            [':u' => $userId, ':un' => $userName, ':id' => $instanceId]
        );
    }

    public function reassign(int $instanceId, int $userId, string $userName, int $actorId): void
    {
        $inst = $this->findById($instanceId);
        if (!$inst) throw new RuntimeException('Instance not found.');
        if (!in_array($inst['status'], ['DRAFT','REJECTED'], true)) throw new RuntimeException('Only DRAFT / REJECTED instances can be reassigned.');
        $db = new DB();
        $db->insertUpdate(
            "UPDATE work_instances SET assignee_id = :u, assignee_name = :un, edited_by = :a, edited_on = NOW()
              WHERE c0d = :id",
            [':u' => $userId, ':un' => $userName, ':a' => $actorId, ':id' => $instanceId]
        );
    }

    /* ── Result upsert (mobile-safe) ──────────────────────────────────── */

    /**
     * Save one item's answer. Idempotent — retried POSTs collapse.
     * Also bumps status DRAFT → DRAFT (implicit "in progress" is DRAFT until submit).
     */
    public function setResult(int $instanceId, int $templateItemId, array $vals, int $actorId): void
    {
        $inst = $this->findById($instanceId);
        if (!$inst) throw new RuntimeException('Instance not found.');
        if (!in_array($inst['status'], ['DRAFT','REJECTED'], true)) throw new RuntimeException('Instance is ' . $inst['status'] . ' — cannot save further results.');
        if ($templateItemId <= 0) throw new RuntimeException('Item id required.');

        $db = new DB();
        // Load item to route the value into the right column.
        $itemRow = $db->mySelect("SELECT input_type, template_id FROM work_template_items WHERE c0d = :i", [':i' => $templateItemId]);
        if (empty($itemRow)) throw new RuntimeException('Template item not found.');
        if ((int)$itemRow[0]['template_id'] !== (int)$inst['template_id']) throw new RuntimeException('Item does not belong to this instance\'s template.');
        $type = strtoupper((string)$itemRow[0]['input_type']);

        $valueText = null; $valueNumber = null; $valueDate = null;
        switch ($type) {
            case 'NUMBER':    $valueNumber = isset($vals['value']) && $vals['value'] !== '' ? (float)$vals['value'] : null; break;
            case 'DATE':      $valueDate   = !empty($vals['value']) ? (string)$vals['value'] : null; break;
            case 'CHECK':     $valueText   = (string)($vals['value'] ?? '') !== '' ? (string)$vals['value'] : null; break; // OK / NOTOK / NA
            case 'SELECT':
            case 'TEXT':
            case 'LONGTEXT':
            case 'PHOTO':
            case 'SIGNATURE':
            default:          $valueText   = isset($vals['value']) ? (string)$vals['value'] : null; break;
        }
        $note        = (string)($vals['note'] ?? '');
        $timeSource  = strtoupper((string)($vals['time_source'] ?? 'SERVER'));
        if (!in_array($timeSource, ['SERVER','DEVICE','GPS'], true)) $timeSource = 'SERVER';

        $db->insertUpdate(
            "INSERT INTO work_instance_results
                (instance_id, template_item_id, value_text, value_number, value_date, note,
                 answered_at, answered_by, time_source)
             VALUES (:i, :ti, :vt, :vn, :vd, :nt, NOW(), :ab, :ts)
             ON DUPLICATE KEY UPDATE
                value_text = VALUES(value_text),
                value_number = VALUES(value_number),
                value_date = VALUES(value_date),
                note = VALUES(note),
                answered_at = NOW(),
                answered_by = VALUES(answered_by),
                time_source = VALUES(time_source)",
            [
                ':i' => $instanceId, ':ti' => $templateItemId,
                ':vt' => $valueText, ':vn' => $valueNumber, ':vd' => $valueDate,
                ':nt' => $note, ':ab' => $actorId, ':ts' => $timeSource,
            ]
        );

        // Stamp started_at on first save
        if (empty($inst['started_at'])) {
            $db->insertUpdate("UPDATE work_instances SET started_at = NOW() WHERE c0d = :id AND started_at IS NULL", [':id' => $instanceId]);
        }
    }

    public function resultsFor(int $instanceId): array
    {
        $db = new DB();
        return $db->mySelect("SELECT * FROM work_instance_results WHERE instance_id = :i", [':i' => $instanceId]) ?: [];
    }

    /** Convenience: returns results keyed by template_item_id for view rendering. */
    public function resultsMap(int $instanceId): array
    {
        $out = [];
        foreach ($this->resultsFor($instanceId) as $r) $out[(int)$r['template_item_id']] = $r;
        return $out;
    }

    /* ── State transitions ──────────────────────────────────────────── */

    public function submit(int $instanceId, int $actorId, string $actorName): void
    {
        $inst = $this->findById($instanceId);
        if (!$inst) throw new RuntimeException('Instance not found.');
        if (!in_array($inst['status'], ['DRAFT','REJECTED'], true)) throw new RuntimeException('Instance is ' . $inst['status'] . '.');
        if ((int)$inst['assignee_id'] !== $actorId && (int)$inst['assignee_id'] !== 0) {
            // Only the assignee submits; if unclaimed, the actor implicitly claims + submits.
            throw new RuntimeException('Only the assignee can submit.');
        }
        // Guard: every required item must have a result.
        if (!class_exists('WorkTemplates')) require_once ROOT . '/app/models/WorkTemplates.php';
        $items = (new WorkTemplates())->itemsForTemplate((int)$inst['template_id']);
        $results = $this->resultsMap($instanceId);
        $missing = [];
        foreach ($items as $it) {
            if ((int)$it['is_required'] !== 1) continue;
            if (!isset($results[(int)$it['c0d']])) $missing[] = (string)$it['prompt'];
        }
        if (!empty($missing)) throw new RuntimeException('Required items missing: ' . implode(' · ', array_slice($missing, 0, 3)) . (count($missing) > 3 ? ' …' : ''));

        $db = new DB();
        $db->insertUpdate(
            "UPDATE work_instances SET status = 'SUBMITTED', submitted_at = NOW(),
                                       assignee_id = IF(assignee_id = 0, :a, assignee_id),
                                       assignee_name = IF(assignee_name = '', :an, assignee_name),
                                       edited_by = :a, edited_on = NOW()
              WHERE c0d = :id",
            [':a' => $actorId, ':an' => $actorName, ':id' => $instanceId]
        );
    }

    public function approve(int $instanceId, int $reviewerId, string $reviewerName, string $notes = ''): void
    {
        $inst = $this->findById($instanceId);
        if (!$inst) throw new RuntimeException('Instance not found.');
        if ($inst['status'] !== 'SUBMITTED') throw new RuntimeException('Only SUBMITTED instances can be approved.');
        $db = new DB();
        $db->insertUpdate(
            "UPDATE work_instances SET status = 'APPROVED', reviewed_at = NOW(),
                                       reviewed_by = :r, reviewed_by_name = :rn, review_notes = :nt,
                                       edited_by = :r, edited_on = NOW()
              WHERE c0d = :id",
            [':r' => $reviewerId, ':rn' => $reviewerName, ':nt' => $notes, ':id' => $instanceId]
        );
    }

    public function reject(int $instanceId, int $reviewerId, string $reviewerName, string $reason): void
    {
        $inst = $this->findById($instanceId);
        if (!$inst) throw new RuntimeException('Instance not found.');
        if ($inst['status'] !== 'SUBMITTED') throw new RuntimeException('Only SUBMITTED instances can be rejected.');
        if (trim($reason) === '') throw new RuntimeException('Reason required to reject.');
        $db = new DB();
        $db->insertUpdate(
            "UPDATE work_instances SET status = 'REJECTED', reviewed_at = NOW(),
                                       reviewed_by = :r, reviewed_by_name = :rn, review_notes = :nt,
                                       edited_by = :r, edited_on = NOW()
              WHERE c0d = :id",
            [':r' => $reviewerId, ':rn' => $reviewerName, ':nt' => $reason, ':id' => $instanceId]
        );
    }

    public function reopen(int $instanceId, int $actorId): void
    {
        $inst = $this->findById($instanceId);
        if (!$inst) throw new RuntimeException('Instance not found.');
        if (!in_array($inst['status'], ['REJECTED'], true)) throw new RuntimeException('Only REJECTED instances can be reopened.');
        $db = new DB();
        $db->insertUpdate(
            "UPDATE work_instances SET status = 'DRAFT', edited_by = :a, edited_on = NOW() WHERE c0d = :id",
            [':a' => $actorId, ':id' => $instanceId]
        );
    }

    public function cancel(int $instanceId, int $actorId, string $reason): void
    {
        $inst = $this->findById($instanceId);
        if (!$inst) throw new RuntimeException('Instance not found.');
        if (in_array($inst['status'], ['APPROVED','CANCELLED'], true)) throw new RuntimeException('Cannot cancel — instance is ' . $inst['status'] . '.');
        $db = new DB();
        $db->insertUpdate(
            "UPDATE work_instances SET status = 'CANCELLED', review_notes = :nt,
                                       edited_by = :a, edited_on = NOW()
              WHERE c0d = :id",
            [':nt' => $reason, ':a' => $actorId, ':id' => $instanceId]
        );
    }

    /* ── Attachments (schema wired; upload UI is a later slice) ──────── */

    /** Idempotent on (instance_id, sha256) — mobile burst-safe. */
    public function addAttachment(int $instanceId, array $d, int $actorId): int
    {
        $sha = strtolower(trim((string)($d['sha256'] ?? '')));
        if ($sha === '' || strlen($sha) !== 64) throw new RuntimeException('sha256 required (64 hex chars).');
        $db = new DB();
        $existing = $db->mySelect("SELECT c0d FROM work_instance_attachments WHERE instance_id = :i AND sha256 = :s", [':i' => $instanceId, ':s' => $sha]);
        if (!empty($existing)) return (int)$existing[0]['c0d'];

        $fb = $db->insertUpdate(
            "INSERT INTO work_instance_attachments
                (instance_id, template_item_id, phase, file_path, mime_type, size_bytes, sha256, caption,
                 gps_lat, gps_lng, device_captured_at, uploaded_at, time_source, time_trust, uploaded_by)
             VALUES (:i, :ti, :ph, :fp, :mt, :sz, :sha, :cap,
                     :lat, :lng, :dca, NOW(), :ts, :tt, :u)",
            [
                ':i' => $instanceId, ':ti' => (int)($d['template_item_id'] ?? 0),
                ':ph' => strtoupper((string)($d['phase'] ?? 'GENERAL')),
                ':fp' => (string)($d['file_path'] ?? ''), ':mt' => (string)($d['mime_type'] ?? ''),
                ':sz' => (int)($d['size_bytes'] ?? 0), ':sha' => $sha,
                ':cap' => (string)($d['caption'] ?? ''),
                ':lat' => $d['gps_lat'] ?? null, ':lng' => $d['gps_lng'] ?? null,
                ':dca' => !empty($d['device_captured_at']) ? (string)$d['device_captured_at'] : null,
                ':ts' => strtoupper((string)($d['time_source'] ?? 'SERVER')),
                ':tt' => strtoupper((string)($d['time_trust'] ?? 'TRUSTED')),
                ':u' => $actorId,
            ]
        );
        return (int)($fb['last_id'] ?? 0);
    }

    public function attachmentsFor(int $instanceId): array
    {
        $db = new DB();
        return $db->mySelect(
            "SELECT * FROM work_instance_attachments WHERE instance_id = :i AND deleted = 0 ORDER BY c0d",
            [':i' => $instanceId]
        ) ?: [];
    }

    /* ── Mobile-clean serialisation ─────────────────────────────────── */

    /**
     * Return an instance in the exact shape a mobile client (or future API) can consume.
     * Includes the frozen template snapshot (items) + results + attachments.
     */
    public function toArray(int $instanceId): ?array
    {
        $inst = $this->findById($instanceId);
        if (!$inst) return null;
        if (!class_exists('WorkTemplates')) require_once ROOT . '/app/models/WorkTemplates.php';
        $items = (new WorkTemplates())->itemsForTemplate((int)$inst['template_id']);
        $results = $this->resultsMap($instanceId);
        $itemsOut = [];
        foreach ($items as $it) {
            $iid = (int)$it['c0d'];
            $r = $results[$iid] ?? null;
            $itemsOut[] = [
                'id'                => $iid,
                'section'           => $it['section'],
                'sort_order'        => (int)$it['sort_order'],
                'prompt'            => $it['prompt'],
                'input_type'        => $it['input_type'],
                'is_required'       => (int)$it['is_required'] === 1,
                'options_json'      => $it['options_json'],
                'depends_on_item_id'=> (int)$it['depends_on_item_id'],
                'depends_on_value'  => $it['depends_on_value'],
                'help_text'         => $it['help_text'],
                'numeric'           => ['min' => $it['numeric_min'], 'max' => $it['numeric_max'], 'unit' => $it['numeric_unit']],
                'result'            => $r ? [
                    'value_text'   => $r['value_text'],
                    'value_number' => $r['value_number'],
                    'value_date'   => $r['value_date'],
                    'note'         => $r['note'],
                    'answered_at'  => $r['answered_at'],
                    'time_source'  => $r['time_source'],
                ] : null,
            ];
        }

        return [
            'id'               => (int)$inst['c0d'],
            'template_id'      => (int)$inst['template_id'],
            'template_code'    => $inst['template_code'],
            'template_title'   => $inst['template_title'],
            'template_version' => (int)$inst['template_version'],
            'scope'            => $inst['scope'],
            'target_entity_type'=> $inst['target_entity_type'],
            'target_entity_id' => (int)$inst['target_entity_id'],
            'ref_label'        => $inst['ref_label'],
            'status'           => $inst['status'],
            'scheduled_for'    => $inst['scheduled_for'],
            'assignee_id'      => (int)$inst['assignee_id'],
            'assignee_name'    => $inst['assignee_name'],
            'started_at'       => $inst['started_at'],
            'submitted_at'     => $inst['submitted_at'],
            'reviewed_at'      => $inst['reviewed_at'],
            'reviewed_by_name' => $inst['reviewed_by_name'],
            'review_notes'     => $inst['review_notes'],
            'items'            => $itemsOut,
            'attachments'      => $this->attachmentsFor($instanceId),
        ];
    }
}
