<?php
/**
 * ServicePmPlans — the first real consumer of the Work Templates engine (8b-T).
 *
 * A plan says: for THIS project, apply THIS PM template to THAT asset at THIS cadence.
 * generateNextInstances(planId, horizonDays) creates WorkInstances for every plan item
 * whose next_due falls within (today, today + horizonDays), then advances next_due by
 * one cadence-step. Idempotent within a window — a second call with the same horizon
 * generates nothing new.
 *
 * The generated WorkInstances target the asset directly (target_entity_type='assets')
 * so the Field Technician's mobile-ready queue (WorkInstances::myQueue) picks them up.
 */
class ServicePmPlans extends Model
{
    /* Cadence → days advance mapping. ON_DEMAND items don't auto-advance. */
    public const CADENCE_DAYS = [
        'DAILY'       => 1,
        'WEEKLY'      => 7,
        'FORTNIGHTLY' => 14,
        'MONTHLY'     => 30,
        'QUARTERLY'   => 90,
        'HALFYEARLY'  => 180,
        'YEARLY'      => 365,
        'ON_DEMAND'   => 0,
    ];

    /* ── Plan header ─────────────────────────────────────────────────── */

    public function create(int $projectId, array $d, int $userId, string $userName): int
    {
        if ($projectId <= 0) throw new RuntimeException('project_id required.');
        $title = trim((string)($d['title'] ?? ''));
        if ($title === '') throw new RuntimeException('Plan title required.');

        $db = new DB();
        $fb = $db->insertUpdate(
            "INSERT INTO service_pm_plans
                (project_id, title, description, planned_start, planned_end, status,
                 created_by, created_by_name, created_on)
             VALUES (:p, :t, :d, :ps, :pe, 'PLANNED', :u, :un, NOW())",
            [
                ':p' => $projectId, ':t' => $title, ':d' => (string)($d['description'] ?? ''),
                ':ps' => !empty($d['planned_start']) ? (string)$d['planned_start'] : null,
                ':pe' => !empty($d['planned_end'])   ? (string)$d['planned_end']   : null,
                ':u' => $userId, ':un' => $userName,
            ]
        );
        return (int)($fb['last_id'] ?? 0);
    }

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

    public function listForProject(int $projectId): array
    {
        $db = new DB();
        return $db->mySelect(
            "SELECT * FROM service_pm_plans WHERE project_id = :p AND deleted = 0 ORDER BY c0d DESC",
            [':p' => $projectId]
        ) ?: [];
    }

    public function activate(int $planId, int $userId): void
    {
        $db = new DB();
        $db->insertUpdate(
            "UPDATE service_pm_plans SET status = 'ACTIVE', activated_on = COALESCE(activated_on, NOW()),
                                         edited_by = :u, edited_on = NOW()
              WHERE c0d = :id AND status IN ('PLANNED','ACTIVE')",
            [':u' => $userId, ':id' => $planId]
        );
    }

    public function close(int $planId, int $userId): void
    {
        $db = new DB();
        $db->insertUpdate(
            "UPDATE service_pm_plans SET status = 'CLOSED', closed_on = NOW(),
                                         edited_by = :u, edited_on = NOW()
              WHERE c0d = :id",
            [':u' => $userId, ':id' => $planId]
        );
    }

    /* ── Plan items ──────────────────────────────────────────────────── */

    /**
     * Add an asset × template mapping. Idempotent — if the same triple already exists, update
     * cadence + next_due rather than inserting a duplicate.
     */
    public function addItem(int $planId, int $assetId, int $templateId, string $cadence, array $d): int
    {
        $plan = $this->findById($planId);
        if (!$plan) throw new RuntimeException('Plan not found.');
        if ($plan['status'] === 'CLOSED' || $plan['status'] === 'CANCELLED') {
            throw new RuntimeException('Plan is ' . $plan['status'] . ' — cannot edit items.');
        }
        $cadence = strtoupper($cadence);
        if (!isset(self::CADENCE_DAYS[$cadence])) throw new RuntimeException('Invalid cadence: ' . $cadence);

        // Verify template + asset exist.
        $db = new DB();
        $tplRow = $db->mySelect("SELECT c0d, is_active FROM work_templates WHERE c0d = :t AND deleted = 0", [':t' => $templateId]);
        if (empty($tplRow)) throw new RuntimeException('Template not found.');
        if ((int)$tplRow[0]['is_active'] !== 1) throw new RuntimeException('Template is inactive — pick the current version.');
        $assetRow = $db->mySelect("SELECT c0d FROM assets WHERE c0d = :a AND deleted = 0", [':a' => $assetId]);
        if (empty($assetRow)) throw new RuntimeException('Asset not found.');

        $nextDue = !empty($d['start_date']) ? (string)$d['start_date'] : date('Y-m-d');

        $existing = $db->mySelect(
            "SELECT c0d FROM service_pm_plan_items WHERE plan_id = :p AND asset_id = :a AND template_id = :t AND deleted = 0",
            [':p' => $planId, ':a' => $assetId, ':t' => $templateId]
        );
        if (!empty($existing)) {
            $db->insertUpdate(
                "UPDATE service_pm_plan_items
                    SET cadence = :c, next_due = :nd, default_assignee_id = :aid,
                        default_assignee_name = :anm, notes = :nt, is_active = 1
                  WHERE c0d = :id",
                [
                    ':c' => $cadence, ':nd' => $nextDue,
                    ':aid' => (int)($d['default_assignee_id'] ?? 0),
                    ':anm' => (string)($d['default_assignee_name'] ?? ''),
                    ':nt' => (string)($d['notes'] ?? ''),
                    ':id' => (int)$existing[0]['c0d'],
                ]
            );
            return (int)$existing[0]['c0d'];
        }
        $fb = $db->insertUpdate(
            "INSERT INTO service_pm_plan_items
                (plan_id, asset_id, template_id, cadence, next_due,
                 default_assignee_id, default_assignee_name, notes, created_on)
             VALUES (:p, :a, :t, :c, :nd, :aid, :anm, :nt, NOW())",
            [
                ':p' => $planId, ':a' => $assetId, ':t' => $templateId,
                ':c' => $cadence, ':nd' => $nextDue,
                ':aid' => (int)($d['default_assignee_id'] ?? 0),
                ':anm' => (string)($d['default_assignee_name'] ?? ''),
                ':nt' => (string)($d['notes'] ?? ''),
            ]
        );
        return (int)($fb['last_id'] ?? 0);
    }

