<?php
/**
 * PmEvents — one row per triggered PM cycle.
 *
 * State machine:
 *   upcoming  → the sweep created the row when the truck came within the
 *               template's warning threshold
 *   due       → within threshold (from either odometer or date)
 *   overdue   → past the planned km/date (fires the high-severity alert)
 *   completed → mechanic recorded the service. Schedule rolls forward.
 *   skipped   → deliberately not done, with reason. Schedule rolls forward
 *               using today's odo/date so we don't stack overdues.
 *
 * The sweep is idempotent — for each active schedule there is at most one
 * open event (upcoming|due|overdue). Once completed/skipped, the next
 * threshold crossing creates a fresh event.
 */
class PmEvents extends Model
{
    /* ==================== reads =========================================== */

    public function statusCounts(): array
    {
        $rs = (new DB())->mySelect(
            "SELECT status, COUNT(*) n FROM pm_events WHERE tenant_id = :tenant GROUP BY status",
            [':tenant' => defined('DEFAULT_TENANT_ID') ? DEFAULT_TENANT_ID : 1]
        ) ?: [];
        $counts = ['upcoming'=>0,'due'=>0,'overdue'=>0,'completed'=>0,'skipped'=>0];
        foreach ($rs as $r) $counts[$r['status']] = (int)$r['n'];
        return $counts;
    }

    public function listPaginated(int $page, int $perPage, array $filters = []): array
    {
        $where  = ['e.tenant_id = :tenant'];
        $params = [':tenant' => defined('DEFAULT_TENANT_ID') ? DEFAULT_TENANT_ID : 1];

        if (!empty($filters['status'])) {
            if ($filters['status'] === 'open') $where[] = "e.status IN ('upcoming','due','overdue')";
            else                                { $where[] = 'e.status = :st'; $params[':st'] = trim($filters['status']); }
        }
        if (!empty($filters['kind']))     { $where[] = 't.kind      = :kd'; $params[':kd'] = trim($filters['kind']); }
        if (!empty($filters['truck_id'])) { $where[] = 'e.truck_id  = :ti'; $params[':ti'] = (int)$filters['truck_id']; }
        if (!empty($filters['severity'])) { $where[] = 'e.severity  = :sv'; $params[':sv'] = trim($filters['severity']); }

        $whereSql = implode(' AND ', $where);
        $db = new DB();

        $countRs = $db->mySelect(
            "SELECT COUNT(*) AS n FROM pm_events e
               JOIN pm_templates t ON t.c0d = e.template_id
              WHERE $whereSql", $params
        );
        $total = (int)($countRs[0]['n'] ?? 0);

        $page    = max(1, $page);
        $perPage = max(1, min(200, $perPage));
        $offset  = ($page - 1) * $perPage;

        $rows = $db->mySelect(
            "SELECT e.c0d, e.event_no, e.status, e.severity, e.triggered_at,
                    e.planned_km, e.planned_date, e.due_since, e.overdue_since,
                    e.completed_at, e.completed_km, e.work_order_id,
                    t.name AS template_name, t.kind, t.priority, t.interval_km, t.interval_days,
                    e.truck_id, tr.reg_no, tr.make, tr.model, tr.current_odometer,
                    CASE
                        WHEN e.planned_km IS NOT NULL AND tr.current_odometer > e.planned_km
                            THEN tr.current_odometer - e.planned_km ELSE 0
                    END AS km_over,
                    CASE
                        WHEN e.planned_date IS NOT NULL AND e.planned_date < CURDATE()
                            THEN DATEDIFF(CURDATE(), e.planned_date) ELSE 0
                    END AS days_over
               FROM pm_events e
               JOIN pm_templates t ON t.c0d = e.template_id
               JOIN trucks       tr ON tr.c0d = e.truck_id
              WHERE $whereSql
           ORDER BY FIELD(e.status,'overdue','due','upcoming','completed','skipped'),
                    FIELD(e.severity,'critical','high','normal','low'),
                    e.planned_date ASC
              LIMIT $perPage OFFSET $offset",
            $params
        ) ?: [];

        return ['rows' => $rows, 'total' => $total];
    }

    public function byId(int $id): array
    {
        $rs = (new DB())->mySelect(
            "SELECT e.*, t.name AS template_name, t.kind, t.priority, t.interval_km, t.interval_days,
                    t.threshold_km, t.threshold_days, t.default_labour_hours, t.default_parts_cost,
                    tr.reg_no, tr.make, tr.model, tr.current_odometer,
                    u.full_name AS completed_by_name, s.full_name AS skipped_by_name
               FROM pm_events e
               JOIN pm_templates t ON t.c0d = e.template_id
               JOIN trucks       tr ON tr.c0d = e.truck_id
          LEFT JOIN users        u  ON u.c0d  = e.completed_by
          LEFT JOIN users        s  ON s.c0d  = e.skipped_by
              WHERE e.c0d = :id LIMIT 1",
            [':id' => $id]
        );
        return $rs[0] ?? [];
    }

