<?php
/**
 * GpsDashboard — read-only queries for the live map, truck detail, trip
 * playback, and alerts feed. No writes; every mutation lives in the
 * public GpsIngest controller and its own model layer.
 *
 * Design note: all lat/lng values are cast to float in PHP-land so the
 * JSON responses can be consumed directly by Leaflet without another
 * coercion pass in JS.
 */
class GpsDashboard extends Model
{
    private int $tenantId;

    public function __construct(?int $tenantId = null)
    {
        $this->tenantId = $tenantId ?? (defined('DEFAULT_TENANT_ID') ? DEFAULT_TENANT_ID : 1);
    }

    /* ══════════════════════════════════════════════════════════════════
       Live map: every device with a known truck + the last position.
       ══════════════════════════════════════════════════════════════════ */
    public function livePositions(): array
    {
        $db = new DB();
        $rows = $db->mySelect(
            "SELECT d.c0d               AS device_id,
                    d.device_code, d.status AS device_status, d.last_seen_at,
                    d.truck_id,
                    t.reg_no AS truck_reg, t.make, t.model, t.status AS truck_status,
                    p.c0d               AS position_id,
                    p.ts, p.lat, p.lng, p.speed_kph, p.heading_deg, p.ignition,
                    TIMESTAMPDIFF(SECOND, p.ts, NOW()) AS seconds_ago
               FROM gps_devices d
          LEFT JOIN gps_positions p ON p.c0d = d.last_position_id
          LEFT JOIN trucks t         ON t.c0d = d.truck_id
              WHERE d.deleted = 0
                AND d.tenant_id = :t
                AND d.status = 'active'
                AND p.lat IS NOT NULL
           ORDER BY p.ts DESC",
            [':t' => $this->tenantId]
        ) ?: [];

        // Cast numeric fields so Leaflet.js can consume the JSON as-is.
        foreach ($rows as &$r) {
            $r['lat']         = (float)$r['lat'];
            $r['lng']         = (float)$r['lng'];
            $r['speed_kph']   = $r['speed_kph'] !== null ? (float)$r['speed_kph'] : null;
            $r['heading_deg'] = $r['heading_deg'] !== null ? (float)$r['heading_deg'] : null;
            $r['seconds_ago'] = (int)$r['seconds_ago'];
            $r['stale']       = $r['seconds_ago'] > 900;   // >15 min = stale
        }
        return $rows;
    }

    /* ══════════════════════════════════════════════════════════════════
       Truck detail: last-24h track + last-position + recent events.
       Sampled if the point count would blow past $maxPoints.
       ══════════════════════════════════════════════════════════════════ */
    public function truckTrack(int $truckId, int $hours = 24, int $maxPoints = 500): array
    {
        $db = new DB();
        $windowStart = date('Y-m-d H:i:s', time() - $hours * 3600);

        $rs = $db->mySelect(
            "SELECT COUNT(*) AS n FROM gps_positions
              WHERE truck_id = :t AND ts >= :from AND valid = 1",
            [':t' => $truckId, ':from' => $windowStart]
        );
        $total = (int)($rs[0]['n'] ?? 0);
        $step  = $total > $maxPoints ? (int)ceil($total / $maxPoints) : 1;

        $pos = $db->mySelect(
            "SELECT c0d, ts, lat, lng, speed_kph, heading_deg, ignition
               FROM gps_positions
              WHERE truck_id = :t AND ts >= :from AND valid = 1
           ORDER BY ts",
            [':t' => $truckId, ':from' => $windowStart]
        ) ?: [];

        // Sample down to $maxPoints, always keeping the first + last point
        // so the ends of the polyline are anchored where they belong.
        $sampled = [];
        foreach ($pos as $i => $p) {
            if ($i === 0 || $i === count($pos) - 1 || $i % $step === 0) {
                $sampled[] = [
                    'ts'          => $p['ts'],
                    'lat'         => (float)$p['lat'],
                    'lng'         => (float)$p['lng'],
                    'speed_kph'   => $p['speed_kph'] !== null ? (float)$p['speed_kph'] : null,
                    'heading_deg' => $p['heading_deg'] !== null ? (float)$p['heading_deg'] : null,
                    'ignition'    => $p['ignition'] !== null ? (bool)$p['ignition'] : null,
                ];
            }
        }

        $events = $db->mySelect(
            "SELECT c0d, ts, kind, severity, lat, lng, speed_kph, payload_json
               FROM gps_events
              WHERE truck_id = :t AND ts >= :from
           ORDER BY ts DESC
              LIMIT 50",
            [':t' => $truckId, ':from' => $windowStart]
        ) ?: [];
        foreach ($events as &$e) {
            $e['lat']       = $e['lat'] !== null ? (float)$e['lat'] : null;
            $e['lng']       = $e['lng'] !== null ? (float)$e['lng'] : null;
            $e['speed_kph'] = $e['speed_kph'] !== null ? (float)$e['speed_kph'] : null;
        }

        // Distance covered (rough: sum of haversine hops on the sampled track)
        $km = 0.0;
        for ($i = 1; $i < count($sampled); $i++) {
            $km += $this->haversineKm(
                $sampled[$i - 1]['lat'], $sampled[$i - 1]['lng'],
                $sampled[$i]['lat'],     $sampled[$i]['lng']
            );
        }
        $topSpeed = 0.0;
        $avgSpeed = null;
        $moving = array_filter($sampled, fn($p) => $p['speed_kph'] !== null && $p['speed_kph'] > 3);
        if (!empty($moving)) {
            $topSpeed = max(array_column($moving, 'speed_kph'));
            $avgSpeed = array_sum(array_column($moving, 'speed_kph')) / count($moving);
        }

        return [
            'truck_id'        => $truckId,
            'window_hours'    => $hours,
            'window_start'    => $windowStart,
            'total_points'    => $total,
            'returned_points' => count($sampled),
            'sample_step'     => $step,
            'points'          => $sampled,
            'events'          => $events,
            'distance_km'     => round($km, 2),
            'top_speed_kph'   => round((float)$topSpeed, 1),
            'avg_speed_kph'   => $avgSpeed !== null ? round((float)$avgSpeed, 1) : null,
        ];
    }

