<?php
/**
 * WorkTemplates — reusable checklist blueprints.
 *
 * One template engine, three consumer modules:
 *   - Services: PM plan templates (scope=PM, target=asset)     → Field Technician queue
 *   - Works:    Inspection / handover  (scope=INSPECTION/HANDOVER, target=works_phase / c_projects)
 *   - Goods:    Receiving checklists   (scope=RECEIVING, target=lpo / goods_client_pos)
 *
 * Version fork rule: once a template has any live (non-terminal) instances, editing items
 * requires forking a new version. The old row stays visible but is-active=0; in-flight
 * instances keep pointing at the old version_id via template_id + template_version.
 */
class WorkTemplates extends Model
{
    const SCOPES = ['PM','INSPECTION','HANDOVER','RECEIVING','SAFETY','CHECKLIST'];
    const INPUT_TYPES = ['CHECK','NUMBER','TEXT','LONGTEXT','SELECT','DATE','PHOTO','SIGNATURE'];
    const CADENCES = ['ON_DEMAND','DAILY','WEEKLY','FORTNIGHTLY','MONTHLY','QUARTERLY','HALFYEARLY','YEARLY'];

    /* ── Template header CRUD ─────────────────────────────────────────── */

    public function create(array $d, int $uid, string $uname): int
    {
        $code = trim((string)($d['code'] ?? ''));
        if ($code === '') throw new RuntimeException('Template code required.');
        $title = trim((string)($d['title'] ?? ''));
        if ($title === '') throw new RuntimeException('Template title required.');
        $scope = strtoupper((string)($d['scope'] ?? 'CHECKLIST'));
        if (!in_array($scope, self::SCOPES, true)) throw new RuntimeException('Invalid scope.');

        $db = new DB();
        $existing = $db->mySelect("SELECT COUNT(*) n FROM work_templates WHERE code = :c AND deleted = 0", [':c' => $code]);
        if ((int)$existing[0]['n'] > 0) throw new RuntimeException('A template with code "' . $code . '" already exists — use "New version" to fork it.');

        $fb = $db->insertUpdate(
            "INSERT INTO work_templates
                (code, title, description, scope, target_entity_type, cadence, version, is_active,
                 expected_minutes, reviewer_role, supersedes_id, created_by, created_by_name, created_on)
             VALUES (:c, :t, :d, :s, :te, :cd, 1, 1,
                     :em, :rr, 0, :u, :un, NOW())",
            [
                ':c' => $code, ':t' => $title, ':d' => (string)($d['description'] ?? ''),
                ':s' => $scope, ':te' => (string)($d['target_entity_type'] ?? 'c_projects'),
                ':cd' => (string)($d['cadence'] ?? 'ON_DEMAND'),
                ':em' => (int)($d['expected_minutes'] ?? 0),
                ':rr' => (string)($d['reviewer_role'] ?? ''),
                ':u' => $uid, ':un' => $uname,
            ]
        );
        return (int)($fb['last_id'] ?? 0);
    }

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

    /** Return the currently-active (highest-version, is_active=1) row for a given code. */
    public function findActiveByCode(string $code): ?array
    {
        $db = new DB();
        $r = $db->mySelect(
            "SELECT * FROM work_templates WHERE code = :c AND is_active = 1 AND deleted = 0
              ORDER BY version DESC LIMIT 1",
            [':c' => $code]
        );
        return $r[0] ?? null;
    }

    public function listActive(string $scope = '', string $targetEntityType = ''): array
    {
        $db = new DB();
        $sql = "SELECT * FROM work_templates WHERE is_active = 1 AND deleted = 0";
        $p = [];
        if ($scope !== '')            { $sql .= " AND scope = :s"; $p[':s'] = $scope; }
        if ($targetEntityType !== '') { $sql .= " AND target_entity_type = :te"; $p[':te'] = $targetEntityType; }
        $sql .= " ORDER BY scope, title";
        return $db->mySelect($sql, $p) ?: [];
    }

    /** All templates (all versions + inactive) for the admin listing. */
    public function listAll(): array
    {
        $db = new DB();
        return $db->mySelect("SELECT * FROM work_templates WHERE deleted = 0 ORDER BY code, version DESC", []) ?: [];
    }