    public function forTruck(int $truckId, int $limit = 20): array
    {
        return (new DB())->mySelect(
            "SELECT e.c0d, e.event_no, e.status, e.severity, e.triggered_at,
                    e.planned_km, e.planned_date, e.completed_at, e.completed_km,
                    t.name AS template_name, t.kind, t.priority
               FROM pm_events e
               JOIN pm_templates t ON t.c0d = e.template_id
              WHERE e.truck_id = :tid
           ORDER BY FIELD(e.status,'overdue','due','upcoming','completed','skipped'), e.triggered_at DESC
              LIMIT " . max(1, min(200, $limit)),
            [':tid' => $truckId]
        ) ?: [];
    }

    public function openForSchedule(int $scheduleId): array
    {
        $rs = (new DB())->mySelect(
            "SELECT * FROM pm_events WHERE schedule_id = :s AND status IN ('upcoming','due','overdue') LIMIT 1",
            [':s' => $scheduleId]
        );
        return $rs[0] ?? [];
    }

    /* ==================== writes ========================================== */

    /**
     * The sweep. Called by CronController::tickAction every 5 min.
     * Returns [emitted, updated] counts.
     */
    public function sweepDue(): array
    {
        $emitted = 0; $updated = 0;
        $schedules = (new TruckPmSchedules())->activeAll();
        if (empty($schedules)) return ['emitted' => 0, 'updated' => 0];

        $rules   = new AlertRules();
        $dueRule       = $rules->activeByKind('pm_due')[0]     ?? null;
        $overdueRule   = $rules->activeByKind('pm_overdue')[0] ?? null;
        $alerts = new Alerts();
        $db = new DB();
        $today = date('Y-m-d');

        foreach ($schedules as $s) {
            $tKm   = (int)$s['current_odometer'];
            $thKm  = (int)$s['threshold_km'];
            $thDay = (int)$s['threshold_days'];
            $nextKm   = $s['next_due_km']   !== null ? (int)$s['next_due_km']   : null;
            $nextDate = $s['next_due_date'] ?: null;

            // Decide state
            $isOverdueKm   = $nextKm   !== null && $tKm >= $nextKm;
            $isOverdueDate = $nextDate !== null && $today >= $nextDate;
            $isDueKm       = $nextKm   !== null && ($nextKm - $tKm) <= $thKm;
            $isDueDate     = $nextDate !== null && $thDay > 0 && (strtotime($nextDate) - strtotime($today)) / 86400 <= $thDay;

            $isOverdue = $isOverdueKm || $isOverdueDate;
            $isDue     = !$isOverdue && ($isDueKm || $isDueDate);

            if (!$isDue && !$isOverdue) continue;

            $newStatus   = $isOverdue ? 'overdue' : 'due';
            $newSeverity = $isOverdue ? 'high'    : ($s['priority'] === 'critical' ? 'high' : 'medium');

            // Idempotent: one open event per schedule
            $open = $this->openForSchedule((int)$s['c0d']);
            if (empty($open)) {
                $eventNo = $this->nextEventNo();
                $r = $db->insertUpdate(
                    "INSERT INTO pm_events
                        (tenant_id, event_no, truck_id, template_id, schedule_id, triggered_at, triggered_by,
                         planned_km, planned_date, status, severity,
                         due_since, overdue_since, created_on)
                     VALUES
                        (:tenant, :en, :tid, :tp, :sid, NOW(), 'cron',
                         :pkm, :pd, :st, :sev, :dsince, :osince, NOW())",
                    [
                        ':tenant' => defined('DEFAULT_TENANT_ID') ? DEFAULT_TENANT_ID : 1,
                        ':en' => $eventNo,
                        ':tid' => (int)$s['truck_id'], ':tp' => (int)$s['template_id'], ':sid' => (int)$s['c0d'],
                        ':pkm' => $nextKm, ':pd' => $nextDate,
                        ':st'  => $newStatus, ':sev' => $newSeverity,
                        ':dsince' => date('Y-m-d H:i:s'),
                        ':osince' => $isOverdue ? date('Y-m-d H:i:s') : null,
                    ]
                );
                $eventId = (int)$r['last_id'];
                $emitted++;
                $this->emitAlert($eventId, $s, $newStatus, $tKm, $isOverdue ? $overdueRule : $dueRule, $alerts);
            } else {
                // Existing event — escalate if the status has hardened
                $wasOverdue = $open['status'] === 'overdue';
                if ($newStatus === 'overdue' && !$wasOverdue) {
                    $db->insertUpdate(
                        "UPDATE pm_events SET status = 'overdue', severity = 'high', overdue_since = NOW() WHERE c0d = :id",
                        [':id' => (int)$open['c0d']]
                    );
                    $this->emitAlert((int)$open['c0d'], $s, 'overdue', $tKm, $overdueRule, $alerts);
                    $updated++;
                }
            }
        }
        return ['emitted' => $emitted, 'updated' => $updated];
    }

