<?php
/**
 * CronController — public endpoint hit by cPanel cron every 5 minutes.
 * Configured cron entry (cPanel):
 *     [every-5-min]  curl -s "https://.../Cron/tick?token=<CRON_TOKEN>"
 *
 * Auth: query token matched against CRON_TOKEN config constant. NO session,
 * NO CSRF (same treatment as GpsIngest — registered in Router::PUBLIC_CONTROLLERS).
 *
 * Two workers:
 *   tickAction    — runs every 5 min: alerts engine (async rules) + outbox dispatch
 *   nightlyAction — once a day: housekeeping (device offline sweep, digest emails)
 *
 * Each run is logged to cron_runs so operators can prove the cron is alive.
 */
class CronController extends Controller
{
    public function tickAction(): void
    {
        header('Content-Type: application/json');
        if (!$this->authOk()) { http_response_code(401); echo json_encode(['ok'=>false,'msg'=>'Unauthorized']); return; }

        @set_time_limit(60);
        $db = new DB();

        // Log start
        $r = $db->insertUpdate(
            "INSERT INTO cron_runs (worker, started_at) VALUES ('tick', NOW())", []
        );
        $runId = (int)$r['last_id'];

        // Load watermark from previous successful run
        $prev = $db->mySelect(
            "SELECT watermark_after FROM cron_runs
              WHERE worker='tick' AND ok=1 AND watermark_after IS NOT NULL
           ORDER BY c0d DESC LIMIT 1", []
        );
        $watermark = $prev[0]['watermark_after'] ?? date('Y-m-d H:i:s', strtotime('-6 minutes'));

        $stats = ['positions_scanned'=>0,'events_scanned'=>0,'alerts_created'=>0,
                  'sms_sent'=>0,'sms_failed'=>0,'emails_sent'=>0,'emails_failed'=>0,
                  'ok'=>true,'error'=>null,'watermark_after'=>$watermark];

        try {
            // 1) Evaluate async rules — device_offline sweep
            $stats['alerts_created'] += $this->sweepDeviceOffline();

            // 2) PM sweep — schedule → event with alert
            $pm = (new PmEvents())->sweepDue();
            $stats['alerts_created'] += (int)$pm['emitted'] + (int)$pm['updated'];

            // 2b) Doc-expiry sweep — trucks + drivers docs → 3-bracket alerts
            $doc = (new DocExpiries())->sweep();
            $stats['alerts_created'] += (int)$doc['emitted'];

            // 3) Advance watermark to now (positions/events between $watermark and now
            //    were already evaluated synchronously in GpsPositions::ingest).
            $stats['watermark_after'] = date('Y-m-d H:i:s');

            // 4) Dispatch pending outbox
            $sms   = (new Outbox())->dispatchPendingSms(20);
            $email = (new Outbox())->dispatchPendingEmail(20);
            $stats['sms_sent']      = $sms['sent'];
            $stats['sms_failed']    = $sms['failed'];
            $stats['emails_sent']   = $email['sent'];
            $stats['emails_failed'] = $email['failed'];
        } catch (Throwable $e) {
            $stats['ok']    = false;
            $stats['error'] = mb_substr($e->getMessage(), 0, 480);
        }

        // Finalize run
        $db->insertUpdate(
            "UPDATE cron_runs
                SET finished_at = NOW(), watermark_before = :wb, watermark_after = :wa,
                    positions_scanned = :ps, events_scanned = :es, alerts_created = :ac,
                    sms_sent = :ss, sms_failed = :sf, emails_sent = :ms, emails_failed = :mf,
                    ok = :ok, error = :err
              WHERE c0d = :id",
            [
                ':wb'=>$watermark, ':wa'=>$stats['watermark_after'],
                ':ps'=>$stats['positions_scanned'], ':es'=>$stats['events_scanned'],
                ':ac'=>$stats['alerts_created'],
                ':ss'=>$stats['sms_sent'], ':sf'=>$stats['sms_failed'],
                ':ms'=>$stats['emails_sent'],':mf'=>$stats['emails_failed'],
                ':ok'=>$stats['ok'] ? 1 : 0, ':err'=>$stats['error'],
                ':id'=>$runId,
            ]
        );

        echo json_encode(['ok'=>$stats['ok'],'run_id'=>$runId,'stats'=>$stats]);
    }

