<?php
/**
 * GpsIngestController — public webhook receiver for GPS positions.
 *
 * Auth: `?token=` matches config's GPS_INGEST_TOKEN. Bearer header also accepted.
 * NO session, NO CSRF (Router lets this through because it's in the public
 * controller list — see Router::isPublic()). Configure the config constant
 * before enabling in production.
 *
 * Accepts three body shapes:
 *   1. Traccar-forward JSON:  {"device":{...}, "position":{...}}
 *   2. Osmand-style (single position, common with mobile apps):
 *      {"deviceId":"...", "lat":..., "lon":..., "timestamp":...}
 *   3. Batch:                  {"positions": [ {...}, {...} ]}
 *
 * Response is always JSON so the sender can retry cleanly:
 *   { "ok": true, "ingested": 3 }  or  { "ok": false, "msg": "..." }
 */
class GpsIngestController extends Controller
{
    private const CONFIG_KEY = 'GPS_INGEST_TOKEN';

    public function webhookAction(): void
    {
        header('Content-Type: application/json');

        // Auth
        $token = $_GET['token'] ?? $this->bearerToken();
        $expected = defined(self::CONFIG_KEY) ? constant(self::CONFIG_KEY) : '';
        if (!$expected) {
            http_response_code(503);
            echo json_encode(['ok'=>false,'msg'=>'Ingestion not configured (missing GPS_INGEST_TOKEN).']);
            return;
        }
        if (!$token || !hash_equals($expected, (string)$token)) {
            http_response_code(401);
            echo json_encode(['ok'=>false,'msg'=>'Unauthorized.']);
            return;
        }

        // Body
        $raw = file_get_contents('php://input');
        $body = json_decode((string)$raw, true);
        if (!is_array($body)) {
            http_response_code(400);
            echo json_encode(['ok'=>false,'msg'=>'Body must be JSON.']);
            return;
        }

        // Normalise to a list of positions
        $positions = [];
        if (isset($body['positions']) && is_array($body['positions'])) {
            $positions = $body['positions'];
        } elseif (isset($body['position']) && is_array($body['position'])) {
            // Traccar forward: device is in body.device, position in body.position
            $p = $body['position'];
            $deviceCode = $body['device']['uniqueId'] ?? $body['device']['name'] ?? null;
            if ($deviceCode) $p['device_code'] = $deviceCode;
            $positions[] = $p;
        } else {
            // Assume a single position at root
            $positions[] = $body;
        }

        $ingested = 0;
        $skipped  = 0;
        $errors   = [];
        $pos      = new GpsPositions();
        $devModel = new GpsDevices();

        foreach ($positions as $p) {
            $mapped = $this->mapIncoming($p);
            if (!$mapped) { $skipped++; continue; }

            // Resolve device by code if device_id not directly present
            if (empty($mapped['device_id'])) {
                $code = $mapped['device_code'] ?? '';
                if ($code === '') { $skipped++; continue; }
                $dev = $devModel->byCodeForIngestion($code);
                if (!$dev) {
                    // Auto-register unknown devices as inactive so admin can adopt them
                    (new DB())->insertUpdate(
                        "INSERT INTO gps_devices (tenant_id, device_code, device_type, protocol, status, notes, created_on)
                         VALUES (1, :code, 'unknown', 'traccar', 'inactive', 'Auto-registered from ingestion — assign a truck to activate.', NOW())",
                        [':code' => strtoupper($code)]
                    );
                    $dev = $devModel->byCodeForIngestion($code);
                    if (!$dev) { $skipped++; continue; }
                }
                $mapped['device_id'] = (int)$dev['device_id'];
            }

            $r = $pos->ingest($mapped);
            if ($r['ok']) $ingested++;
            else { $skipped++; $errors[] = $r['msg'] ?? '?'; }
        }

        echo json_encode(['ok'=>true, 'ingested'=>$ingested, 'skipped'=>$skipped, 'errors'=>$errors]);
    }

    /**
     * Map incoming heterogeneous payloads to the GpsPositions::ingest() shape.
     * Handles Traccar, Osmand, and generic JSON.
     */
    private function mapIncoming(array $p): ?array
    {
        // Coordinate: lat/lng vs latitude/longitude vs lat/lon
        $lat = $p['lat'] ?? $p['latitude']  ?? null;
        $lng = $p['lng'] ?? $p['longitude'] ?? $p['lon'] ?? null;
        if (!is_numeric($lat) || !is_numeric($lng)) return null;

        // Time: Traccar sends ISO strings; Osmand sends unix seconds; some send fixTime.
        $ts = $p['ts'] ?? $p['fixTime'] ?? $p['deviceTime'] ?? $p['timestamp'] ?? null;
        if (is_numeric($ts)) $ts = date('Y-m-d H:i:s', (int)$ts);
        elseif (is_string($ts)) $ts = date('Y-m-d H:i:s', strtotime($ts) ?: time());
        else $ts = date('Y-m-d H:i:s');

        // Speed is sometimes in knots (Traccar). If we see the flag or the value looks like knots, convert.
        $speed = $p['speed_kph'] ?? $p['speed'] ?? null;
        if (is_numeric($speed) && isset($p['speedUnit']) && $p['speedUnit'] === 'knots') {
            $speed = (float)$speed * 1.852;
        }

        // Attributes may be nested (Traccar) or inline
        $attrs   = $p['attributes'] ?? [];
        $ignition = $p['ignition'] ?? ($attrs['ignition'] ?? null);
        $odo      = $p['odometer_can_km'] ?? $p['odometer'] ?? ($attrs['odometer'] ?? null);
        // Traccar's odometer is metres by default; if > 1e6 assume metres
        if (is_numeric($odo) && $odo > 1_000_000) $odo = (int)($odo / 1000);
        $fuel     = $p['fuel_level_l'] ?? $p['fuel'] ?? ($attrs['fuel'] ?? null);

        return [
            'device_code'     => $p['device_code'] ?? $p['deviceId'] ?? $p['unique_id'] ?? null,
            'device_id'       => isset($p['device_id']) ? (int)$p['device_id'] : null,
            'ts'              => $ts,
            'lat'             => (float)$lat,
            'lng'             => (float)$lng,
            'altitude_m'      => $p['altitude_m'] ?? $p['altitude'] ?? null,
            'speed_kph'       => is_numeric($speed) ? (float)$speed : null,
            'heading_deg'     => $p['heading_deg'] ?? $p['course']  ?? null,
            'valid'           => $p['valid'] ?? 1,
            'ignition'        => is_null($ignition) ? null : (int)!!$ignition,
            'odometer_can_km' => is_numeric($odo)  ? (int)$odo    : null,
            'fuel_level_l'    => is_numeric($fuel) ? (float)$fuel : null,
            'engine_rpm'      => isset($attrs['rpm'])         ? (int)$attrs['rpm']         : null,
            'engine_temp_c'   => isset($attrs['engineTemp'])  ? (float)$attrs['engineTemp']: null,
            'battery_v'       => isset($attrs['battery'])     ? (float)$attrs['battery']   : null,
            'attrs_json'      => !empty($attrs) ? $attrs : null,
        ];
    }

    private function bearerToken(): ?string
    {
        $h = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
        if (stripos($h, 'Bearer ') === 0) return trim(substr($h, 7));
        return null;
    }
}
