<?php
/**
 * GpsPositions — write path called by the ingestion controller for each
 * incoming position, plus a couple of read helpers used by the web UI.
 *
 * Every insert also:
 *   - refreshes gps_devices.last_seen_at + last_position_id (cache)
 *   - flips gps_devices.status from 'offline' back to 'active'
 *   - emits derived gps_events (over_speed / harsh_brake / ignition_on|off /
 *     fuel_drop)
 *   - matches active geofences → emits geofence_enter / geofence_exit events
 *   - evaluates sync alert rules and creates alerts + queues SMS/email
 */
class GpsPositions extends Model
{
    private const OVER_SPEED_KPH   = 80.0;   // fallback threshold if no rule configured
    private const FUEL_DROP_LITRES = 20.0;   // fallback threshold if no rule configured

    public function ingest(array $row): array
    {
        $db = new DB();

        // Look up device — plus previous ignition/fuel/speed for transition detection
        // and previous fence membership (from previous position's attrs_json.fences).
        $devRs = $db->mySelect(
            "SELECT g.c0d, g.truck_id, g.tenant_id, g.status,
                    p.ignition AS prev_ignition, p.fuel_level_l AS prev_fuel, p.speed_kph AS prev_speed,
                    p.attrs_json AS prev_attrs
               FROM gps_devices g
          LEFT JOIN gps_positions p ON p.c0d = g.last_position_id
              WHERE g.c0d = :id LIMIT 1",
            [':id' => (int)$row['device_id']]
        );
        $dev = $devRs[0] ?? null;
        if (!$dev) return ['ok' => false, 'msg' => 'Unknown device.'];

        $truckId = $dev['truck_id'] ? (int)$dev['truck_id'] : null;

        // Persist position
        $r = $db->insertUpdate(
            "INSERT INTO gps_positions
                (device_id, truck_id, ts, server_ts, lat, lng, altitude_m, speed_kph, heading_deg,
                 valid, ignition, odometer_can_km, fuel_level_l, engine_rpm, engine_temp_c, battery_v, attrs_json)
             VALUES
                (:did, :tid, :ts, NOW(), :lat, :lng, :alt, :sp, :hd,
                 :valid, :ign, :odo, :fuel, :rpm, :et, :bv, :attrs)",
            [
                ':did'   => (int)$row['device_id'],
                ':tid'   => $truckId,
                ':ts'    => $row['ts'] ?? date('Y-m-d H:i:s'),
                ':lat'   => (float)$row['lat'],
                ':lng'   => (float)$row['lng'],
                ':alt'   => isset($row['altitude_m']) ? (float)$row['altitude_m'] : null,
                ':sp'    => isset($row['speed_kph'])  ? (float)$row['speed_kph']  : null,
                ':hd'    => isset($row['heading_deg']) ? (float)$row['heading_deg'] : null,
                ':valid' => isset($row['valid']) ? (int)!!$row['valid'] : 1,
                ':ign'   => isset($row['ignition']) ? (int)!!$row['ignition'] : null,
                ':odo'   => isset($row['odometer_can_km']) ? (int)$row['odometer_can_km'] : null,
                ':fuel'  => isset($row['fuel_level_l'])    ? (float)$row['fuel_level_l']  : null,
                ':rpm'   => isset($row['engine_rpm'])      ? (int)$row['engine_rpm']      : null,
                ':et'    => isset($row['engine_temp_c'])   ? (float)$row['engine_temp_c'] : null,
                ':bv'    => isset($row['battery_v'])       ? (float)$row['battery_v']     : null,
                ':attrs' => isset($row['attrs_json']) ? (is_string($row['attrs_json']) ? $row['attrs_json'] : json_encode($row['attrs_json'])) : null,
            ]
        );
        $posId = (int)$r['last_id'];

        // Refresh device cache
        $db->insertUpdate(
            "UPDATE gps_devices SET last_seen_at = NOW(), last_position_id = :pid,
                                    status = CASE WHEN status = 'offline' THEN 'active' ELSE status END
              WHERE c0d = :id",
            [':pid' => $posId, ':id' => (int)$row['device_id']]
        );

        // Bump the truck's current_odometer if CAN odometer was provided
        if ($truckId && isset($row['odometer_can_km']) && (int)$row['odometer_can_km'] > 0) {
            $db->insertUpdate(
                "UPDATE trucks SET current_odometer = :odo, edited_by = 0, edited_on = NOW()
                  WHERE c0d = :tid AND current_odometer < :odo",
                [':odo' => (int)$row['odometer_can_km'], ':tid' => $truckId]
            );
        }

        // ── Derived gps_events ─────────────────────────────────────────
        $events = [];
        if (isset($row['speed_kph']) && (float)$row['speed_kph'] > self::OVER_SPEED_KPH) {
            $events[] = ['kind'=>'over_speed', 'severity'=>'medium', 'speed'=>(float)$row['speed_kph']];
        }
        if (isset($row['ignition']) && $dev['prev_ignition'] !== null && (int)$row['ignition'] !== (int)$dev['prev_ignition']) {
            $events[] = ['kind'=> (int)$row['ignition'] ? 'ignition_on' : 'ignition_off', 'severity'=>'info'];
        }
        if (isset($row['fuel_level_l']) && $dev['prev_fuel'] !== null) {
            $drop = (float)$dev['prev_fuel'] - (float)$row['fuel_level_l'];
            if ($drop >= self::FUEL_DROP_LITRES) {
                $events[] = ['kind'=>'fuel_drop', 'severity'=>'high', 'delta_l'=>-$drop];
            }
        }

        // ── Geofence enter/exit ────────────────────────────────────────
        // Previous fence membership was stashed in prev_attrs.fences (array of ids).
        $prevFenceIds = [];
        if (!empty($dev['prev_attrs'])) {
            $prev = is_string($dev['prev_attrs']) ? (json_decode($dev['prev_attrs'], true) ?: []) : (array)$dev['prev_attrs'];
            if (!empty($prev['fences']) && is_array($prev['fences'])) $prevFenceIds = array_map('intval', $prev['fences']);
        }
        $inside = (new Geofences())->matchPoint((float)$row['lat'], (float)$row['lng']);
        $curFenceIds = array_map(fn($f) => (int)$f['c0d'], $inside);

        $entered = array_diff($curFenceIds, $prevFenceIds);
        $exited  = array_diff($prevFenceIds, $curFenceIds);
        $fenceMap = []; foreach ($inside as $f) $fenceMap[(int)$f['c0d']] = $f;

        // Stash current membership on this position so the next one can compare.
        // We embed it into attrs_json in-place (idempotent).
        if (!empty($curFenceIds)) {
            $existingAttrs = [];
            if (isset($row['attrs_json'])) {
                $existingAttrs = is_string($row['attrs_json']) ? (json_decode($row['attrs_json'], true) ?: []) : (array)$row['attrs_json'];
            }
            $existingAttrs['fences'] = $curFenceIds;
            $db->insertUpdate("UPDATE gps_positions SET attrs_json = :a WHERE c0d = :id",
                [':a' => json_encode($existingAttrs), ':id' => $posId]);
        }

        // Emit gps_events for enter/exit
        foreach ($entered as $fid) $events[] = ['kind'=>'geofence_enter', 'severity'=>'info', 'geofence_id'=>$fid, 'geofence_name'=>$fenceMap[$fid]['name'] ?? '?'];
        foreach ($exited  as $fid) $events[] = ['kind'=>'geofence_exit',  'severity'=>'info', 'geofence_id'=>$fid];

        foreach ($events as $ev) {
            $db->insertUpdate(
                "INSERT INTO gps_events (device_id, truck_id, ts, kind, severity, lat, lng, speed_kph, payload_json, position_id, created_on)
                 VALUES (:did, :tid, :ts, :k, :sv, :lat, :lng, :sp, :pl, :pid, NOW())",
                [
                    ':did'=>(int)$row['device_id'], ':tid'=>$truckId, ':ts'=>$row['ts'] ?? date('Y-m-d H:i:s'),
                    ':k'=>$ev['kind'], ':sv'=>$ev['severity'],
                    ':lat'=>(float)$row['lat'], ':lng'=>(float)$row['lng'],
                    ':sp'=>isset($row['speed_kph']) ? (float)$row['speed_kph'] : null,
                    ':pl'=>json_encode($ev), ':pid'=>$posId,
                ]
            );
        }

        // ── Sync alert rule evaluation ─────────────────────────────────
        $this->evaluateSyncRules($row, $dev, $truckId, $posId, $events, $entered, $exited, $fenceMap);

        return ['ok' => true, 'position_id' => $posId, 'events' => count($events)];
    }

