"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.TenantPurgeWindowError = exports.TenantPurgeBlockedError = void 0;
exports.getTenantTombstone = getTenantTombstone;
exports.createTenantTombstone = createTenantTombstone;
exports.evaluateTenantMutationPolicy = evaluateTenantMutationPolicy;
exports.sendTenantMutationDeniedPayload = sendTenantMutationDeniedPayload;
exports.purgeTenantNow = purgeTenantNow;
const crypto_1 = __importDefault(require("crypto"));
const promises_1 = __importDefault(require("fs/promises"));
const path_1 = __importDefault(require("path"));
const enterpriseAuditService_1 = require("./enterpriseAuditService");
const enterpriseDb_1 = require("./enterpriseDb");
const traceMetadataStore_1 = require("./traceMetadataStore");
const DELETE_WINDOW_MS = 7 * 24 * 60 * 60 * 1000;
const SAFE_TENANT_SEGMENT_RE = /^[a-zA-Z0-9._:-]+$/;
const ACTIVE_RUN_STATUSES = ['pending', 'running', 'awaiting_user'];
const ACTIVE_LEASE_STATES = ['pending', 'starting', 'active', 'draining', 'restarting'];
class TenantPurgeBlockedError extends Error {
    constructor(blockers) {
        super('Tenant purge is blocked by active work');
        this.blockers = blockers;
        this.name = 'TenantPurgeBlockedError';
    }
}
exports.TenantPurgeBlockedError = TenantPurgeBlockedError;
class TenantPurgeWindowError extends Error {
    constructor(purgeAfter) {
        super('Tenant purge window has not elapsed');
        this.purgeAfter = purgeAfter;
        this.name = 'TenantPurgeWindowError';
    }
}
exports.TenantPurgeWindowError = TenantPurgeWindowError;
function sha256(value) {
    return `sha256:${crypto_1.default.createHash('sha256').update(value).digest('hex')}`;
}
function stableStringify(value) {
    return JSON.stringify(canonicalize(value));
}
function canonicalize(value) {
    if (Array.isArray(value))
        return value.map(canonicalize);
    if (!value || typeof value !== 'object')
        return value;
    const input = value;
    const out = {};
    for (const key of Object.keys(input).sort()) {
        const child = input[key];
        if (child !== undefined)
            out[key] = canonicalize(child);
    }
    return out;
}
function assertSafeTenantId(tenantId) {
    if (!SAFE_TENANT_SEGMENT_RE.test(tenantId) || tenantId === '.' || tenantId === '..') {
        throw new Error(`Unsafe tenant id: ${tenantId}`);
    }
    return tenantId;
}
function rowToRecord(row) {
    return {
        tenantId: row.tenant_id,
        requestedBy: row.requested_by,
        requestedAt: row.requested_at,
        purgeAfter: row.purge_after,
        status: row.status,
        proofHash: row.proof_hash,
    };
}
function getTenantTombstoneRow(db, tenantId) {
    return db.prepare(`
    SELECT *
    FROM tenant_tombstones
    WHERE tenant_id = ?
    LIMIT 1
  `).get(tenantId) ?? null;
}
function getTenantTombstone(db, tenantId) {
    const row = getTenantTombstoneRow(db, tenantId);
    return row ? rowToRecord(row) : null;
}
function ensureTenantLifecycleGraph(db, context, now) {
    db.prepare(`
    INSERT OR IGNORE INTO organizations (id, name, status, plan, created_at, updated_at)
    VALUES (?, ?, 'active', 'enterprise', ?, ?)
  `).run(context.tenantId, context.tenantId, now, now);
    db.prepare(`
    INSERT OR IGNORE INTO users (id, tenant_id, email, display_name, idp_subject, created_at, updated_at)
    VALUES (?, ?, ?, ?, ?, ?, ?)
  `).run(context.userId, context.tenantId, `${context.userId}@tenant.local`, context.userId, `tenant:${context.tenantId}:${context.userId}`, now, now);
}
function createTenantTombstone(db, context, reason, now = Date.now()) {
    assertSafeTenantId(context.tenantId);
    const purgeAfter = now + DELETE_WINDOW_MS;
    const tx = db.transaction(() => {
        ensureTenantLifecycleGraph(db, context, now);
        db.prepare(`
      INSERT INTO tenant_tombstones
        (tenant_id, requested_by, requested_at, purge_after, status, proof_hash)
      VALUES
        (?, ?, ?, ?, 'tombstoned', NULL)
      ON CONFLICT(tenant_id) DO UPDATE SET
        requested_by = excluded.requested_by,
        requested_at = excluded.requested_at,
        purge_after = excluded.purge_after,
        status = excluded.status,
        proof_hash = NULL
    `).run(context.tenantId, context.userId, now, purgeAfter);
        db.prepare(`
      UPDATE organizations
      SET status = 'tombstoned', updated_at = ?
      WHERE id = ?
    `).run(now, context.tenantId);
        (0, enterpriseAuditService_1.recordEnterpriseAuditEvent)(db, {
            tenantId: context.tenantId,
            actorUserId: context.userId,
            action: 'tenant.tombstoned',
            resourceType: 'tenant',
            resourceId: context.tenantId,
            metadata: {
                purgeAfter,
                reason: reason || undefined,
                requestId: context.requestId,
            },
        });
    });
    tx();
    return rowToRecord(getTenantTombstoneRow(db, context.tenantId));
}
function evaluateTenantMutationPolicy(context) {
    const db = (0, enterpriseDb_1.openEnterpriseDb)();
    try {
        const tombstone = getTenantTombstoneRow(db, context.tenantId);
        if (!tombstone) {
            return {
                allowed: true,
                httpStatus: 200,
                code: 'TENANT_ACTIVE',
                status: 'active',
                message: 'Tenant accepts new work',
            };
        }
        return {
            allowed: false,
            httpStatus: 423,
            code: 'TENANT_TOMBSTONED',
            status: tombstone.status,
            message: 'Tenant is tombstoned and does not accept new uploads or analysis runs',
            details: {
                tenantId: context.tenantId,
                requestedAt: tombstone.requested_at,
                purgeAfter: tombstone.purge_after,
                proofHash: tombstone.proof_hash,
            },
        };
    }
    finally {
        db.close();
    }
}
function sendTenantMutationDeniedPayload(decision) {
    return {
        success: false,
        code: decision.code,
        status: decision.status,
        error: decision.message,
        details: decision.details,
    };
}
function getPurgeBlockers(db, tenantId) {
    const activeRuns = db.prepare(`
    SELECT id, 'analysis_run' AS kind, status, workspace_id
    FROM analysis_runs
    WHERE tenant_id = ?
      AND status IN (${ACTIVE_RUN_STATUSES.map(() => '?').join(', ')})
    ORDER BY workspace_id ASC, id ASC
  `).all(tenantId, ...ACTIVE_RUN_STATUSES);
    const activeLeases = db.prepare(`
    SELECT id, 'trace_processor_lease' AS kind, state AS status, workspace_id
    FROM trace_processor_leases
    WHERE tenant_id = ?
      AND state IN (${ACTIVE_LEASE_STATES.map(() => '?').join(', ')})
    ORDER BY workspace_id ASC, id ASC
  `).all(tenantId, ...ACTIVE_LEASE_STATES);
    return [...activeRuns, ...activeLeases];
}
function countRows(db, table, tenantId) {
    const row = db.prepare(`
    SELECT COUNT(*) AS count
    FROM ${table}
    WHERE tenant_id = ?
  `).get(tenantId);
    return row?.count ?? 0;
}
function collectDeletedCounts(db, tenantId) {
    const holderCount = db.prepare(`
    SELECT COUNT(*) AS count
    FROM trace_processor_holders h
    JOIN trace_processor_leases l ON l.id = h.lease_id
    WHERE l.tenant_id = ?
  `).get(tenantId)?.count ?? 0;
    return {
        workspaces: countRows(db, 'workspaces', tenantId),
        users: countRows(db, 'users', tenantId),
        apiKeys: countRows(db, 'api_keys', tenantId),
        traceAssets: countRows(db, 'trace_assets', tenantId),
        traceProcessorLeases: countRows(db, 'trace_processor_leases', tenantId),
        traceProcessorHolders: holderCount,
        analysisSessions: countRows(db, 'analysis_sessions', tenantId),
        analysisRuns: countRows(db, 'analysis_runs', tenantId),
        conversationTurns: countRows(db, 'conversation_turns', tenantId),
        reportArtifacts: countRows(db, 'report_artifacts', tenantId),
        memoryEntries: countRows(db, 'memory_entries', tenantId),
        skillRegistryEntries: countRows(db, 'skill_registry_entries', tenantId),
        providerCredentials: countRows(db, 'provider_credentials', tenantId),
        providerSnapshots: countRows(db, 'provider_snapshots', tenantId),
    };
}
function tenantDataPath(tenantId) {
    const safeTenantId = assertSafeTenantId(tenantId);
    const root = path_1.default.resolve((0, traceMetadataStore_1.resolveEnterpriseDataRoot)());
    const target = path_1.default.resolve(root, safeTenantId);
    if (target !== root && !target.startsWith(`${root}${path_1.default.sep}`)) {
        throw new Error(`Resolved tenant data path escapes data root: ${target}`);
    }
    return target;
}
async function removeTenantDataPath(tenantId) {
    const target = tenantDataPath(tenantId);
    await promises_1.default.rm(target, { recursive: true, force: true });
    return { path: target, removed: true };
}
async function purgeTenantNow(db, context, now = Date.now()) {
    assertSafeTenantId(context.tenantId);
    const tombstone = getTenantTombstoneRow(db, context.tenantId);
    if (!tombstone || tombstone.status === 'purged') {
        throw new Error('Tenant tombstone not found');
    }
    if (tombstone.purge_after > now) {
        throw new TenantPurgeWindowError(tombstone.purge_after);
    }
    const blockers = getPurgeBlockers(db, context.tenantId);
    if (blockers.length > 0) {
        db.prepare(`
      UPDATE tenant_tombstones
      SET status = 'purge_blocked'
      WHERE tenant_id = ?
    `).run(context.tenantId);
        (0, enterpriseAuditService_1.recordEnterpriseAuditEvent)(db, {
            tenantId: context.tenantId,
            action: 'tenant.purge_blocked',
            resourceType: 'tenant',
            resourceId: context.tenantId,
            metadata: {
                blockers,
                requestId: context.requestId,
            },
        });
        throw new TenantPurgeBlockedError(blockers);
    }
    const deletedCounts = collectDeletedCounts(db, context.tenantId);
    db.prepare(`
    UPDATE tenant_tombstones
    SET status = 'purging'
    WHERE tenant_id = ?
  `).run(context.tenantId);
    const dataRemoval = await removeTenantDataPath(context.tenantId);
    const proofWithoutHash = {
        tenantId: context.tenantId,
        requestedBy: tombstone.requested_by,
        purgedBy: context.userId,
        requestedAt: tombstone.requested_at,
        purgeAfter: tombstone.purge_after,
        purgedAt: now,
        deletedCounts,
        dataPath: dataRemoval.path,
        dataPathRemoved: dataRemoval.removed,
    };
    const proofHash = sha256(stableStringify(proofWithoutHash));
    const proof = {
        ...proofWithoutHash,
        proofHash,
    };
    const tx = db.transaction(() => {
        db.prepare('DELETE FROM provider_snapshots WHERE tenant_id = ?').run(context.tenantId);
        db.prepare('DELETE FROM provider_credentials WHERE tenant_id = ?').run(context.tenantId);
        db.prepare('DELETE FROM api_keys WHERE tenant_id = ?').run(context.tenantId);
        db.prepare('DELETE FROM memory_entries WHERE tenant_id = ?').run(context.tenantId);
        db.prepare('DELETE FROM skill_registry_entries WHERE tenant_id = ?').run(context.tenantId);
        db.prepare('DELETE FROM workspaces WHERE tenant_id = ?').run(context.tenantId);
        db.prepare('DELETE FROM users WHERE tenant_id = ?').run(context.tenantId);
        db.prepare(`
      UPDATE organizations
      SET status = 'purged', updated_at = ?
      WHERE id = ?
    `).run(now, context.tenantId);
        db.prepare(`
      UPDATE tenant_tombstones
      SET status = 'purged', proof_hash = ?
      WHERE tenant_id = ?
    `).run(proofHash, context.tenantId);
        (0, enterpriseAuditService_1.recordEnterpriseAuditEvent)(db, {
            tenantId: context.tenantId,
            action: 'tenant.purged',
            resourceType: 'tenant',
            resourceId: context.tenantId,
            metadata: {
                proof,
                requestId: context.requestId,
            },
        });
    });
    tx();
    return proof;
}
//# sourceMappingURL=enterpriseTenantLifecycleService.js.map