    private function emitAlert(int $eventId, array $sched, string $state, int $currentKm, ?array $rule, Alerts $alerts): void
    {
        if (!$rule) return;
        $title = ($state === 'overdue' ? 'PM overdue' : 'PM due') . " · " . $sched['reg_no'] . " · " . $sched['template_name'];
        $body  = "Truck {$sched['reg_no']} needs {$sched['template_name']}.";
        if (!empty($sched['next_due_km'])) {
            $delta = $currentKm - (int)$sched['next_due_km'];
            $body .= $delta >= 0
                ? " Odometer " . number_format($currentKm) . " km is " . number_format(abs($delta)) . " km past the planned " . number_format((int)$sched['next_due_km']) . " km."
                : " Odometer " . number_format($currentKm) . " km — planned at " . number_format((int)$sched['next_due_km']) . " km (" . number_format(abs($delta)) . " km ahead).";
        }
        if (!empty($sched['next_due_date'])) {
            $body .= " Planned date: " . $sched['next_due_date'] . ".";
        }

        $alertId = $alerts->emit($rule, [
            'device_id'   => null,
            'truck_id'    => (int)$sched['truck_id'],
            'ts'          => date('Y-m-d H:i:s'),
            'title'       => $title,
            'message'     => $body,
            'payload'     => [
                'event_id'   => $eventId,
                'schedule_id'=> (int)$sched['c0d'],
                'template'   => $sched['template_name'],
                'current_km' => $currentKm,
                'planned_km' => $sched['next_due_km'],
                'planned_date' => $sched['next_due_date'],
            ],
        ]);
        if ($alertId) {
            Alerts::queueNotifications($rule, $alertId, $title, $body);
            (new DB())->insertUpdate("UPDATE pm_events SET alert_id = :a WHERE c0d = :id", [':a' => $alertId, ':id' => $eventId]);
        }
    }

    /**
     * Mechanic marks the PM as done. Records completion and rolls the
     * schedule forward using the completion km/date so the next event
     * fires at the correct interval.
     */
    public function markCompleted(int $id, array $d, int $uid): array
    {
        $e = $this->byId($id);
        if (!$e)                              return ['ok' => false, 'msg' => 'Event not found.'];
        if (in_array($e['status'], ['completed','skipped'], true)) return ['ok' => false, 'msg' => "Already {$e['status']}."];

        $completedKm   = isset($d['completed_km']) && $d['completed_km'] !== '' ? (int)$d['completed_km'] : (int)$e['current_odometer'];
        $completedDate = $d['completed_date'] ?? date('Y-m-d');
        $woId          = !empty($d['work_order_id']) ? (int)$d['work_order_id'] : null;

        $db = new DB();
        $db->insertUpdate(
            "UPDATE pm_events
                SET status = 'completed', completed_at = NOW(),
                    completed_km = :km, completed_by = :uid, work_order_id = :wo,
                    notes = COALESCE(NULLIF(:notes, ''), notes)
              WHERE c0d = :id",
            [':km' => $completedKm, ':uid' => $uid, ':wo' => $woId, ':notes' => trim((string)($d['notes'] ?? '')), ':id' => $id]
        );

        // Roll schedule forward
        (new TruckPmSchedules())->updateBaseline((int)$e['schedule_id'], $completedKm, $completedDate, $uid);

        // Auto-resolve the alert (if any)
        if ($e['alert_id']) {
            $db->insertUpdate(
                "UPDATE alerts SET status = 'resolved', resolution_note = CONCAT('PM completed at ', :km, ' km'), acknowledged_by = COALESCE(acknowledged_by, :uid), acknowledged_at = COALESCE(acknowledged_at, NOW())
                  WHERE c0d = :id AND status IN ('open','acknowledged')",
                [':km' => $completedKm, ':uid' => $uid, ':id' => (int)$e['alert_id']]
            );
        }
        return ['ok' => true, 'msg' => 'PM marked completed.'];
    }

    public function skip(int $id, string $reason, int $uid): array
    {
        if ($reason === '') return ['ok' => false, 'msg' => 'A reason is required to skip.'];
        $e = $this->byId($id);
        if (!$e)                              return ['ok' => false, 'msg' => 'Event not found.'];
        if (in_array($e['status'], ['completed','skipped'], true)) return ['ok' => false, 'msg' => "Already {$e['status']}."];

        (new DB())->insertUpdate(
            "UPDATE pm_events SET status='skipped', skipped_by=:uid, skip_reason=:r WHERE c0d = :id",
            [':uid' => $uid, ':r' => mb_substr($reason, 0, 255), ':id' => $id]
        );
        // Roll schedule from today so we don't restack an immediate overdue
        (new TruckPmSchedules())->updateBaseline((int)$e['schedule_id'], (int)$e['current_odometer'], date('Y-m-d'), $uid);
        return ['ok' => true, 'msg' => 'PM skipped.'];
    }

    private function nextEventNo(): string
    {
        $year = (int)date('Y');
        $rs = (new DB())->mySelect(
            "SELECT event_no FROM pm_events WHERE event_no LIKE :p ORDER BY c0d DESC LIMIT 1",
            [':p' => 'PM-' . $year . '-%']
        );
        $seq = 1;
        if (!empty($rs) && preg_match('/-(\d+)$/', $rs[0]['event_no'], $m)) $seq = (int)$m[1] + 1;
        return sprintf('PM-%d-%04d', $year, $seq);
    }
}