    /* ══════════════════════════════════════════════════════════════════
       Trip playback: every point for one trip, no sampling. Also loads
       trip metadata so the view can header it.
       ══════════════════════════════════════════════════════════════════ */
    public function tripPoints(int $tripId): array
    {
        $db = new DB();
        $trip = $db->mySelect(
            "SELECT tr.c0d, tr.trip_no, tr.truck_id, tr.driver_id, tr.status,
                    tr.started_at, tr.ended_at, tr.origin, tr.destination,
                    tk.reg_no AS truck_reg, dv.full_name AS driver_name
               FROM trips tr
          LEFT JOIN trucks  tk ON tk.c0d = tr.truck_id
          LEFT JOIN drivers dv ON dv.c0d = tr.driver_id
              WHERE tr.c0d = :id LIMIT 1",
            [':id' => $tripId]
        );
        if (empty($trip)) return ['trip' => null, 'points' => [], 'events' => []];
        $t = $trip[0];

        $windowStart = $t['started_at'] ?: date('Y-m-d 00:00:00');
        $windowEnd   = $t['ended_at']   ?: date('Y-m-d H:i:s');

        $pos = $db->mySelect(
            "SELECT ts, lat, lng, speed_kph, heading_deg, ignition
               FROM gps_positions
              WHERE truck_id = :tk AND ts BETWEEN :from AND :to AND valid = 1
           ORDER BY ts",
            [':tk' => (int)$t['truck_id'], ':from' => $windowStart, ':to' => $windowEnd]
        ) ?: [];
        foreach ($pos as &$p) {
            $p['lat']         = (float)$p['lat'];
            $p['lng']         = (float)$p['lng'];
            $p['speed_kph']   = $p['speed_kph'] !== null ? (float)$p['speed_kph'] : null;
            $p['heading_deg'] = $p['heading_deg'] !== null ? (float)$p['heading_deg'] : null;
            $p['ignition']    = $p['ignition']    !== null ? (bool)$p['ignition']    : null;
        }

        $events = $db->mySelect(
            "SELECT ts, kind, severity, lat, lng, payload_json
               FROM gps_events
              WHERE trip_id = :tid OR (truck_id = :tk AND ts BETWEEN :from AND :to)
           ORDER BY ts",
            [':tid' => $tripId, ':tk' => (int)$t['truck_id'], ':from' => $windowStart, ':to' => $windowEnd]
        ) ?: [];
        foreach ($events as &$e) {
            $e['lat'] = $e['lat'] !== null ? (float)$e['lat'] : null;
            $e['lng'] = $e['lng'] !== null ? (float)$e['lng'] : null;
        }

        // Rough distance covered on this trip
        $km = 0.0;
        for ($i = 1; $i < count($pos); $i++) {
            $km += $this->haversineKm(
                $pos[$i - 1]['lat'], $pos[$i - 1]['lng'],
                $pos[$i]['lat'],     $pos[$i]['lng']
            );
        }

        return [
            'trip'        => $t,
            'points'      => $pos,
            'events'      => $events,
            'distance_km' => round($km, 2),
        ];
    }

