<?php
declare(strict_types=1);

/**
 * SCLS Audit Log Ingest — POST /api/audit/log.php
 *
 * Node-facing API: receives encrypted audit entries from fleet nodes.
 * Auth: HMAC signature verification (NOT dashboard session auth).
 * Writes directly to engine.sec_audit_log via PDO.
 */

// No DASHBOARD_ROOT/bootstrap — this is a node-facing API
ini_set('display_errors', '0');
error_reporting(0);
header('Content-Type: application/json');

// ── Only accept POST ──────────────────────────────────────────────────────
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    http_response_code(405);
    exit(json_encode(['status' => 'error', 'error' => 'Method not allowed']));
}

// ── Parse headers ─────────────────────────────────────────────────────────
$ts    = $_SERVER['HTTP_X_ENGINE_TS']    ?? '';
$nonce = $_SERVER['HTTP_X_ENGINE_NONCE'] ?? '';
$sig   = $_SERVER['HTTP_X_ENGINE_SIG']   ?? '';
$node  = $_SERVER['HTTP_X_ENGINE_NODE']  ?? '';

if (!$ts || !$nonce || !$sig || !$node) {
    http_response_code(400);
    exit(json_encode(['status' => 'error', 'error' => 'Missing required headers']));
}

// ── Validate node ID format ───────────────────────────────────────────────
if (!preg_match('/^\d{6}$/', $node)) {
    http_response_code(400);
    exit(json_encode(['status' => 'error', 'error' => 'Invalid node ID format']));
}

// ── Timestamp drift check (5 minutes) ─────────────────────────────────────
if (abs(time() - (int)$ts) > 300) {
    http_response_code(401);
    exit(json_encode(['status' => 'error', 'error' => 'Timestamp drift exceeded']));
}

// ── Nonce format check ────────────────────────────────────────────────────
if (!preg_match('/^[a-f0-9]{16,64}$/', $nonce)) {
    http_response_code(400);
    exit(json_encode(['status' => 'error', 'error' => 'Invalid nonce format']));
}

// ── Read body ─────────────────────────────────────────────────────────────
$rawBody = file_get_contents('php://input');
if (!$rawBody || strlen($rawBody) > 2 * 1024 * 1024) {
    http_response_code(400);
    exit(json_encode(['status' => 'error', 'error' => 'Invalid or oversized request body']));
}

$data = json_decode($rawBody, true);
if (json_last_error() !== JSON_ERROR_NONE || !is_array($data)) {
    http_response_code(400);
    exit(json_encode(['status' => 'error', 'error' => 'Invalid JSON body']));
}

// ── Load node key ─────────────────────────────────────────────────────────
$keyFile = '/opt/sysc/engine/vault/node_keys/' . $node . '.key';
if (!file_exists($keyFile) || !is_readable($keyFile)) {
    http_response_code(403);
    exit(json_encode(['status' => 'error', 'error' => 'Node not authorized']));
}

$nodeKey = trim(file_get_contents($keyFile));
if (strlen($nodeKey) !== 64 || !ctype_xdigit($nodeKey)) {
    http_response_code(500);
    exit(json_encode(['status' => 'error', 'error' => 'Invalid node key']));
}

// ── HMAC verification ─────────────────────────────────────────────────────
// Canonical format: ts.nonce.METHOD.path.body (dots, not colons)
$canonicalPath = '/api/audit/log';
$payload = $ts . '.' . $nonce . '.POST.' . $canonicalPath . '.' . $rawBody;
$expected = hash_hmac('sha256', $payload, hex2bin($nodeKey));

if (!hash_equals($expected, $sig)) {
    http_response_code(401);
    exit(json_encode(['status' => 'error', 'error' => 'HMAC verification failed']));
}

// ── Rate limit: 100/min per node (file-based) ─────────────────────────────
$rateLimitDir = '/tmp/scls_rate';
if (!is_dir($rateLimitDir)) {
    @mkdir($rateLimitDir, 0700, true);
}
$rlFile = $rateLimitDir . '/node_' . $node . '.rl';
$rlData = ['window' => 0, 'count' => 0];
if (file_exists($rlFile)) {
    $rlRaw = @file_get_contents($rlFile);
    if ($rlRaw) {
        $rlData = json_decode($rlRaw, true) ?: $rlData;
    }
}

$currentMinute = intdiv(time(), 60);
if (($rlData['window'] ?? 0) !== $currentMinute) {
    $rlData = ['window' => $currentMinute, 'count' => 0];
}

if ($rlData['count'] >= 100) {
    http_response_code(429);
    exit(json_encode(['status' => 'error', 'error' => 'Rate limit exceeded (100/min)']));
}

$rlData['count']++;
@file_put_contents($rlFile, json_encode($rlData), LOCK_EX);

// ── Validate required fields ──────────────────────────────────────────────
$requiredFields = ['event_uuid', 'tool_id', 'tool_name', 'encrypted_statement',
                   'encryption_iv', 'hmac_signature', 'started_at'];

foreach ($requiredFields as $field) {
    if (empty($data[$field])) {
        http_response_code(400);
        exit(json_encode(['status' => 'error', 'error' => "Missing required field: {$field}"]));
    }
}

// Validate event_uuid format (UUID v4)
if (!preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i', $data['event_uuid'])) {
    http_response_code(400);
    exit(json_encode(['status' => 'error', 'error' => 'Invalid event_uuid format']));
}

