import AnalyticsDatabaseService from "./AnalyticsDatabaseService";
import ProjectService from "./ProjectService";
import { IsBillingEnabled, IsEnterpriseEdition } from "../EnvironmentConfig";
import logger from "../Utils/Logger";
import AuditLog from "../../Models/AnalyticsModels/AuditLog";
import AuditLogAction from "../../Types/AuditLog/AuditLogAction";
import { PlanType } from "../../Types/Billing/SubscriptionPlan";
import { getColumnAccessControlForAllColumns } from "../../Types/Database/AccessControl/ColumnAccessControl";
import ObjectID from "../../Types/ObjectID";
import UserType from "../../Types/UserType";
import UserService from "./UserService";
import OneUptimeDate from "../../Types/Date";
const PROJECT_SETTINGS_CACHE_TTL_MS = 60 * 1000;
const USER_CACHE_TTL_MS = 5 * 60 * 1000;
const SKIPPED_FIELDS = new Set([
    "_id",
    "id",
    "createdAt",
    "updatedAt",
    "deletedAt",
    "version",
    "slug",
]);
const NAME_CANDIDATE_FIELDS = [
    "name",
    "title",
    "displayName",
];
export class AuditLogService extends AnalyticsDatabaseService {
    constructor(clickhouseDatabase) {
        super({ modelType: AuditLog, database: clickhouseDatabase });
        this.projectSettingsCache = new Map();
        this.userCache = new Map();
    }
    invalidateProjectSettings(projectId) {
        this.projectSettingsCache.delete(projectId.toString());
    }
    async recordCreate(data) {
        var _a;
        try {
            const projectId = this.resolveProjectId(data.model, data.createdItem, data.props);
            if (!projectId) {
                return;
            }
            const settings = await this.getProjectSettings(projectId);
            if (!this.isEligible(settings, data.props)) {
                return;
            }
            const redactedFields = this.getRedactedFields(data.model);
            const changes = this.buildSnapshotChanges({
                model: data.createdItem,
                redactedFields,
                valueKey: "newValue",
            });
            await this.insert({
                projectId,
                resourceType: this.getResourceType(data.model),
                resourceId: (_a = data.createdItem.id) !== null && _a !== void 0 ? _a : null,
                resourceName: this.getResourceName(data.createdItem),
                action: AuditLogAction.Create,
                changes,
                props: data.props,
                retentionInDays: settings.retentionInDays,
            });
        }
        catch (err) {
            logger.warn("AuditLog: failed to record create event");
            logger.warn(err);
        }
    }
    async recordUpdate(data) {
        try {
            const projectId = this.resolveProjectId(data.model, data.before, data.props);
            if (!projectId) {
                return;
            }
            const settings = await this.getProjectSettings(projectId);
            if (!this.isEligible(settings, data.props)) {
                return;
            }
            const redactedFields = this.getRedactedFields(data.model);
            const changes = this.buildUpdateDiff({
                before: data.before,
                updatedFields: data.updatedFields,
                redactedFields,
            });
            if (changes.length === 0) {
                return;
            }
            await this.insert({
                projectId,
                resourceType: this.getResourceType(data.model),
                resourceId: data.itemId,
                resourceName: this.getResourceName(data.before),
                action: AuditLogAction.Update,
                changes,
                props: data.props,
                retentionInDays: settings.retentionInDays,
            });
        }
        catch (err) {
            logger.warn("AuditLog: failed to record update event");
            logger.warn(err);
        }
    }
    async recordDelete(data) {
        try {
            const projectId = this.resolveProjectId(data.model, data.deletedItem, data.props);
            if (!projectId) {
                return;
            }
            const settings = await this.getProjectSettings(projectId);
            if (!this.isEligible(settings, data.props)) {
                return;
            }
            const redactedFields = this.getRedactedFields(data.model);
            const changes = this.buildSnapshotChanges({
                model: data.deletedItem,
                redactedFields,
                valueKey: "oldValue",
            });
            await this.insert({
                projectId,
                resourceType: this.getResourceType(data.model),
                resourceId: data.itemId,
                resourceName: this.getResourceName(data.deletedItem),
                action: AuditLogAction.Delete,
                changes,
                props: data.props,
                retentionInDays: settings.retentionInDays,
            });
        }
        catch (err) {
            logger.warn("AuditLog: failed to record delete event");
            logger.warn(err);
        }
    }
    async insert(params) {
        const auditLog = new AuditLog();
        auditLog.projectId = params.projectId;
        auditLog.resourceType = params.resourceType;
        if (params.resourceId) {
            auditLog.resourceId = params.resourceId;
        }
        if (params.resourceName) {
            auditLog.resourceName = params.resourceName;
        }
        auditLog.action = params.action;
        const actor = await this.resolveActor(params.props);
        if (actor.userId) {
            auditLog.userId = actor.userId;
        }
        if (actor.userName) {
            auditLog.userName = actor.userName;
        }
        if (actor.userEmail) {
            auditLog.userEmail = actor.userEmail;
        }
        if (actor.userType) {
            auditLog.userType = actor.userType;
        }
        auditLog.changes = params.changes;
        auditLog.retentionDate = this.computeRetentionDate(params.retentionInDays);
        await this.create({
            data: auditLog,
            props: { isRoot: true },
        });
    }
    computeRetentionDate(retentionInDays) {
        const days = Math.max(1, Math.min(retentionInDays || 7, 180));
        return OneUptimeDate.addRemoveDays(OneUptimeDate.getCurrentDate(), days);
    }
    isEligible(settings, props) {
        if (!settings) {
            return false;
        }
        if (!settings.enableAuditLogs) {
            return false;
        }
        if (!settings.storeSystemEventsInAuditLogs && this.isSystemEvent(props)) {
            return false;
        }
        if (IsEnterpriseEdition) {
            return true;
        }
        if (IsBillingEnabled) {
            return settings.planName === PlanType.Enterprise;
        }
        /*
         * Neither enterprise edition nor billing is enabled — audit logs are not
         * available on the free self-hosted build.
         */
        return false;
    }
    isSystemEvent(props) {
        /*
         * A system event is one not initiated by a user — no userType is set and
         * the operation is running with root privileges (e.g. background jobs,
         * internal cleanup tasks).
         */
        return !props.userType && Boolean(props.isRoot);
    }
    async getProjectSettings(projectId) {
        var _a;
        const key = projectId.toString();
        const now = Date.now();
        const cached = this.projectSettingsCache.get(key);
        if (cached && cached.expiresAt > now) {
            return cached;
        }
        const project = await ProjectService.findOneById({
            id: projectId,
            select: {
                _id: true,
                enableAuditLogs: true,
                auditLogsRetentionInDays: true,
                storeSystemEventsInAuditLogs: true,
                planName: true,
            },
            props: { isRoot: true },
        });
        if (!project) {
            return null;
        }
        const settings = {
            enableAuditLogs: Boolean(project.enableAuditLogs),
            retentionInDays: (_a = project.auditLogsRetentionInDays) !== null && _a !== void 0 ? _a : 7,
            storeSystemEventsInAuditLogs: Boolean(project.storeSystemEventsInAuditLogs),
            planName: project.planName,
            expiresAt: now + PROJECT_SETTINGS_CACHE_TTL_MS,
        };
        this.projectSettingsCache.set(key, settings);
        return settings;
    }
    async resolveActor(props) {
        var _a, _b;
        const userType = props.userType
            ? String(props.userType)
            : props.isRoot
                ? "System"
                : null;
        if (!props.userId) {
            return {
                userId: null,
                userName: null,
                userEmail: null,
                userType,
            };
        }
        // API-key actions don't have a user record to look up.
        if (props.userType === UserType.API) {
            return {
                userId: props.userId,
                userName: null,
                userEmail: null,
                userType,
            };
        }
        const cached = await this.getUserInfo(props.userId);
        return {
            userId: props.userId,
            userName: (_a = cached === null || cached === void 0 ? void 0 : cached.name) !== null && _a !== void 0 ? _a : null,
            userEmail: (_b = cached === null || cached === void 0 ? void 0 : cached.email) !== null && _b !== void 0 ? _b : null,
            userType,
        };
    }
    async getUserInfo(userId) {
        var _a, _b, _c, _d;
        const key = userId.toString();
        const now = Date.now();
        const cached = this.userCache.get(key);
        if (cached && cached.expiresAt > now) {
            return { name: cached.name, email: cached.email };
        }
        const user = await UserService.findOneById({
            id: userId,
            select: { _id: true, name: true, email: true },
            props: { isRoot: true },
        });
        const name = (_b = (_a = user === null || user === void 0 ? void 0 : user.name) === null || _a === void 0 ? void 0 : _a.toString()) !== null && _b !== void 0 ? _b : null;
        const email = (_d = (_c = user === null || user === void 0 ? void 0 : user.email) === null || _c === void 0 ? void 0 : _c.toString()) !== null && _d !== void 0 ? _d : null;
        this.userCache.set(key, {
            name,
            email,
            expiresAt: now + USER_CACHE_TTL_MS,
        });
        return { name, email };
    }
    resolveProjectId(model, item, props) {
        if (props.tenantId) {
            return props.tenantId;
        }
        const tenantColumn = model.getTenantColumn();
        if (!tenantColumn) {
            return undefined;
        }
        const value = item.getValue(tenantColumn);
        return value !== null && value !== void 0 ? value : undefined;
    }
    getRedactedFields(model) {
        const redacted = new Set();
        const allAccessControl = getColumnAccessControlForAllColumns(model);
        for (const key of Object.keys(allAccessControl)) {
            const ac = allAccessControl[key];
            if (ac && Array.isArray(ac.read) && ac.read.length === 0) {
                redacted.add(key);
            }
        }
        return redacted;
    }
    buildSnapshotChanges(data) {
        const changes = [];
        const columns = data.model.getTableColumns().columns;
        for (const column of columns) {
            if (SKIPPED_FIELDS.has(column)) {
                continue;
            }
            if (data.redactedFields.has(column)) {
                continue;
            }
            const value = data.model[column];
            if (value === undefined) {
                continue;
            }
            changes.push({
                field: column,
                [data.valueKey]: this.serializeValue(value),
            });
        }
        return changes;
    }
    buildUpdateDiff(data) {
        const changes = [];
        for (const field of Object.keys(data.updatedFields)) {
            if (SKIPPED_FIELDS.has(field)) {
                continue;
            }
            if (data.redactedFields.has(field)) {
                continue;
            }
            const newValue = data.updatedFields[field];
            const oldValue = data.before[field];
            if (this.areValuesEqual(oldValue, newValue)) {
                continue;
            }
            changes.push({
                field,
                oldValue: this.serializeValue(oldValue),
                newValue: this.serializeValue(newValue),
            });
        }
        return changes;
    }
    areValuesEqual(a, b) {
        if (a === b) {
            return true;
        }
        if (a === null || a === undefined) {
            return b === null || b === undefined;
        }
        if (b === null || b === undefined) {
            return false;
        }
        try {
            return JSON.stringify(a) === JSON.stringify(b);
        }
        catch (_a) {
            return false;
        }
    }
    serializeValue(value) {
        if (value === null || value === undefined) {
            return null;
        }
        if (value instanceof ObjectID) {
            return value.toString();
        }
        if (value instanceof Date) {
            return value.toISOString();
        }
        if (typeof value === "string" ||
            typeof value === "number" ||
            typeof value === "boolean") {
            return value;
        }
        try {
            return JSON.parse(JSON.stringify(value));
        }
        catch (_a) {
            return String(value);
        }
    }
    getResourceType(model) {
        if (model.singularName) {
            return model.singularName;
        }
        return model.constructor
            .name;
    }
    getResourceName(item) {
        for (const field of NAME_CANDIDATE_FIELDS) {
            const columns = item.getTableColumns().columns;
            if (!columns.includes(field)) {
                continue;
            }
            const value = item[field];
            if (typeof value === "string" && value.length > 0) {
                return value;
            }
        }
        return null;
    }
}
export default new AuditLogService();
//# sourceMappingURL=AuditLogService.js.map