    public function updateHeader(int $id, array $d, int $uid): void
    {
        $t = $this->findById($id);
        if (!$t) throw new RuntimeException('Template not found.');
        if ($this->hasLiveInstances($id)) {
            throw new RuntimeException('Template has live instances — fork a new version instead of editing.');
        }
        $db = new DB();
        $db->insertUpdate(
            "UPDATE work_templates SET
                title = :t, description = :d, cadence = :cd, expected_minutes = :em,
                reviewer_role = :rr, edited_by = :u, edited_on = NOW()
              WHERE c0d = :id",
            [
                ':t' => (string)($d['title'] ?? $t['title']),
                ':d' => (string)($d['description'] ?? $t['description']),
                ':cd' => (string)($d['cadence'] ?? $t['cadence']),
                ':em' => (int)($d['expected_minutes'] ?? $t['expected_minutes']),
                ':rr' => (string)($d['reviewer_role'] ?? $t['reviewer_role']),
                ':u' => $uid, ':id' => $id,
            ]
        );
    }

    /** True if the template has any non-terminal instances (DRAFT/SUBMITTED). */
    public function hasLiveInstances(int $templateId): bool
    {
        $db = new DB();
        $r = $db->mySelect(
            "SELECT COUNT(*) n FROM work_instances
              WHERE template_id = :t AND status IN ('DRAFT','SUBMITTED') AND deleted = 0",
            [':t' => $templateId]
        );
        return (int)($r[0]['n'] ?? 0) > 0;
    }

    /**
     * Fork: create version N+1 as an editable copy of an existing template.
     * The current version is deactivated so new instances pick up the fork; live instances
     * on the old version keep pointing at their frozen version_id.
     */
    public function forkNewVersion(int $sourceId, int $uid, string $uname): int
    {
        $src = $this->findById($sourceId);
        if (!$src) throw new RuntimeException('Source template not found.');

        $db = new DB();
        // Find max version for this code
        $vMax = (int)($db->mySelect("SELECT MAX(version) n FROM work_templates WHERE code = :c AND deleted = 0", [':c' => $src['code']])[0]['n'] ?? 0);
        $newVersion = $vMax + 1;

        $fb = $db->insertUpdate(
            "INSERT INTO work_templates
                (code, title, description, scope, target_entity_type, cadence, version, is_active,
                 expected_minutes, reviewer_role, supersedes_id, created_by, created_by_name, created_on)
             VALUES (:c, :t, :d, :s, :te, :cd, :v, 1,
                     :em, :rr, :sup, :u, :un, NOW())",
            [
                ':c' => $src['code'], ':t' => $src['title'], ':d' => (string)$src['description'],
                ':s' => $src['scope'], ':te' => $src['target_entity_type'], ':cd' => $src['cadence'],
                ':v' => $newVersion,
                ':em' => (int)$src['expected_minutes'], ':rr' => (string)$src['reviewer_role'],
                ':sup' => $sourceId, ':u' => $uid, ':un' => $uname,
            ]
        );
        $newId = (int)($fb['last_id'] ?? 0);

        // Copy items
        foreach ($this->itemsForTemplate($sourceId) as $it) {
            $this->addItem($newId, [
                'section' => (string)$it['section'],
                'sort_order' => (int)$it['sort_order'],
                'prompt' => (string)$it['prompt'],
                'input_type' => (string)$it['input_type'],
                'is_required' => (int)$it['is_required'],
                'options_json' => (string)($it['options_json'] ?? ''),
                'depends_on_item_id' => (int)$it['depends_on_item_id'],
                'depends_on_value' => (string)$it['depends_on_value'],
                'help_text' => (string)$it['help_text'],
                'numeric_min' => $it['numeric_min'],
                'numeric_max' => $it['numeric_max'],
                'numeric_unit' => (string)$it['numeric_unit'],
            ]);
        }

        // Deactivate the old one
        $db->insertUpdate("UPDATE work_templates SET is_active = 0, edited_by = :u, edited_on = NOW() WHERE c0d = :id", [':u' => $uid, ':id' => $sourceId]);
        return $newId;
    }

    public function deactivate(int $id, int $uid): void
    {
        $db = new DB();
        $db->insertUpdate("UPDATE work_templates SET is_active = 0, edited_by = :u, edited_on = NOW() WHERE c0d = :id", [':u' => $uid, ':id' => $id]);
    }

    /* ── Item CRUD ────────────────────────────────────────────────────── */

    public function itemsForTemplate(int $templateId): array
    {
        $db = new DB();
        return $db->mySelect(
            "SELECT * FROM work_template_items WHERE template_id = :t AND deleted = 0 ORDER BY sort_order, c0d",
            [':t' => $templateId]
        ) ?: [];
    }

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