// Validate tool_id (4 hex chars)
if (!preg_match('/^[a-f0-9]{4}$/', $data['tool_id'])) {
    http_response_code(400);
    exit(json_encode(['status' => 'error', 'error' => 'Invalid tool_id format']));
}

// Validate encryption_iv (32 hex chars = 16 bytes)
if (!preg_match('/^[a-f0-9]{32}$/', $data['encryption_iv'])) {
    http_response_code(400);
    exit(json_encode(['status' => 'error', 'error' => 'Invalid encryption_iv format']));
}

// Validate hmac_signature (64 hex chars)
if (!preg_match('/^[a-f0-9]{64}$/', $data['hmac_signature'])) {
    http_response_code(400);
    exit(json_encode(['status' => 'error', 'error' => 'Invalid hmac_signature format']));
}

// Validate status enum
$validStatuses = ['pending', 'success', 'failed', 'timeout', 'killed'];
$status = $data['status'] ?? 'pending';
if (!in_array($status, $validStatuses, true)) {
    $status = 'pending';
}

// Validate severity enum
$validSeverities = ['info', 'low', 'medium', 'high', 'critical'];
$severity = $data['severity'] ?? 'info';
if (!in_array($severity, $validSeverities, true)) {
    $severity = 'info';
}

// ── Connect to Engine DB ──────────────────────────────────────────────────
try {
    // Load credentials from env file
    $dbHost = '127.0.0.1';
    $dbPort = 3306;
    $dbName = 'engine';
    $dbUser = 'engine_write';
    $dbPass = '';

    $envFiles = [
        '/opt/apache/engine/vault/encrypted/engine_db.env',
        '/var/www/private/engine_db.env',
    ];

    foreach ($envFiles as $envFile) {
        if (file_exists($envFile)) {
            $lines = file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
            foreach ($lines as $line) {
                if (str_starts_with(trim($line), '#')) continue;
                [$k, $v] = explode('=', $line, 2) + ['', ''];
                $k = trim($k);
                $v = trim($v, " \t\n\r\"'");
                match ($k) {
                    'ENGINE_DB_HOST' => $dbHost = $v,
                    'ENGINE_DB_PORT' => $dbPort = (int)$v,
                    'ENGINE_DB_USER' => $dbUser = $v,
                    'ENGINE_DB_PASS' => $dbPass = $v,
                    'ENGINE_DB_NAME' => $dbName = $v,
                    default          => null,
                };
            }
            break;
        }
    }

    $dsn = "mysql:host={$dbHost};port={$dbPort};dbname={$dbName};charset=utf8mb4";
    $pdo = new PDO($dsn, $dbUser, $dbPass, [
        PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        PDO::ATTR_EMULATE_PREPARES   => false,
        PDO::ATTR_TIMEOUT            => 10,
    ]);
} catch (\Throwable $e) {
    http_response_code(500);
    exit(json_encode(['status' => 'error', 'error' => 'Database connection failed']));
}

// ── Encode source IP to binary ────────────────────────────────────────────
$sourceIpBin = null;
if (!empty($data['source_ip'])) {
    $packed = @inet_pton($data['source_ip']);
    if ($packed !== false) {
        $sourceIpBin = $packed;
    }
}

// ── INSERT ────────────────────────────────────────────────────────────────
try {
    $stmt = $pdo->prepare(
        'INSERT INTO sec_audit_log (
            event_uuid, node_id, tool_id, tool_name, tool_category,
            encrypted_statement, encryption_iv, encryption_version, hmac_signature,
            session_id, admin_id, trust_score, exit_code, duration_ms,
            cpu_percent, memory_mb, source_ip, network_zone,
            status, severity, flags, started_at, completed_at
        ) VALUES (
            ?, ?, ?, ?, ?,
            ?, ?, ?, ?,
            ?, ?, ?, ?, ?,
            ?, ?, ?, ?,
            ?, ?, ?, ?, ?
        )'
    );

    $stmt->execute([
        $data['event_uuid'],
        $node,
        $data['tool_id'],
        substr($data['tool_name'], 0, 50),
        substr($data['tool_category'] ?? 'general', 0, 20),
        $data['encrypted_statement'],
        $data['encryption_iv'],
        (int)($data['encryption_version'] ?? 1),
        $data['hmac_signature'],
        isset($data['session_id']) ? substr($data['session_id'], 0, 50) : null,
        isset($data['admin_id'])   ? substr($data['admin_id'], 0, 50) : null,
        isset($data['trust_score']) ? round((float)$data['trust_score'], 2) : null,
        isset($data['exit_code'])   ? (int)$data['exit_code'] : null,
        isset($data['duration_ms']) ? (int)$data['duration_ms'] : null,
        isset($data['cpu_percent']) ? round((float)$data['cpu_percent'], 2) : null,
        isset($data['memory_mb'])   ? round((float)$data['memory_mb'], 2) : null,
        $sourceIpBin,
        isset($data['network_zone']) ? substr($data['network_zone'], 0, 20) : null,
        $status,
        $severity,
        (int)($data['flags'] ?? 0),
        $data['started_at'],
        $data['completed_at'] ?? null,
    ]);

    $insertId = $pdo->lastInsertId();

    http_response_code(201);
    echo json_encode([
        'status'     => 'ok',
        'id'         => (int)$insertId,
        'event_uuid' => $data['event_uuid'],
        'ts'         => date('c'),
    ]);
} catch (\Throwable $e) {
    http_response_code(500);
    echo json_encode(['status' => 'error', 'error' => 'Insert failed']);
}
