"use strict";
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2024-2026 Gracker (Chris)
// This file is part of SmartPerfetto. See LICENSE for details.
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ENTERPRISE_DATA_DIR_ENV = void 0;
exports.getUploadRoot = getUploadRoot;
exports.resolveEnterpriseDataRoot = resolveEnterpriseDataRoot;
exports.getTracesDir = getTracesDir;
exports.getEnterpriseTracesDirForContext = getEnterpriseTracesDirForContext;
exports.getWritableTraceDirForContext = getWritableTraceDirForContext;
exports.isSafeTraceId = isSafeTraceId;
exports.getTraceMetadataPath = getTraceMetadataPath;
exports.getTraceFilePath = getTraceFilePath;
exports.writeTraceMetadata = writeTraceMetadata;
exports.readTraceMetadata = readTraceMetadata;
exports.listTraceMetadata = listTraceMetadata;
exports.listTraceMetadataForContext = listTraceMetadataForContext;
exports.deleteTraceMetadata = deleteTraceMetadata;
exports.deleteTraceMetadataForContext = deleteTraceMetadataForContext;
exports.buildTraceOwnerMetadata = buildTraceOwnerMetadata;
exports.isTraceMetadataOwnedByContext = isTraceMetadataOwnedByContext;
exports.readTraceMetadataForContext = readTraceMetadataForContext;
const path_1 = __importDefault(require("path"));
const promises_1 = __importDefault(require("fs/promises"));
const enterpriseDb_1 = require("./enterpriseDb");
const enterpriseRepository_1 = require("./enterpriseRepository");
const enterpriseMigration_1 = require("./enterpriseMigration");
const resourceOwnership_1 = require("./resourceOwnership");
const rbac_1 = require("./rbac");
const enterpriseQuotaPolicyService_1 = require("./enterpriseQuotaPolicyService");
const SAFE_TRACE_ID_RE = /^[a-zA-Z0-9._:-]+$/;
exports.ENTERPRISE_DATA_DIR_ENV = 'SMARTPERFETTO_DATA_DIR';
function getUploadRoot() {
    return process.env.UPLOAD_DIR || './uploads';
}
function resolveEnterpriseDataRoot(env = process.env) {
    const configured = env[exports.ENTERPRISE_DATA_DIR_ENV];
    return path_1.default.resolve(configured && configured.trim().length > 0 ? configured : 'data');
}
function getTracesDir() {
    return path_1.default.join(getUploadRoot(), 'traces');
}
function enterpriseTraceStoreEnabled(env = process.env) {
    return (0, enterpriseMigration_1.enterpriseDbReadAuthorityEnabled)(env);
}
function enterpriseTraceDbWritesEnabled(env = process.env) {
    return (0, enterpriseMigration_1.enterpriseDbWritesEnabled)(env);
}
function legacyTraceMetadataWritesEnabled(env = process.env) {
    return (0, enterpriseMigration_1.legacyFilesystemWritesEnabled)(env);
}
function assertSafePathSegment(value, label) {
    if (!SAFE_TRACE_ID_RE.test(value) || value === '.' || value === '..') {
        throw new Error(`Unsafe ${label}: ${value}`);
    }
    return value;
}
function getEnterpriseTracesDirForContext(context, env = process.env) {
    return path_1.default.join(resolveEnterpriseDataRoot(env), assertSafePathSegment(context.tenantId, 'tenant id'), assertSafePathSegment(context.workspaceId, 'workspace id'), 'traces');
}
function getWritableTraceDirForContext(context) {
    return enterpriseTraceStoreEnabled()
        ? getEnterpriseTracesDirForContext(context)
        : getTracesDir();
}
function isSafeTraceId(traceId) {
    return SAFE_TRACE_ID_RE.test(traceId);
}
function getTraceMetadataPath(traceId) {
    if (!isSafeTraceId(traceId))
        return null;
    return path_1.default.join(getTracesDir(), `${traceId}.json`);
}
function getTraceFilePath(traceId) {
    if (!isSafeTraceId(traceId))
        return null;
    return path_1.default.join(getTracesDir(), `${traceId}.trace`);
}
function parseMetadataJson(value) {
    if (!value)
        return {};
    try {
        const parsed = JSON.parse(value);
        return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
            ? parsed
            : {};
    }
    catch {
        return {};
    }
}
function metadataDateMs(uploadedAt) {
    const parsed = uploadedAt ? Date.parse(uploadedAt) : Number.NaN;
    return Number.isFinite(parsed) ? parsed : Date.now();
}
function metadataJsonForRow(metadata) {
    return JSON.stringify({
        filename: metadata.filename,
        uploadedAt: metadata.uploadedAt,
        ...(typeof metadata.port === 'number' ? { port: metadata.port } : {}),
        ...(metadata.externalRpc ? { externalRpc: true } : {}),
    });
}
function rowToTraceMetadata(row) {
    const extra = parseMetadataJson(row.metadata_json);
    const filename = typeof extra.filename === 'string'
        ? extra.filename
        : path_1.default.basename(row.local_path);
    const uploadedAt = typeof extra.uploadedAt === 'string'
        ? extra.uploadedAt
        : new Date(row.created_at).toISOString();
    const port = typeof extra.port === 'number' ? extra.port : undefined;
    const externalRpc = extra.externalRpc === true;
    return {
        id: row.id,
        filename,
        size: row.size_bytes ?? 0,
        uploadedAt,
        status: row.status,
        path: row.local_path,
        tenantId: row.tenant_id,
        workspaceId: row.workspace_id,
        ...(row.owner_user_id ? { userId: row.owner_user_id } : {}),
        ...(typeof port === 'number' ? { port } : {}),
        ...(externalRpc ? { externalRpc: true } : {}),
        ...(typeof row.expires_at === 'number' ? { expiresAt: row.expires_at } : {}),
    };
}
function isTraceAssetRowLive(row, now = Date.now()) {
    return row.expires_at === null || row.expires_at > now;
}
function withEnterpriseTraceDb(fn) {
    const db = (0, enterpriseDb_1.openEnterpriseDb)();
    try {
        return fn(db);
    }
    finally {
        db.close();
    }
}
function ensureEnterpriseTraceOwner(db, tenantId, workspaceId, userId) {
    const now = Date.now();
    db.prepare(`
    INSERT OR IGNORE INTO organizations (id, name, status, plan, created_at, updated_at)
    VALUES (?, ?, 'active', 'enterprise', ?, ?)
  `).run(tenantId, tenantId, now, now);
    db.prepare(`
    INSERT OR IGNORE INTO workspaces (id, tenant_id, name, created_at, updated_at)
    VALUES (?, ?, ?, ?, ?)
  `).run(workspaceId, tenantId, workspaceId, now, now);
    if (!userId)
        return;
    db.prepare(`
    INSERT INTO users (id, tenant_id, email, display_name, idp_subject, created_at, updated_at)
    VALUES (?, ?, ?, ?, ?, ?, ?)
    ON CONFLICT(id) DO UPDATE SET
      email = excluded.email,
      display_name = excluded.display_name,
      updated_at = excluded.updated_at
  `).run(userId, tenantId, `${userId}@trace.local`, userId, `trace:${userId}`, now, now);
}
function enterpriseLocalPathForMetadata(metadata) {
    if (metadata.path && metadata.path.trim().length > 0)
        return metadata.path;
    if (metadata.externalRpc && typeof metadata.port === 'number') {
        return `external-rpc:${metadata.port}`;
    }
    if (enterpriseTraceStoreEnabled() &&
        metadata.tenantId &&
        metadata.workspaceId) {
        return path_1.default.join(resolveEnterpriseDataRoot(), assertSafePathSegment(metadata.tenantId, 'tenant id'), assertSafePathSegment(metadata.workspaceId, 'workspace id'), 'traces', `${assertSafePathSegment(metadata.id, 'trace id')}.trace`);
    }
    const fallbackPath = getTraceFilePath(metadata.id);
    if (!fallbackPath)
        throw new Error(`Unsafe trace id: ${metadata.id}`);
    return fallbackPath;
}
function writeEnterpriseTraceMetadata(metadata) {
    if (!metadata.tenantId || !metadata.workspaceId) {
        throw new Error('Enterprise trace metadata requires tenantId and workspaceId');
    }
    const tenantId = assertSafePathSegment(metadata.tenantId, 'tenant id');
    const workspaceId = assertSafePathSegment(metadata.workspaceId, 'workspace id');
    const ownerUserId = metadata.userId
        ? assertSafePathSegment(metadata.userId, 'user id')
        : null;
    const createdAt = metadataDateMs(metadata.uploadedAt);
    withEnterpriseTraceDb((db) => {
        ensureEnterpriseTraceOwner(db, tenantId, workspaceId, ownerUserId ?? undefined);
        const expiresAt = (0, enterpriseQuotaPolicyService_1.resolveEnterpriseRetentionExpiresAt)(db, { tenantId, workspaceId, ...(ownerUserId ? { userId: ownerUserId } : {}) }, 'trace', createdAt);
        const repo = (0, enterpriseRepository_1.createEnterpriseWorkspaceRepository)(db, 'trace_assets');
        const changes = repo.upsertById({ tenantId, workspaceId, ...(ownerUserId ? { userId: ownerUserId } : {}) }, metadata.id, {
            owner_user_id: ownerUserId,
            local_path: enterpriseLocalPathForMetadata(metadata),
            size_bytes: metadata.size,
            status: metadata.status,
            metadata_json: metadataJsonForRow(metadata),
            created_at: createdAt,
            expires_at: expiresAt,
        });
        if (changes === 0) {
            throw new Error('Trace metadata id already exists outside the repository scope');
        }
    });
}
async function writeTraceMetadata(metadata) {
    if (!isSafeTraceId(metadata.id)) {
        throw new Error(`Unsafe trace id: ${metadata.id}`);
    }
    if (enterpriseTraceDbWritesEnabled()) {
        writeEnterpriseTraceMetadata(metadata);
    }
    if (!legacyTraceMetadataWritesEnabled())
        return;
    const tracesDir = getTracesDir();
    await promises_1.default.mkdir(tracesDir, { recursive: true });
    await promises_1.default.writeFile(path_1.default.join(tracesDir, `${metadata.id}.json`), JSON.stringify(metadata, null, 2));
}
function readEnterpriseTraceMetadata(traceId) {
    return withEnterpriseTraceDb((db) => {
        const row = db.prepare(`
      SELECT *
      FROM trace_assets
      WHERE id = ?
        AND (expires_at IS NULL OR expires_at > ?)
    `).get(traceId, Date.now());
        return row ? rowToTraceMetadata(row) : null;
    });
}
function readEnterpriseTraceMetadataForScope(traceId, scope) {
    return withEnterpriseTraceDb((db) => {
        const repo = (0, enterpriseRepository_1.createEnterpriseWorkspaceRepository)(db, 'trace_assets');
        const row = repo.getById(scope, traceId);
        return row && isTraceAssetRowLive(row) ? rowToTraceMetadata(row) : null;
    });
}
async function readTraceMetadata(traceId) {
    if (enterpriseTraceStoreEnabled()) {
        if (!isSafeTraceId(traceId))
            return null;
        return readEnterpriseTraceMetadata(traceId);
    }
    const metadataPath = getTraceMetadataPath(traceId);
    if (!metadataPath)
        return null;
    try {
        const raw = await promises_1.default.readFile(metadataPath, 'utf8');
        const parsed = JSON.parse(raw);
        if (!parsed || parsed.id !== traceId) {
            return null;
        }
        return parsed;
    }
    catch {
        return null;
    }
}
function listEnterpriseTraceMetadata() {
    return withEnterpriseTraceDb((db) => {
        const rows = db.prepare(`
      SELECT *
      FROM trace_assets
      WHERE expires_at IS NULL OR expires_at > ?
      ORDER BY created_at DESC
    `).all(Date.now());
        return rows.map(rowToTraceMetadata);
    });
}
async function listTraceMetadata() {
    if (enterpriseTraceStoreEnabled()) {
        return listEnterpriseTraceMetadata();
    }
    const tracesDir = getTracesDir();
    let files;
    try {
        files = await promises_1.default.readdir(tracesDir);
    }
    catch {
        return [];
    }
    const traces = [];
    for (const file of files) {
        if (!file.endsWith('.json'))
            continue;
        const traceId = file.slice(0, -'.json'.length);
        const metadata = await readTraceMetadata(traceId);
        if (metadata)
            traces.push(metadata);
    }
    return traces;
}
async function listTraceMetadataForContext(context) {
    if (enterpriseTraceStoreEnabled()) {
        return withEnterpriseTraceDb((db) => {
            const repo = (0, enterpriseRepository_1.createEnterpriseWorkspaceRepository)(db, 'trace_assets');
            const now = Date.now();
            return repo.list((0, enterpriseRepository_1.repositoryScopeFromRequestContext)(context), {}, {
                orderBy: 'created_at',
                direction: 'DESC',
            })
                .filter(row => isTraceAssetRowLive(row, now))
                .map(rowToTraceMetadata)
                .filter(metadata => (0, rbac_1.canReadTraceResource)(metadata, context));
        });
    }
    const ownedTraces = [];
    for (const trace of await listTraceMetadata()) {
        const owned = await readTraceMetadataForContext(trace.id, context);
        if (owned)
            ownedTraces.push(owned);
    }
    return ownedTraces;
}
async function deleteTraceMetadata(traceId) {
    if (!isSafeTraceId(traceId))
        return;
    if (enterpriseTraceDbWritesEnabled()) {
        withEnterpriseTraceDb((db) => {
            db.prepare('DELETE FROM trace_assets WHERE id = ?').run(traceId);
        });
    }
    if (!legacyTraceMetadataWritesEnabled())
        return;
    const metadataPath = getTraceMetadataPath(traceId);
    if (!metadataPath)
        return;
    try {
        await promises_1.default.unlink(metadataPath);
    }
    catch {
        // Already deleted.
    }
}
async function deleteTraceMetadataForContext(traceId, context) {
    if (!isSafeTraceId(traceId))
        return;
    if (enterpriseTraceDbWritesEnabled()) {
        withEnterpriseTraceDb((db) => {
            (0, enterpriseRepository_1.createEnterpriseWorkspaceRepository)(db, 'trace_assets')
                .deleteById((0, enterpriseRepository_1.repositoryScopeFromRequestContext)(context), traceId);
        });
    }
    if (!legacyTraceMetadataWritesEnabled())
        return;
    const metadataPath = getTraceMetadataPath(traceId);
    if (!metadataPath)
        return;
    try {
        await promises_1.default.unlink(metadataPath);
    }
    catch {
        // Already deleted.
    }
}
function buildTraceOwnerMetadata(context) {
    return (0, resourceOwnership_1.ownerFieldsFromContext)(context);
}
function isTraceMetadataOwnedByContext(metadata, context) {
    return Boolean(metadata && (0, rbac_1.canReadTraceResource)(metadata, context));
}
async function readTraceMetadataForContext(traceId, context) {
    if (enterpriseTraceStoreEnabled()) {
        if (!isSafeTraceId(traceId))
            return null;
        const metadata = readEnterpriseTraceMetadataForScope(traceId, (0, enterpriseRepository_1.repositoryScopeFromRequestContext)(context));
        return isTraceMetadataOwnedByContext(metadata, context) ? metadata : null;
    }
    const metadata = await readTraceMetadata(traceId);
    return isTraceMetadataOwnedByContext(metadata, context) ? metadata : null;
}
//# sourceMappingURL=traceMetadataStore.js.map