    private function assertItemEditable(int $templateId): void
    {
        if ($this->hasLiveInstances($templateId)) {
            throw new RuntimeException('Cannot edit items: template has live instances — fork a new version first.');
        }
    }

    public function addItem(int $templateId, array $d): int
    {
        $this->assertItemEditable($templateId);
        $type = strtoupper((string)($d['input_type'] ?? 'CHECK'));
        if (!in_array($type, self::INPUT_TYPES, true)) throw new RuntimeException('Invalid input type.');
        $db = new DB();
        // If no sort_order given, append.
        $nextOrder = isset($d['sort_order']) && (int)$d['sort_order'] > 0
            ? (int)$d['sort_order']
            : (int)($db->mySelect("SELECT COALESCE(MAX(sort_order),0)+10 n FROM work_template_items WHERE template_id = :t AND deleted = 0", [':t' => $templateId])[0]['n'] ?? 10);

        $fb = $db->insertUpdate(
            "INSERT INTO work_template_items
                (template_id, section, sort_order, prompt, input_type, is_required, options_json,
                 depends_on_item_id, depends_on_value, help_text,
                 numeric_min, numeric_max, numeric_unit, created_on)
             VALUES (:t, :sec, :so, :p, :it, :req, :oj,
                     :doi, :dov, :ht,
                     :nmin, :nmax, :nu, NOW())",
            [
                ':t' => $templateId,
                ':sec' => (string)($d['section'] ?? ''),
                ':so' => $nextOrder,
                ':p' => (string)($d['prompt'] ?? ''),
                ':it' => $type,
                ':req' => !empty($d['is_required']) ? 1 : 0,
                ':oj' => (string)($d['options_json'] ?? ''),
                ':doi' => (int)($d['depends_on_item_id'] ?? 0),
                ':dov' => (string)($d['depends_on_value'] ?? ''),
                ':ht' => (string)($d['help_text'] ?? ''),
                ':nmin' => $d['numeric_min'] ?? null,
                ':nmax' => $d['numeric_max'] ?? null,
                ':nu' => (string)($d['numeric_unit'] ?? ''),
            ]
        );
        return (int)($fb['last_id'] ?? 0);
    }

    public function updateItem(int $itemId, array $d): void
    {
        $it = $this->findItem($itemId);
        if (!$it) throw new RuntimeException('Item not found.');
        $this->assertItemEditable((int)$it['template_id']);
        $type = strtoupper((string)($d['input_type'] ?? $it['input_type']));
        if (!in_array($type, self::INPUT_TYPES, true)) throw new RuntimeException('Invalid input type.');
        $db = new DB();
        $db->insertUpdate(
            "UPDATE work_template_items SET
                section = :sec, sort_order = :so, prompt = :p, input_type = :it, is_required = :req,
                options_json = :oj, depends_on_item_id = :doi, depends_on_value = :dov,
                help_text = :ht, numeric_min = :nmin, numeric_max = :nmax, numeric_unit = :nu
              WHERE c0d = :id",
            [
                ':sec' => (string)($d['section'] ?? $it['section']),
                ':so' => (int)($d['sort_order'] ?? $it['sort_order']),
                ':p' => (string)($d['prompt'] ?? $it['prompt']),
                ':it' => $type,
                ':req' => !empty($d['is_required']) ? 1 : 0,
                ':oj' => (string)($d['options_json'] ?? $it['options_json']),
                ':doi' => (int)($d['depends_on_item_id'] ?? $it['depends_on_item_id']),
                ':dov' => (string)($d['depends_on_value'] ?? $it['depends_on_value']),
                ':ht' => (string)($d['help_text'] ?? $it['help_text']),
                ':nmin' => $d['numeric_min'] ?? $it['numeric_min'],
                ':nmax' => $d['numeric_max'] ?? $it['numeric_max'],
                ':nu' => (string)($d['numeric_unit'] ?? $it['numeric_unit']),
                ':id' => $itemId,
            ]
        );
    }

    public function deleteItem(int $itemId): void
    {
        $it = $this->findItem($itemId);
        if (!$it) return;
        $this->assertItemEditable((int)$it['template_id']);
        $db = new DB();
        $db->insertUpdate("UPDATE work_template_items SET deleted = 1 WHERE c0d = :id", [':id' => $itemId]);
    }
}