    public function nightlyAction(): void
    {
        header('Content-Type: application/json');
        if (!$this->authOk()) { http_response_code(401); echo json_encode(['ok'=>false,'msg'=>'Unauthorized']); return; }

        $db = new DB();
        $r  = $db->insertUpdate("INSERT INTO cron_runs (worker, started_at) VALUES ('nightly', NOW())", []);
        $runId = (int)$r['last_id'];

        try {
            // Mark devices offline if not seen in > 30 min
            $db->insertUpdate(
                "UPDATE gps_devices
                    SET status = 'offline'
                  WHERE status = 'active' AND deleted = 0
                    AND (last_seen_at IS NULL OR last_seen_at < NOW() - INTERVAL 30 MINUTE)",
                []
            );
            // (Future) daily digest emails to fleet managers — TODO Iter 3.3
        } catch (Throwable $e) {
            $db->insertUpdate("UPDATE cron_runs SET finished_at=NOW(), ok=0, error=:e WHERE c0d=:id",
                [':e'=>mb_substr($e->getMessage(),0,480), ':id'=>$runId]);
            echo json_encode(['ok'=>false,'msg'=>$e->getMessage()]);
            return;
        }

        $db->insertUpdate("UPDATE cron_runs SET finished_at=NOW(), ok=1 WHERE c0d=:id", [':id'=>$runId]);
        echo json_encode(['ok'=>true,'run_id'=>$runId]);
    }

    private function authOk(): bool
    {
        $token    = $_GET['token'] ?? '';
        $expected = defined('CRON_TOKEN') ? CRON_TOKEN : '';
        return $expected !== '' && $token !== '' && hash_equals($expected, (string)$token);
    }

    /**
     * device_offline rule sweep: for each active rule of kind=device_offline,
     * find devices whose last_seen_at exceeds the offline_minutes threshold and
     * emit alerts (dedup'd by cooldown).
     */
    private function sweepDeviceOffline(): int
    {
        $rules  = (new AlertRules())->activeByKind('device_offline');
        if (empty($rules)) return 0;

        $alerts = new Alerts();
        $db     = new DB();
        $count  = 0;

        foreach ($rules as $rule) {
            $th    = json_decode((string)$rule['threshold_json'], true) ?: [];
            $mins  = max(1, (int)($th['offline_minutes'] ?? 30));

            $devs = $db->mySelect(
                "SELECT g.c0d AS device_id, g.device_code, g.truck_id, t.reg_no,
                        g.last_seen_at, TIMESTAMPDIFF(MINUTE, g.last_seen_at, NOW()) AS mins_offline
                   FROM gps_devices g
              LEFT JOIN trucks t ON t.c0d = g.truck_id
                  WHERE g.deleted = 0 AND g.status IN ('active','offline')
                    AND g.last_seen_at IS NOT NULL
                    AND g.last_seen_at < NOW() - INTERVAL :m MINUTE",
                [':m' => $mins]
            ) ?: [];

            foreach ($devs as $d) {
                $reg = $d['reg_no'] ?: $d['device_code'];
                $id  = $alerts->emit($rule, [
                    'device_id' => (int)$d['device_id'],
                    'truck_id'  => $d['truck_id'] ? (int)$d['truck_id'] : null,
                    'ts'        => date('Y-m-d H:i:s'),
                    'title'     => "Device offline · {$reg}",
                    'message'   => "Tracker {$d['device_code']}" . ($d['reg_no'] ? " ({$reg})" : '') . " has not reported for " . (int)$d['mins_offline'] . " minutes.",
                    'payload'   => ['minutes_offline' => (int)$d['mins_offline'], 'last_seen_at' => $d['last_seen_at']],
                ]);
                if ($id) {
                    Alerts::queueNotifications($rule, $id, "Device offline · {$reg}", "Tracker {$d['device_code']} has not reported for {$d['mins_offline']} minutes.");
                    $count++;
                }
            }
        }
        return $count;
    }
}