    public function itemsFor(int $planId): array
    {
        $db = new DB();
        return $db->mySelect(
            "SELECT i.*, a.asset_no, a.name AS asset_name,
                    t.code AS template_code, t.title AS template_title, t.version AS template_version
               FROM service_pm_plan_items i
               JOIN assets a ON a.c0d = i.asset_id
               JOIN work_templates t ON t.c0d = i.template_id
              WHERE i.plan_id = :p AND i.deleted = 0
              ORDER BY i.next_due, i.c0d",
            [':p' => $planId]
        ) ?: [];
    }

    /* ── The generator — the whole point of a PM plan ───────────────── */

    /**
     * For every plan item whose next_due ≤ (today + horizonDays) and is_active=1,
     * create a WorkInstance targeting the asset with the item's template, then advance
     * next_due by one cadence-step. Skips items with an already-open WorkInstance for
     * the same scheduled_for date (idempotency).
     *
     * Returns an array of created instance ids.
     */
    public function generateNextInstances(int $planId, int $horizonDays = 30, int $actorId = 0, string $actorName = ''): array
    {
        $plan = $this->findById($planId);
        if (!$plan) throw new RuntimeException('Plan not found.');
        if ($plan['status'] !== 'ACTIVE') $this->activate($planId, $actorId);
        if (!class_exists('WorkInstances')) require_once ROOT . '/app/models/WorkInstances.php';
        $wi = new WorkInstances();

        $db = new DB();
        $horizon = date('Y-m-d', strtotime('+' . $horizonDays . ' days'));
        $items = $db->mySelect(
            "SELECT i.*, a.asset_no, a.name AS asset_name
               FROM service_pm_plan_items i
               JOIN assets a ON a.c0d = i.asset_id
              WHERE i.plan_id = :p AND i.deleted = 0 AND i.is_active = 1
                AND i.next_due IS NOT NULL AND i.next_due <= :h",
            [':p' => $planId, ':h' => $horizon]
        ) ?: [];

        $created = [];
        foreach ($items as $it) {
            $due  = (string)$it['next_due'];
            $cad  = strtoupper((string)$it['cadence']);
            $step = self::CADENCE_DAYS[$cad] ?? 0;

            // Idempotency: skip if a non-terminal WorkInstance already exists for (template, asset, due).
            $dupCheck = $db->mySelect(
                "SELECT c0d FROM work_instances
                  WHERE template_id = :t AND target_entity_type = 'assets' AND target_entity_id = :a
                    AND scheduled_for = :d AND status IN ('DRAFT','SUBMITTED') AND deleted = 0
                  LIMIT 1",
                [':t' => (int)$it['template_id'], ':a' => (int)$it['asset_id'], ':d' => $due]
            );
            if (!empty($dupCheck)) {
                // Advance next_due even if we skip (avoids re-processing same date next call).
                if ($step > 0) {
                    $newDue = date('Y-m-d', strtotime($due . ' +' . $step . ' days'));
                    $db->insertUpdate(
                        "UPDATE service_pm_plan_items SET next_due = :nd, last_generated_on = NOW() WHERE c0d = :id",
                        [':nd' => $newDue, ':id' => (int)$it['c0d']]
                    );
                }
                continue;
            }

            $refLabel = 'PM · ' . (string)$it['asset_name'] . ' (' . (string)$it['asset_no'] . ') · ' . $due;
            try {
                $instId = $wi->create(
                    (int)$it['template_id'], 'assets', (int)$it['asset_id'],
                    [
                        'ref_label' => $refLabel,
                        'scheduled_for' => $due,
                        'assignee_id' => (int)$it['default_assignee_id'],
                        'assignee_name' => (string)$it['default_assignee_name'],
                        'context_json' => json_encode([
                            'source' => 'ServicePmPlans',
                            'plan_id' => $planId,
                            'plan_item_id' => (int)$it['c0d'],
                            'cadence' => $cad,
                        ]),
                    ],
                    $actorId ?: (int)$plan['created_by'],
                    $actorName ?: (string)$plan['created_by_name']
                );
                if ($instId > 0) $created[] = $instId;
            } catch (\Throwable $e) {
                // Skip on error, but still advance next_due so we don't loop.
            }

            // Advance next_due unless ON_DEMAND
            if ($step > 0) {
                $newDue = date('Y-m-d', strtotime($due . ' +' . $step . ' days'));
                $db->insertUpdate(
                    "UPDATE service_pm_plan_items SET next_due = :nd, last_generated_on = NOW() WHERE c0d = :id",
                    [':nd' => $newDue, ':id' => (int)$it['c0d']]
                );
            } else {
                // ON_DEMAND: clear next_due so it doesn't auto-generate again until a user schedules it.
                $db->insertUpdate(
                    "UPDATE service_pm_plan_items SET next_due = NULL, last_generated_on = NOW() WHERE c0d = :id",
                    [':id' => (int)$it['c0d']]
                );
            }
        }
        return $created;
    }
}
