<?php
declare(strict_types=1);

require __DIR__ . '/common.php';
define('GOBAO_TOOLS_LIBRARY_ONLY', true);
require_once __DIR__ . '/tools.php';

$env = gobao_load_env();
$method = $_SERVER['REQUEST_METHOD'];

function gobao_companion_user(array $input = []): string {
    return gobao_safe_session_id((string)($input['userId'] ?? $_GET['userId'] ?? 'main'));
}

function gobao_companion_device(array $input = []): string {
    $raw = trim((string)($input['deviceId'] ?? $_GET['deviceId'] ?? 'default-device'));
    $raw = preg_replace('/[^a-zA-Z0-9_.@-]+/', '_', $raw) ?: 'default-device';
    return substr($raw, 0, 96);
}

function gobao_companion_vault_name(array $input = []): string {
    $raw = trim((string)($input['vaultName'] ?? $_GET['vaultName'] ?? 'LocalVault'));
    $raw = preg_replace('/[^\pL\pN_.@ -]+/u', '-', $raw) ?: 'LocalVault';
    return mb_substr(trim($raw, '- .'), 0, 90) ?: 'LocalVault';
}

function gobao_companion_register_device(string $userId, string $deviceId, array $input): array {
    $db = gobao_db();
    if (!$db) {
        gobao_fail('Database is not available: ' . gobao_db_last_error(), 500);
    }
    $now = gmdate('c');
    $existingRow = gobao_db_retry(function () use ($db, $deviceId) {
        $exists = $db->prepare('SELECT * FROM local_devices WHERE device_id = :device_id');
        $exists->execute([':device_id' => $deviceId]);
        return $exists->fetch(PDO::FETCH_ASSOC) ?: null;
    });
    if ($existingRow && (string)$existingRow['user_id'] !== $userId) {
        gobao_fail('Device ID is already bound to another user.', 409);
    }
    $incomingCaps = is_array($input['capabilities'] ?? null) ? $input['capabilities'] : [];
    $existingCaps = $existingRow ? (json_decode((string)($existingRow['capabilities_json'] ?? '{}'), true) ?: []) : [];
    $capabilities = array_replace_recursive($existingCaps, $incomingCaps);
    $payload = [
        ':device_id' => $deviceId,
        ':user_id' => $userId,
        ':label' => trim((string)($input['label'] ?? $deviceId)),
        ':root_path' => trim((string)($input['rootPath'] ?? '')),
        ':capabilities_json' => gobao_json_encode($capabilities),
        ':status' => trim((string)($input['status'] ?? 'active')) ?: 'active',
        ':last_seen_at' => $now,
        ':created_at' => $now,
        ':updated_at' => $now,
    ];
    gobao_db_retry(function () use ($db, $existingRow, $payload): void {
        if ($existingRow) {
            $stmt = $db->prepare('
                UPDATE local_devices
                SET user_id = :user_id, label = :label, root_path = :root_path, capabilities_json = :capabilities_json,
                    status = :status, last_seen_at = :last_seen_at, updated_at = :updated_at
                WHERE device_id = :device_id
            ');
            $updatePayload = $payload;
            unset($updatePayload[':created_at']);
            $stmt->execute($updatePayload);
            return;
        }
        $stmt = $db->prepare('
            INSERT INTO local_devices(device_id, user_id, label, root_path, capabilities_json, status, last_seen_at, created_at, updated_at)
            VALUES(:device_id, :user_id, :label, :root_path, :capabilities_json, :status, :last_seen_at, :created_at, :updated_at)
        ');
        $stmt->execute($payload);
    });
    if (empty($input['skipAudit']) && empty($incomingCaps['polling'])) {
        try {
            gobao_audit_record('local_companion_registered', [
                'sessionId' => $userId,
                'actor' => $deviceId,
                'status' => 'ok',
                'refType' => 'local_device',
                'refId' => $deviceId,
                'title' => 'register local companion device',
                'payload' => ['rootPath' => $payload[':root_path']],
            ]);
        } catch (Throwable $e) {
            error_log('GOBAO companion audit skipped: ' . $e->getMessage());
        }
    }
    return gobao_companion_device_status($userId, $deviceId);
}

function gobao_companion_device_status(string $userId, string $deviceId = ''): array {
    $db = gobao_db();
    if (!$db) {
        gobao_fail('Database is not available: ' . gobao_db_last_error(), 500);
    }
    $devicesStmt = $deviceId !== ''
        ? $db->prepare('SELECT * FROM local_devices WHERE user_id = :user_id AND device_id = :device_id ORDER BY updated_at DESC')
        : $db->prepare('SELECT * FROM local_devices WHERE user_id = :user_id ORDER BY updated_at DESC LIMIT 20');
    $params = [':user_id' => $userId];
    if ($deviceId !== '') {
        $params[':device_id'] = $deviceId;
    }
    [$deviceRows, $syncRows] = gobao_db_retry(function () use ($devicesStmt, $params, $db, $userId): array {
        $devicesStmt->execute($params);
        $syncStmt = $db->prepare('SELECT * FROM local_vault_syncs WHERE user_id = :user_id ORDER BY created_at DESC LIMIT 20');
        $syncStmt->execute([':user_id' => $userId]);
        return [
            $devicesStmt->fetchAll(PDO::FETCH_ASSOC) ?: [],
            $syncStmt->fetchAll(PDO::FETCH_ASSOC) ?: [],
        ];
    });
    return [
        'userId' => $userId,
        'deviceId' => $deviceId,
        'devices' => array_map(static function (array $row): array {
            return [
                'deviceId' => $row['device_id'],
                'label' => $row['label'],
                'rootPath' => $row['root_path'],
                'status' => $row['status'],
                'lastSeenAt' => $row['last_seen_at'],
                'updatedAt' => $row['updated_at'],
                'capabilities' => json_decode((string)$row['capabilities_json'], true) ?: [],
            ];
        }, $deviceRows),
        'syncs' => array_map(static function (array $row): array {
            return [
                'id' => (int)$row['id'],
                'deviceId' => $row['device_id'],
                'vaultName' => $row['vault_name'],
                'rootPath' => $row['root_path'],
                'sourceId' => $row['source_id'] !== null ? (int)$row['source_id'] : null,
                'filesCount' => (int)$row['files_count'],
                'chunksCount' => (int)$row['chunks_count'],
                'status' => $row['status'],
                'detail' => json_decode((string)$row['detail_json'], true) ?: [],
                'createdAt' => $row['created_at'],
            ];
        }, $syncRows),
        'database' => gobao_db_counts(),
        'obsidian' => gobao_obsidian_status(gobao_load_env()),
    ];
}

function gobao_companion_store_vault(string $userId, string $deviceId, array $input, array $env): array {
    $db = gobao_db();
    if (!$db) {
        gobao_fail('Database is not available: ' . gobao_db_last_error(), 500);
    }
    $files = $input['files'] ?? [];
    if (!is_array($files) || count($files) === 0) {
        gobao_fail('files array is required', 400);
    }
    $maxFiles = max(1, min(300, (int)($input['maxFiles'] ?? 120)));
    $files = array_slice($files, 0, $maxFiles);
    $vaultName = gobao_companion_vault_name($input);
    $rootPath = trim((string)($input['rootPath'] ?? ''));
    gobao_companion_register_device($userId, $deviceId, [
        'label' => $input['deviceLabel'] ?? $deviceId,
        'rootPath' => $rootPath,
        'capabilities' => ['filesystem' => true, 'obsidian' => true, 'sync' => true],
    ]);

    $repoName = preg_replace('/[^a-zA-Z0-9_.@-]+/', '-', $vaultName) ?: 'LocalVault';
    $batchIndex = max(0, (int)($input['batchIndex'] ?? 0));
    $batchTotal = max(1, (int)($input['batchTotal'] ?? 1));
    $replace = array_key_exists('replace', $input) ? !empty($input['replace']) : ($batchIndex === 0);
    $ref = $batchTotal > 1
        ? $deviceId . '#batch-' . str_pad((string)$batchIndex, 4, '0', STR_PAD_LEFT)
        : $deviceId;
    $now = gmdate('c');
    $db->beginTransaction();
    try {
        if ($replace) {
            $old = $db->prepare('
                SELECT id FROM knowledge_sources
                WHERE source_type = :source_type
                  AND repo_owner = :repo_owner
                  AND repo_name = :repo_name
                  AND (ref = :device_ref OR ref LIKE :batch_ref_prefix)
            ');
            $old->execute([
                ':source_type' => 'local_vault',
                ':repo_owner' => 'local',
                ':repo_name' => $repoName,
                ':device_ref' => $deviceId,
                ':batch_ref_prefix' => $deviceId . '#batch-%',
            ]);
            $deleteChunks = $db->prepare('DELETE FROM knowledge_chunks WHERE source_id = :source_id');
            $deleteSource = $db->prepare('DELETE FROM knowledge_sources WHERE id = :id');
            foreach ($old->fetchAll(PDO::FETCH_COLUMN) ?: [] as $oldId) {
                $oldId = (int)$oldId;
                $deleteChunks->execute([':source_id' => $oldId]);
                $deleteSource->execute([':id' => $oldId]);
            }
        }
        $sourceId = gobao_create_knowledge_source([
            'sourceType' => 'local_vault',
            'repoOwner' => 'local',
            'repoName' => $repoName,
            'repoUrl' => null,
            'ref' => $ref,
            'query' => 'local vault ' . $vaultName . ' ' . $rootPath,
            'status' => 'active',
            'meta' => [
                'userId' => $userId,
                'deviceId' => $deviceId,
                'vaultName' => $vaultName,
                'rootPath' => $rootPath,
                'syncedAt' => $now,
                'batchIndex' => $batchIndex,
                'batchTotal' => $batchTotal,
                'replace' => $replace,
            ],
        ]);
        $insert = $db->prepare('
            INSERT INTO knowledge_chunks(source_id, path, blob_sha, content_sha, chunk_index, content, meta_json, created_at)
            VALUES(:source_id, :path, :blob_sha, :content_sha, :chunk_index, :content, :meta_json, :created_at)
        ');
        $stored = [];
        $skipped = [];
        $chunksCount = 0;
        foreach ($files as $file) {
            $path = trim((string)($file['path'] ?? ''));
            $content = (string)($file['content'] ?? '');
            if ($path === '' || $content === '') {
                $skipped[] = ['path' => $path, 'reason' => 'empty path or content'];
                continue;
            }
            if (strlen($content) > 500000) {
                $content = substr($content, 0, 500000);
            }
            $chunks = gobao_chunk_text_for_knowledge($content, 3600, 240);
            $sha = (string)($file['sha256'] ?? hash('sha256', $content));
            foreach ($chunks as $i => $chunk) {
                $insert->execute([
                    ':source_id' => $sourceId,
                    ':path' => $path,
                    ':blob_sha' => 'local:' . $sha,
                    ':content_sha' => hash('sha256', $chunk),
                    ':chunk_index' => $i,
                    ':content' => $chunk,
                    ':meta_json' => gobao_json_encode([
                        'deviceId' => $deviceId,
                        'vaultName' => $vaultName,
                        'rootPath' => $rootPath,
                        'batchIndex' => $batchIndex,
                        'batchTotal' => $batchTotal,
                        'mtime' => $file['mtime'] ?? null,
                        'size' => $file['size'] ?? strlen($content),
                    ]),
                    ':created_at' => $now,
                ]);
            }
            $chunksCount += count($chunks);
            $stored[] = ['path' => $path, 'chunks' => count($chunks), 'sha256' => $sha];
        }
        $syncStmt = $db->prepare('
            INSERT INTO local_vault_syncs(device_id, user_id, vault_name, root_path, source_id, files_count, chunks_count, status, detail_json, created_at)
            VALUES(:device_id, :user_id, :vault_name, :root_path, :source_id, :files_count, :chunks_count, :status, :detail_json, :created_at)
        ');
        $syncStmt->execute([
            ':device_id' => $deviceId,
            ':user_id' => $userId,
            ':vault_name' => $vaultName,
            ':root_path' => $rootPath,
            ':source_id' => $sourceId,
            ':files_count' => count($stored),
            ':chunks_count' => $chunksCount,
            ':status' => count($stored) ? ($batchTotal > 1 ? 'batch_completed' : 'completed') : 'empty',
            ':detail_json' => gobao_json_encode([
                'batchIndex' => $batchIndex,
                'batchTotal' => $batchTotal,
                'replace' => $replace,
                'ref' => $ref,
                'stored' => $stored,
                'skipped' => $skipped,
            ]),
            ':created_at' => $now,
        ]);
        $syncId = (int)$db->lastInsertId();
        $db->commit();
    } catch (Throwable $e) {
        if ($db->inTransaction()) {
            $db->rollBack();
        }
        gobao_fail('Local vault sync failed: ' . $e->getMessage(), 500);
    }

    $obsidian = null;
    try {
        $obsidian = gobao_obsidian_write_source($sourceId, $env);
    } catch (Throwable $e) {
        $obsidian = ['ok' => false, 'error' => $e->getMessage()];
    }
    gobao_audit_record('local_vault_synced', [
        'sessionId' => $userId,
        'actor' => $deviceId,
        'status' => 'completed',
        'refType' => 'local_vault_sync',
        'refId' => (string)$syncId,
        'title' => 'sync local vault',
        'payload' => [
            'vaultName' => $vaultName,
            'sourceId' => $sourceId,
            'files' => count($stored),
            'chunks' => $chunksCount,
            'batchIndex' => $batchIndex,
            'batchTotal' => $batchTotal,
        ],
    ]);
    return [
        'ok' => true,
        'syncId' => $syncId,
        'sourceId' => $sourceId,
        'vaultName' => $vaultName,
        'deviceId' => $deviceId,
        'batchIndex' => $batchIndex,
        'batchTotal' => $batchTotal,
        'replace' => $replace,
        'ref' => $ref,
        'filesStored' => count($stored),
        'chunksStored' => $chunksCount,
        'stored' => $stored,
        'skipped' => $skipped,
        'obsidian' => $obsidian,
    ];
}

function gobao_companion_bool($value, bool $default = false): bool {
    if ($value === null) {
        return $default;
    }
    if (is_bool($value)) {
        return $value;
    }
    $text = strtolower(trim((string)$value));
    if ($text === '') {
        return $default;
    }
    return in_array($text, ['1', 'true', 'yes', 'on', 'enabled'], true);
}

function gobao_companion_token(array $input): string {
    $deviceHeader = (string)($_SERVER['HTTP_X_DEVICE_TOKEN'] ?? '');
    if ($deviceHeader !== '') {
        return trim($deviceHeader);
    }
    $header = (string)($_SERVER['HTTP_X_COMPANION_TOKEN'] ?? '');
    if ($header !== '') {
        return trim($header);
    }
    $auth = (string)($_SERVER['HTTP_AUTHORIZATION'] ?? $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ?? '');
    if (preg_match('/Bearer\s+(.+)/i', $auth, $m)) {
        return trim($m[1]);
    }
    return trim((string)($input['token'] ?? ''));
}

function gobao_companion_require_token(array $env, array $input): void {
    $secret = trim((string)($env['GOBAO_COMPANION_TOKEN'] ?? $env['GOBAO_COMPANION_SHARED_SECRET'] ?? ''));
    if ($secret === '') {
        gobao_fail('GOBAO_COMPANION_TOKEN is not configured on the server.', 403);
    }
    if (!hash_equals($secret, gobao_companion_token($input))) {
        gobao_fail('Invalid companion token.', 401);
    }
}

function gobao_companion_allow_legacy_token(array $env): bool {
    $raw = strtolower(trim((string)($env['GOBAO_ALLOW_LEGACY_COMPANION_TOKEN'] ?? 'true')));
    return !in_array($raw, ['0', 'false', 'no', 'off'], true);
}

function gobao_companion_require_device_token(string $userId, string $deviceId, array $env, array $input): void {
    $token = gobao_companion_token($input);
    if ($token === '') {
        gobao_fail('Device token is required.', 401);
    }
    $db = gobao_db();
    if (!$db) {
        gobao_fail('Database is not available.', 500);
    }
    $stmt = $db->prepare('
        SELECT device_token_hash, status, revoked_at
        FROM local_devices
        WHERE user_id = :user_id AND device_id = :device_id
        LIMIT 1
    ');
    $stmt->execute([':user_id' => $userId, ':device_id' => $deviceId]);
    $device = $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
    if ($device && (string)$device['status'] === 'active' && empty($device['revoked_at'])
        && !empty($device['device_token_hash'])
        && hash_equals((string)$device['device_token_hash'], gobao_token_hash($token))) {
        $db->prepare('UPDATE local_devices SET token_last_used_at = :now WHERE user_id = :user_id AND device_id = :device_id')
            ->execute([':now' => gmdate('c'), ':user_id' => $userId, ':device_id' => $deviceId]);
        return;
    }
    if ($userId === 'main' && gobao_companion_allow_legacy_token($env)) {
        $legacy = trim((string)($env['GOBAO_COMPANION_TOKEN'] ?? $env['GOBAO_COMPANION_SHARED_SECRET'] ?? ''));
        if ($legacy !== '' && hash_equals($legacy, $token)) {
            return;
        }
    }
    gobao_fail('Invalid, revoked, or mismatched device token.', 401);
}

function gobao_companion_pair_device(array $input): array {
    $code = strtoupper(preg_replace('/[^A-Z0-9]/', '', (string)($input['pairingCode'] ?? '')) ?: '');
    if (strlen($code) !== 10) {
        gobao_fail('Pairing code must contain 10 letters or numbers.', 422);
    }
    $deviceId = gobao_companion_device($input);
    if ($deviceId === 'default-device') {
        gobao_fail('A unique deviceId is required.', 422);
    }
    $db = gobao_db();
    if (!$db) {
        gobao_fail('Database is not available.', 500);
    }
    $db->beginTransaction();
    try {
        $stmt = $db->prepare('
            SELECT * FROM device_pairings
            WHERE code_hash = :code_hash
              AND canceled_at IS NULL
              AND expires_at > :now
            LIMIT 1
        ');
        $stmt->execute([':code_hash' => gobao_token_hash($code), ':now' => gmdate('c')]);
        $pairing = $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
        if (!$pairing) {
            $db->rollBack();
            gobao_fail('Pairing code is invalid, expired, or already used.', 401);
        }
        $claimedDeviceId = trim((string)($pairing['claimed_device_id'] ?? ''));
        if (!empty($pairing['claimed_at']) && $claimedDeviceId !== $deviceId) {
            $db->rollBack();
            gobao_fail('Pairing code was already used by another device.', 401);
        }
        $owner = $db->prepare('SELECT user_id FROM local_devices WHERE device_id = :device_id');
        $owner->execute([':device_id' => $deviceId]);
        $existingOwner = $owner->fetchColumn();
        if ($existingOwner !== false && (string)$existingOwner !== (string)$pairing['user_id']) {
            $db->rollBack();
            gobao_fail('Device ID is already bound to another user.', 409);
        }
        $deviceToken = 'gbd_' . gobao_random_token(40);
        $now = gmdate('c');
        $caps = is_array($input['capabilities'] ?? null) ? $input['capabilities'] : [];
        $upsert = $db->prepare('
            INSERT INTO local_devices(
                device_id, user_id, label, root_path, capabilities_json, status,
                last_seen_at, created_at, updated_at, device_token_hash, paired_at, revoked_at
            )
            VALUES(
                :device_id, :user_id, :label, :root_path, :capabilities_json, :status,
                :last_seen_at, :created_at, :updated_at, :device_token_hash, :paired_at, NULL
            )
            ON CONFLICT(device_id) DO UPDATE SET
                user_id = excluded.user_id,
                label = excluded.label,
                root_path = excluded.root_path,
                capabilities_json = excluded.capabilities_json,
                status = excluded.status,
                last_seen_at = excluded.last_seen_at,
                updated_at = excluded.updated_at,
                device_token_hash = excluded.device_token_hash,
                paired_at = excluded.paired_at,
                revoked_at = NULL
        ');
        $upsert->execute([
            ':device_id' => $deviceId,
            ':user_id' => (string)$pairing['user_id'],
            ':label' => mb_substr(trim((string)($input['label'] ?? $pairing['device_label'] ?? $deviceId)), 0, 100),
            ':root_path' => trim((string)($input['rootPath'] ?? '')),
            ':capabilities_json' => gobao_json_encode($caps),
            ':status' => 'active',
            ':last_seen_at' => $now,
            ':created_at' => $now,
            ':updated_at' => $now,
            ':device_token_hash' => gobao_token_hash($deviceToken),
            ':paired_at' => $now,
        ]);
        if (empty($pairing['claimed_at'])) {
            $claim = $db->prepare('
                UPDATE device_pairings
                SET claimed_at = :now, claimed_device_id = :device_id
                WHERE id = :id AND claimed_at IS NULL
            ');
            $claim->execute([':now' => $now, ':device_id' => $deviceId, ':id' => (int)$pairing['id']]);
            if ($claim->rowCount() !== 1) {
                throw new RuntimeException('Pairing code was claimed concurrently.');
            }
        }
        $db->commit();
        return [
            'paired' => true,
            'userId' => (string)$pairing['user_id'],
            'deviceId' => $deviceId,
            'deviceToken' => $deviceToken,
            'pairedAt' => $now,
            'retried' => !empty($pairing['claimed_at']),
        ];
    } catch (Throwable $e) {
        if ($db->inTransaction()) {
            $db->rollBack();
        }
        gobao_fail('Device pairing failed: ' . $e->getMessage(), 500);
    }
}

function gobao_companion_analyze_visual(array $input, array $env): array {
    gobao_companion_require_device_token(gobao_companion_user($input), gobao_companion_device($input), $env, $input);
    $imageBase64 = trim((string)($input['imageBase64'] ?? ''));
    $mime = strtolower(trim((string)($input['mime'] ?? 'image/jpeg')));
    $prompt = trim((string)($input['prompt'] ?? ''));
    $source = trim((string)($input['source'] ?? 'local-device'));
    return gobao_tools_qwen_vl_analyze_base64($imageBase64, $mime, $prompt, $env, $source);
}

function gobao_companion_permissions_from_input(array $input): array {
    $raw = is_array($input['permissions'] ?? null) ? $input['permissions'] : $input;
    return [
        'windows' => gobao_companion_bool($raw['windows'] ?? $raw['allowWindows'] ?? false),
        'browser' => gobao_companion_bool($raw['browser'] ?? $raw['allowBrowser'] ?? false),
        'adb' => gobao_companion_bool($raw['adb'] ?? $raw['allowAdb'] ?? false),
        'pyautogui' => gobao_companion_bool($raw['pyautogui'] ?? $raw['allowPyAutoGUI'] ?? false),
        'sandbox' => gobao_companion_bool($raw['sandbox'] ?? $raw['allowSandbox'] ?? false),
        'dryRun' => gobao_companion_bool($raw['dryRun'] ?? true, true),
        'requireLocalConfirm' => gobao_companion_bool($raw['requireLocalConfirm'] ?? true, true),
        'autoApprove' => gobao_companion_bool($raw['autoApprove'] ?? $raw['trusted'] ?? false),
    ];
}

function gobao_companion_update_permissions(string $userId, string $deviceId, array $input): array {
    $permissions = gobao_companion_permissions_from_input($input);
    return gobao_companion_register_device($userId, $deviceId, [
        'label' => $input['label'] ?? $deviceId,
        'rootPath' => $input['rootPath'] ?? '',
        'capabilities' => [
            'localControl' => true,
            'permissions' => $permissions,
            'updatedFrom' => $input['updatedFrom'] ?? 'chat-permission-panel',
        ],
        'status' => 'active',
    ]);
}

function gobao_companion_update_chat_permissions(array $input): array {
    $user = gobao_current_user(true);
    $userId = gobao_safe_session_id((string)$user['user_id']);
    $deviceId = 'chat-permission-panel-' . substr(hash('sha256', $userId), 0, 24);
    $input['updatedFrom'] = 'chat-permission-panel';
    $input['label'] = $input['label'] ?? 'GOBAO chat permission panel';
    $input['rootPath'] = '';
    return gobao_companion_update_permissions($userId, $deviceId, $input);
}

function gobao_companion_local_tool_names(): array {
    return [
        'windows_companion_action',
        'android_adb_action',
        'pyautogui_action',
        'shopping_prepare',
        'browser_task',
        'web_crawl',
        'file_task',
        'app_task',
        'phone_task',
        'desktop_task',
        'runtime_task',
        'code_execute',
        'local_capability_status',
    ];
}

function gobao_companion_local_tool_names_sql(): string {
    return "'" . implode("', '", gobao_companion_local_tool_names()) . "'";
}

function gobao_companion_channel_from_tool(string $toolName): string {
    return match ($toolName) {
        'windows_companion_action', 'shopping_prepare', 'browser_task', 'web_crawl', 'file_task', 'app_task', 'runtime_task' => 'windows',
        'android_adb_action', 'phone_task' => 'adb',
        'pyautogui_action', 'desktop_task' => 'pyautogui',
        'code_execute', 'local_capability_status' => 'sandbox',
        default => '',
    };
}

function gobao_companion_permissions_for_device(string $userId, string $deviceId): array {
    $status = gobao_companion_device_status($userId, $deviceId);
    $device = $status['devices'][0] ?? [];
    $caps = is_array($device['capabilities'] ?? null) ? $device['capabilities'] : [];
    $perms = is_array($caps['permissions'] ?? null) ? $caps['permissions'] : [];
    return [
        'windows' => gobao_companion_bool($perms['windows'] ?? $caps['windows'] ?? false),
        'browser' => gobao_companion_bool($perms['browser'] ?? $caps['browser'] ?? false),
        'adb' => gobao_companion_bool($perms['adb'] ?? $caps['adb'] ?? false),
        'pyautogui' => gobao_companion_bool($perms['pyautogui'] ?? $caps['pyautogui'] ?? false),
        'sandbox' => gobao_companion_bool($perms['sandbox'] ?? $caps['sandbox'] ?? false),
        'dryRun' => gobao_companion_bool($perms['dryRun'] ?? true, true),
        'requireLocalConfirm' => gobao_companion_bool($perms['requireLocalConfirm'] ?? true, true),
        'autoApprove' => gobao_companion_bool($perms['autoApprove'] ?? $caps['autoApprove'] ?? false),
    ];
}

function gobao_companion_chat_permissions_for_user(string $userId): array {
    $defaults = [
        'hasPanel' => false,
        'windows' => false,
        'browser' => false,
        'adb' => false,
        'pyautogui' => false,
        'sandbox' => false,
        'dryRun' => true,
        'requireLocalConfirm' => true,
        'autoApprove' => false,
    ];
    $db = gobao_db();
    if (!$db) {
        gobao_fail('Database is not available: ' . gobao_db_last_error(), 500);
    }
    $stmt = $db->prepare("
        SELECT capabilities_json FROM local_devices
        WHERE user_id = :user_id
          AND capabilities_json LIKE '%chat-permission-panel%'
        ORDER BY updated_at DESC
        LIMIT 1
    ");
    $stmt->execute([':user_id' => $userId]);
    $caps = json_decode((string)($stmt->fetchColumn() ?: '{}'), true) ?: [];
    $perms = is_array($caps['permissions'] ?? null) ? $caps['permissions'] : [];
    if (!$perms) {
        return $defaults;
    }
    return [
        'hasPanel' => true,
        'windows' => gobao_companion_bool($perms['windows'] ?? false),
        'browser' => gobao_companion_bool($perms['browser'] ?? false),
        'adb' => gobao_companion_bool($perms['adb'] ?? false),
        'pyautogui' => gobao_companion_bool($perms['pyautogui'] ?? false),
        'sandbox' => gobao_companion_bool($perms['sandbox'] ?? false),
        'dryRun' => gobao_companion_bool($perms['dryRun'] ?? true, true),
        'requireLocalConfirm' => gobao_companion_bool($perms['requireLocalConfirm'] ?? true, true),
        'autoApprove' => gobao_companion_bool($perms['autoApprove'] ?? false),
    ];
}

function gobao_companion_effective_permissions(array $chatPermissions, array $devicePermissions): array {
    $hasChatPanel = !empty($chatPermissions['hasPanel']);
    $trustedDevice = !empty($devicePermissions['autoApprove']);
    return [
        'windows' => !empty($devicePermissions['windows']) && ($trustedDevice || !$hasChatPanel || !empty($chatPermissions['windows'])),
        'browser' => !empty($devicePermissions['browser']) && ($trustedDevice || !$hasChatPanel || !empty($chatPermissions['browser'])),
        'adb' => !empty($devicePermissions['adb']) && ($trustedDevice || !$hasChatPanel || !empty($chatPermissions['adb'])),
        'pyautogui' => !empty($devicePermissions['pyautogui']) && ($trustedDevice || !$hasChatPanel || !empty($chatPermissions['pyautogui'])),
        'sandbox' => !empty($devicePermissions['sandbox']) && ($trustedDevice || !$hasChatPanel || !empty($chatPermissions['sandbox'])),
        'dryRun' => $trustedDevice ? !empty($devicePermissions['dryRun']) : ($hasChatPanel ? (!empty($chatPermissions['dryRun']) || !empty($devicePermissions['dryRun'])) : !empty($devicePermissions['dryRun'])),
        'requireLocalConfirm' => $trustedDevice ? !empty($devicePermissions['requireLocalConfirm']) : ($hasChatPanel ? (!empty($chatPermissions['requireLocalConfirm']) || !empty($devicePermissions['requireLocalConfirm'])) : !empty($devicePermissions['requireLocalConfirm'])),
        'autoApprove' => $trustedDevice || (!empty($chatPermissions['autoApprove']) && (!$hasChatPanel || !empty($chatPermissions['autoApprove']))),
    ];
}

function gobao_companion_update_run_result(int $runId, array $result): void {
    $db = gobao_db();
    if (!$db) {
        gobao_fail('Database is not available: ' . gobao_db_last_error(), 500);
    }
    gobao_db_retry(function () use ($db, $runId, $result): void {
        $stmt = $db->prepare('UPDATE tool_runs SET result_json = :result_json WHERE id = :id');
        $stmt->execute([
            ':result_json' => gobao_json_encode($result),
            ':id' => $runId,
        ]);
    });
}

function gobao_companion_poll_actions(string $userId, string $deviceId, array $input, array $env): array {
    gobao_companion_require_device_token($userId, $deviceId, $env, $input);
    $permissions = gobao_companion_permissions_from_input($input);
    gobao_companion_register_device($userId, $deviceId, [
        'label' => $input['label'] ?? $deviceId,
        'rootPath' => $input['rootPath'] ?? '',
        'capabilities' => [
            'localControl' => true,
            'permissions' => $permissions,
            'polling' => true,
        ],
        'status' => 'active',
        'skipAudit' => true,
    ]);
    $devicePermissions = gobao_companion_permissions_for_device($userId, $deviceId);
    $chatPermissions = gobao_companion_chat_permissions_for_user($userId);
    $effectivePermissions = gobao_companion_effective_permissions($chatPermissions, $devicePermissions);
    $limit = max(1, min(20, (int)($input['limit'] ?? 5)));
    $db = gobao_db();
    if (!$db) {
        gobao_fail('Database is not available: ' . gobao_db_last_error(), 500);
    }
    $localToolsSql = gobao_companion_local_tool_names_sql();
    $rows = gobao_db_retry(function () use ($db, $localToolsSql): array {
        $stmt = $db->query("
            SELECT * FROM tool_runs
            WHERE status = 'completed'
              AND tool_name IN ($localToolsSql)
            ORDER BY id ASC
            LIMIT 200
        ");
        return $stmt->fetchAll(PDO::FETCH_ASSOC) ?: [];
    });
    $actions = [];
    $diagnostics = [
        'scanned' => count($rows),
        'pendingLocal' => 0,
        'skippedNotQueued' => 0,
        'skippedPermission' => 0,
        'skippedDevice' => 0,
        'skippedUser' => 0,
        'expired' => 0,
        'claimed' => 0,
        'effectivePermissions' => $effectivePermissions,
        'recentSkipped' => [],
    ];
    foreach ($rows as $row) {
        if (count($actions) >= $limit) {
            break;
        }
        $result = json_decode((string)($row['result_json'] ?? '{}'), true) ?: [];
        $local = is_array($result['localCompanion'] ?? null) ? $result['localCompanion'] : null;
        if (!$local || (string)($local['status'] ?? '') !== 'queued') {
            $diagnostics['skippedNotQueued']++;
            continue;
        }
        $diagnostics['pendingLocal']++;
        $channel = (string)($local['channel'] ?? gobao_companion_channel_from_tool((string)$row['tool_name']));
        $localAction = (string)($local['action'] ?? 'status');
        $permissionChannel = $channel === 'windows' && str_starts_with($localAction, 'browser_')
            ? 'browser'
            : $channel;
        if ($permissionChannel === '' || empty($effectivePermissions[$permissionChannel])) {
            $diagnostics['skippedPermission']++;
            if (count($diagnostics['recentSkipped']) < 5) {
                $diagnostics['recentSkipped'][] = [
                    'runId' => (int)$row['id'],
                    'reason' => 'permission',
                    'toolName' => (string)$row['tool_name'],
                    'channel' => $channel,
                    'permissionChannel' => $permissionChannel,
                    'action' => $localAction,
                ];
            }
            continue;
        }
        $targetDevice = (string)($local['deviceId'] ?? 'default-device');
        if ($targetDevice !== 'default-device' && $targetDevice !== $deviceId) {
            $diagnostics['skippedDevice']++;
            if (count($diagnostics['recentSkipped']) < 5) {
                $diagnostics['recentSkipped'][] = [
                    'runId' => (int)$row['id'],
                    'reason' => 'device',
                    'targetDevice' => $targetDevice,
                    'currentDevice' => $deviceId,
                ];
            }
            continue;
        }
        $targetUser = (string)($local['userId'] ?? $userId);
        $legacyDefaultLocalTask = $targetDevice === 'default-device'
            && in_array($channel, ['windows', 'browser', 'pyautogui', 'sandbox'], true);
        if ($targetUser !== '' && $targetUser !== 'main' && $targetUser !== $userId && !$legacyDefaultLocalTask) {
            $diagnostics['skippedUser']++;
            if (count($diagnostics['recentSkipped']) < 5) {
                $diagnostics['recentSkipped'][] = [
                    'runId' => (int)$row['id'],
                    'reason' => 'user',
                    'targetUser' => $targetUser,
                    'currentUser' => $userId,
                ];
            }
            continue;
        }
        $expiresAt = strtotime((string)($local['expiresAt'] ?? ''));
        if ($expiresAt && $expiresAt < time()) {
            $diagnostics['expired']++;
            $result['localCompanion']['status'] = 'expired';
            $result['localCompanion']['expiredAt'] = gmdate('c');
            gobao_companion_update_run_result((int)$row['id'], $result);
            continue;
        }
        $result['localCompanion']['status'] = 'claimed';
        $result['localCompanion']['claimedAt'] = gmdate('c');
        $result['localCompanion']['claimedBy'] = $deviceId;
        gobao_companion_update_run_result((int)$row['id'], $result);
        $diagnostics['claimed']++;
        $actions[] = [
            'runId' => (int)$row['id'],
            'toolName' => (string)$row['tool_name'],
            'title' => (string)$row['title'],
            'channel' => $channel,
            'action' => $localAction,
            'payload' => is_array($local['payload'] ?? null) ? $local['payload'] : [],
            'dryRun' => (bool)($local['dryRun'] ?? $effectivePermissions['dryRun']),
            'requiresLocalConfirmation' => (bool)($local['requiresLocalConfirmation'] ?? $effectivePermissions['requireLocalConfirm']),
            'createdAt' => (string)($local['createdAt'] ?? ''),
            'claimedAt' => $result['localCompanion']['claimedAt'],
        ];
    }
    return [
        'ok' => true,
        'userId' => $userId,
        'deviceId' => $deviceId,
        'permissions' => $effectivePermissions,
        'chatPermissions' => $chatPermissions,
        'devicePermissions' => $devicePermissions,
        'actions' => $actions,
        'diagnostics' => $diagnostics,
        'database' => gobao_db_counts(),
    ];
}

function gobao_companion_complete_action(string $userId, string $deviceId, array $input, array $env): array {
    gobao_companion_require_device_token($userId, $deviceId, $env, $input);
    $runId = (int)($input['runId'] ?? $input['id'] ?? 0);
    if ($runId <= 0) {
        gobao_fail('runId is required', 400);
    }
    $db = gobao_db();
    if (!$db) {
        gobao_fail('Database is not available: ' . gobao_db_last_error(), 500);
    }
    $localToolsSql = gobao_companion_local_tool_names_sql();
    $row = gobao_db_retry(function () use ($db, $runId, $localToolsSql) {
        $stmt = $db->prepare("SELECT * FROM tool_runs WHERE id = :id AND tool_name IN ($localToolsSql)");
        $stmt->execute([':id' => $runId]);
        return $stmt->fetch(PDO::FETCH_ASSOC);
    });
    if (!$row) {
        gobao_fail('Local companion action not found', 404);
    }
    $result = json_decode((string)($row['result_json'] ?? '{}'), true) ?: [];
    $local = is_array($result['localCompanion'] ?? null) ? $result['localCompanion'] : [];
    $targetUser = (string)($local['userId'] ?? '');
    $targetDevice = (string)($local['deviceId'] ?? 'default-device');
    $channel = (string)($local['channel'] ?? gobao_companion_channel_from_tool((string)$row['tool_name']));
    $legacyDefaultLocalTask = $targetDevice === 'default-device'
        && in_array($channel, ['windows', 'browser', 'pyautogui', 'sandbox'], true);
    $userMatches = $targetUser === '' || $targetUser === 'main' || $targetUser === $userId || $legacyDefaultLocalTask;
    if (!$userMatches
        || (string)($local['claimedBy'] ?? '') !== $deviceId
        || (string)($local['status'] ?? '') !== 'claimed') {
        gobao_fail('Action is not claimed by this user and device.', 403);
    }
    $ok = gobao_companion_bool($input['ok'] ?? false);
    $result['localCompanion']['status'] = $ok ? 'local_completed' : 'local_failed';
    $result['localCompanion']['completedAt'] = gmdate('c');
    $result['localCompanion']['completedBy'] = $deviceId;
    $result['localCompanion']['localResult'] = is_array($input['result'] ?? null) ? $input['result'] : [];
    if (!$ok) {
        $result['localCompanion']['error'] = (string)($input['error'] ?? 'local action failed');
    }
    gobao_companion_update_run_result($runId, $result);
    try {
        gobao_audit_record($ok ? 'local_companion_action_completed' : 'local_companion_action_failed', [
            'sessionId' => $userId,
            'actor' => $deviceId,
            'status' => $ok ? 'completed' : 'failed',
            'refType' => 'tool_run',
            'refId' => (string)$runId,
            'title' => (string)$row['title'],
            'payload' => [
                'toolName' => (string)$row['tool_name'],
                'result' => $result['localCompanion']['localResult'],
            ],
            'error' => !$ok ? (string)($input['error'] ?? '') : '',
        ]);
    } catch (Throwable $e) {
        error_log('GOBAO companion completion audit skipped: ' . $e->getMessage());
    }
    return [
        'ok' => true,
        'runId' => $runId,
        'localStatus' => $result['localCompanion']['status'],
    ];
}

if ($method === 'GET') {
    $action = trim((string)($_GET['action'] ?? 'status'));
    $currentUser = gobao_current_user(true);
    $userId = gobao_safe_session_id((string)$currentUser['user_id']);
    $deviceId = trim((string)($_GET['deviceId'] ?? ''));
    if ($action === 'status') {
        gobao_ok(gobao_companion_device_status($userId, $deviceId));
    }
    gobao_fail('Unsupported GET action', 400);
}

if ($method !== 'POST') {
    gobao_fail('POST required', 405);
}

$input = gobao_json_input();
$action = trim((string)($input['action'] ?? 'status'));
$env = gobao_load_env();

if ($action === 'pair_device') {
    gobao_ok(gobao_companion_pair_device($input));
}

$userId = gobao_companion_user($input);
$deviceId = gobao_companion_device($input);

if ($action === 'maintenance_tick') {
    gobao_companion_require_device_token($userId, $deviceId, $env, $input);
    gobao_ok([
        'knowledgeFreshness' => gobao_tools_knowledge_freshness_tick([
            'force' => !empty($input['force']),
            'limit' => (int)($input['knowledgeLimit'] ?? 1),
            'refreshFileLimit' => (int)($input['refreshFileLimit'] ?? 2),
            'timeBudgetSeconds' => (int)($input['timeBudgetSeconds'] ?? 35),
            'sessionId' => 'local-companion-maintenance',
            'actor' => $deviceId,
        ], $env),
        'learning' => gobao_tools_scheduled_learning_tick([
            'force' => false,
            'limit' => (int)($input['reviewLimit'] ?? 35),
            'skipKnowledgeFreshness' => true,
            'sessionId' => 'local-companion-maintenance',
        ], $env),
    ]);
}

if ($action === 'analyze_visual') {
    gobao_ok(gobao_companion_analyze_visual($input, $env));
}

if ($action === 'register') {
    gobao_companion_require_device_token($userId, $deviceId, $env, $input);
    gobao_ok(gobao_companion_register_device($userId, $deviceId, $input));
}

if ($action === 'sync_vault') {
    gobao_companion_require_device_token($userId, $deviceId, $env, $input);
    gobao_ok(gobao_companion_store_vault($userId, $deviceId, $input, $env));
}

if ($action === 'update_permissions') {
    if ((string)($input['updatedFrom'] ?? '') === 'chat-permission-panel') {
        gobao_ok(gobao_companion_update_chat_permissions($input));
    }
    gobao_companion_require_device_token($userId, $deviceId, $env, $input);
    gobao_ok(gobao_companion_update_permissions($userId, $deviceId, $input));
}

if ($action === 'poll_actions') {
    gobao_ok(gobao_companion_poll_actions($userId, $deviceId, $input, $env));
}

if ($action === 'complete_action') {
    gobao_ok(gobao_companion_complete_action($userId, $deviceId, $input, $env));
}

if ($action === 'status') {
    gobao_companion_require_device_token($userId, $deviceId, $env, $input);
    gobao_ok(gobao_companion_device_status($userId, $deviceId));
}

gobao_fail('Unsupported action', 400);