    /**
     * Evaluate the sync rules on this position. Skipped kinds (async):
     *   device_offline (handled by cron sweep)
     */
    private function evaluateSyncRules(array $row, array $dev, ?int $truckId, int $posId, array $events, array $entered, array $exited, array $fenceMap): void
    {
        $rules  = new AlertRules();
        $alerts = new Alerts();

        $ctxBase = [
            'device_id' => (int)$dev['c0d'],
            'truck_id'  => $truckId,
            'ts'        => $row['ts'] ?? date('Y-m-d H:i:s'),
            'lat'       => (float)$row['lat'],
            'lng'       => (float)$row['lng'],
            'position_id' => $posId,
        ];

        // over_speed
        if (isset($row['speed_kph'])) {
            foreach ($rules->activeByKind('over_speed') as $rule) {
                $th = json_decode((string)$rule['threshold_json'], true) ?: [];
                $limit = (float)($th['speed_kph'] ?? 80);
                if ((float)$row['speed_kph'] > $limit) {
                    $title = "Over-speed · " . number_format((float)$row['speed_kph'], 0) . " kph";
                    $body  = "Speed {$row['speed_kph']} kph exceeds limit {$limit} kph.";
                    $id = $alerts->emit($rule, $ctxBase + ['title'=>$title, 'message'=>$body, 'payload'=>['speed'=>(float)$row['speed_kph'], 'limit'=>$limit]]);
                    if ($id) Alerts::queueNotifications($rule, $id, $title, $body);
                }
            }
        }

        // fuel_drop — reuses the derived event; rule threshold overrides constant
        if (isset($row['fuel_level_l']) && $dev['prev_fuel'] !== null) {
            $drop = (float)$dev['prev_fuel'] - (float)$row['fuel_level_l'];
            if ($drop > 0) {
                foreach ($rules->activeByKind('fuel_drop') as $rule) {
                    $th = json_decode((string)$rule['threshold_json'], true) ?: [];
                    $limit = (float)($th['drop_l'] ?? 20);
                    if ($drop >= $limit) {
                        $title = "Fuel drop · " . number_format($drop, 1) . " L";
                        $body  = "Fuel level fell {$drop} L (from " . number_format((float)$dev['prev_fuel'], 1) . " to " . number_format((float)$row['fuel_level_l'], 1) . " L). Possible siphon.";
                        $id = $alerts->emit($rule, $ctxBase + ['title'=>$title, 'message'=>$body, 'payload'=>['drop_l'=>$drop, 'limit'=>$limit]]);
                        if ($id) Alerts::queueNotifications($rule, $id, $title, $body);
                    }
                }
            }
        }

        // ignition_after_hours
        if (isset($row['ignition']) && (int)$row['ignition'] === 1 && $dev['prev_ignition'] !== null && (int)$dev['prev_ignition'] === 0) {
            $hour = (int)date('G', strtotime($ctxBase['ts']));
            foreach ($rules->activeByKind('ignition_after_hours') as $rule) {
                $th   = json_decode((string)$rule['threshold_json'], true) ?: [];
                $from = (int)($th['hour_from'] ?? 20);
                $to   = (int)($th['hour_to']   ?? 5);
                $inWindow = $from <= $to ? ($hour >= $from && $hour < $to) : ($hour >= $from || $hour < $to);
                if ($inWindow) {
                    $title = "After-hours ignition at " . date('H:i', strtotime($ctxBase['ts']));
                    $body  = "Truck engine started outside operating hours ({$from}:00 – {$to}:00).";
                    $id = $alerts->emit($rule, $ctxBase + ['title'=>$title, 'message'=>$body, 'payload'=>['hour'=>$hour]]);
                    if ($id) Alerts::queueNotifications($rule, $id, $title, $body);
                }
            }
        }

        // geofence_exit
        foreach ($exited as $fid) {
            foreach ($rules->activeByKind('geofence_exit') as $rule) {
                $scope = json_decode((string)($rule['scope_geofence_ids'] ?? ''), true) ?: [];
                if (!empty($scope) && !in_array($fid, array_map('intval', $scope), true)) continue;
                $title = "Geofence exit · " . ($fenceMap[$fid]['name'] ?? "fence #{$fid}");
                $body  = "Vehicle exited the geofence.";
                $id = $alerts->emit($rule, $ctxBase + ['geofence_id'=>$fid, 'title'=>$title, 'message'=>$body]);
                if ($id) Alerts::queueNotifications($rule, $id, $title, $body);
            }
        }
        // geofence_enter (mirror)
        foreach ($entered as $fid) {
            foreach ($rules->activeByKind('geofence_enter') as $rule) {
                $scope = json_decode((string)($rule['scope_geofence_ids'] ?? ''), true) ?: [];
                if (!empty($scope) && !in_array($fid, array_map('intval', $scope), true)) continue;
                $title = "Geofence enter · " . ($fenceMap[$fid]['name'] ?? "fence #{$fid}");
                $body  = "Vehicle entered the geofence.";
                $id = $alerts->emit($rule, $ctxBase + ['geofence_id'=>$fid, 'title'=>$title, 'message'=>$body]);
                if ($id) Alerts::queueNotifications($rule, $id, $title, $body);
            }
        }
    }
}