    /* ══════════════════════════════════════════════════════════════════
       Alerts feed — recent gps_events with optional filters.
       ══════════════════════════════════════════════════════════════════ */
    public function alerts(array $filters = [], int $limit = 200): array
    {
        $db = new DB();
        $where  = [];
        $params = [];

        if (!empty($filters['truck_id']))  { $where[] = 'e.truck_id = :tk'; $params[':tk'] = (int)$filters['truck_id']; }
        if (!empty($filters['device_id'])) { $where[] = 'e.device_id = :dv'; $params[':dv'] = (int)$filters['device_id']; }
        if (!empty($filters['kind']))      { $where[] = 'e.kind     = :k';  $params[':k']  = trim((string)$filters['kind']); }
        if (!empty($filters['severity']))  { $where[] = 'e.severity = :s';  $params[':s']  = trim((string)$filters['severity']); }
        if (!empty($filters['from']))      { $where[] = 'e.ts       >= :from'; $params[':from'] = $filters['from']; }
        if (!empty($filters['to']))        { $where[] = 'e.ts       <= :to';   $params[':to']   = $filters['to']; }

        $whereSql = empty($where) ? '1=1' : implode(' AND ', $where);

        return $db->mySelect(
            "SELECT e.c0d, e.ts, e.kind, e.severity, e.lat, e.lng, e.speed_kph,
                    e.payload_json, e.truck_id, e.trip_id,
                    t.reg_no AS truck_reg, d.device_code
               FROM gps_events e
          LEFT JOIN trucks       t ON t.c0d = e.truck_id
          LEFT JOIN gps_devices  d ON d.c0d = e.device_id
              WHERE $whereSql
           ORDER BY e.ts DESC
              LIMIT $limit",
            $params
        ) ?: [];
    }

    /** Fleet-wide summary tiles for the dashboard header. */
    public function fleetSummary(): array
    {
        $db = new DB();
        $rs = $db->mySelect(
            "SELECT
                (SELECT COUNT(*) FROM gps_devices WHERE deleted = 0 AND status = 'active') AS active_devices,
                (SELECT COUNT(*) FROM trucks WHERE deleted = 0 AND status IN ('available','on_trip')) AS active_trucks,
                (SELECT COUNT(*) FROM gps_devices d
                  WHERE d.deleted = 0 AND d.status = 'active'
                    AND (d.last_seen_at IS NULL OR d.last_seen_at < NOW() - INTERVAL 15 MINUTE)) AS stale_devices,
                (SELECT COUNT(*) FROM gps_events
                  WHERE ts >= NOW() - INTERVAL 24 HOUR
                    AND severity IN ('warn','crit','critical')) AS alerts_24h",
            []
        );
        return $rs[0] ?? ['active_devices' => 0, 'active_trucks' => 0, 'stale_devices' => 0, 'alerts_24h' => 0];
    }

    /* ── helpers ───────────────────────────────────────────────────── */

    /** Great-circle distance in km (spherical approximation, R = 6371 km). */
    private function haversineKm(float $lat1, float $lon1, float $lat2, float $lon2): float
    {
        $R  = 6371.0;
        $dLat = deg2rad($lat2 - $lat1);
        $dLon = deg2rad($lon2 - $lon1);
        $a = sin($dLat / 2) ** 2 + cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * sin($dLon / 2) ** 2;
        return $R * 2 * atan2(sqrt($a), sqrt(1 - $a));
    }

    /** Active geofences (for map overlays). Returned as an array Leaflet can consume. */
    public function geofences(): array
    {
        $db = new DB();
        $rows = $db->mySelect(
            "SELECT c0d, name, kind, center_lat, center_lng, radius_m, color, trigger_alert, geometry_json
               FROM geofences
              WHERE deleted = 0 AND active = 1 AND tenant_id = :t",
            [':t' => $this->tenantId]
        ) ?: [];
        foreach ($rows as &$g) {
            $g['center_lat'] = $g['center_lat'] !== null ? (float)$g['center_lat'] : null;
            $g['center_lng'] = $g['center_lng'] !== null ? (float)$g['center_lng'] : null;
            $g['radius_m']   = $g['radius_m']   !== null ? (int)$g['radius_m']     : null;
        }
        return $rows;
    }
}
