var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
import { MeteredPlanUtil } from "../Types/Billing/MeteredPlan/AllMeteredPlans";
import DatabaseService from "./DatabaseService";
import SortOrder from "../../Types/BaseDatabase/SortOrder";
import LIMIT_MAX from "../../Types/Database/LimitMax";
import OneUptimeDate from "../../Types/Date";
import Decimal from "../../Types/Decimal";
import BadDataException from "../../Types/Exception/BadDataException";
import ProductType from "../../Types/MeteredPlan/ProductType";
import ObjectID from "../../Types/ObjectID";
import Model, { DEFAULT_RETENTION_IN_DAYS, } from "../../Models/DatabaseModels/TelemetryUsageBilling";
import ServiceService from "./ServiceService";
import ProjectService from "./ProjectService";
import SpanService from "./SpanService";
import LogService from "./LogService";
import MetricService from "./MetricService";
import ExceptionInstanceService from "./ExceptionInstanceService";
import ProfileService from "./ProfileService";
import ProfileSampleService from "./ProfileSampleService";
import DiskSize from "../../Types/DiskSize";
import logger from "../Utils/Logger";
import HostService from "./HostService";
import DockerHostService from "./DockerHostService";
import KubernetesClusterService from "./KubernetesClusterService";
import ServiceType from "../../Types/Telemetry/ServiceType";
import { AverageSpanRowSizeInBytes, AverageLogRowSizeInBytes, AverageMetricRowSizeInBytes, AverageExceptionRowSizeInBytes, AverageProfileRowSizeInBytes, AverageProfileSampleRowSizeInBytes, IsBillingEnabled, } from "../EnvironmentConfig";
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
export class Service extends DatabaseService {
    constructor() {
        super(Model);
        if (IsBillingEnabled) {
            this.hardDeleteItemsOlderThanInDays("createdAt", 120);
        }
    }
    async getUnreportedUsageBilling(data) {
        return await this.findBy({
            query: {
                projectId: data.projectId,
                productType: data.productType,
                isReportedToBillingProvider: false,
            },
            skip: 0,
            limit: LIMIT_MAX, /// because a project can have MANY telemetry services.
            select: {
                _id: true,
                totalCostInUSD: true,
            },
            props: {
                isRoot: true,
            },
        });
    }
    async stageTelemetryUsageForProject(data) {
        var _a, _b;
        if (!IsBillingEnabled) {
            return;
        }
        const usageDate = data.usageDate
            ? OneUptimeDate.fromString(data.usageDate)
            : OneUptimeDate.addRemoveDays(OneUptimeDate.getCurrentDate(), -1);
        const averageRowSizeInBytes = this.getAverageRowSizeForProduct(data.productType);
        const averageExceptionRowSizeInBytes = this.getAverageExceptionRowSize();
        const averageProfileSampleRowSizeInBytes = this.getAverageProfileSampleRowSize();
        if (data.productType !== ProductType.Traces &&
            data.productType !== ProductType.Profiles &&
            averageRowSizeInBytes <= 0) {
            return;
        }
        if (data.productType === ProductType.Traces &&
            averageRowSizeInBytes <= 0 &&
            averageExceptionRowSizeInBytes <= 0) {
            return;
        }
        if (data.productType === ProductType.Profiles &&
            averageRowSizeInBytes <= 0 &&
            averageProfileSampleRowSizeInBytes <= 0) {
            return;
        }
        const usageDayString = OneUptimeDate.getDateString(usageDate);
        const startOfDay = OneUptimeDate.getStartOfDay(usageDate);
        const endOfDay = OneUptimeDate.getEndOfDay(usageDate);
        const usageByServiceId = new Map();
        const addUsage = (rows, fallbackRowSizeInBytes) => {
            for (const row of rows) {
                const bytes = row.estimatedBytes > 0
                    ? row.estimatedBytes
                    : row.rowCount * fallbackRowSizeInBytes;
                const existing = usageByServiceId.get(row.primaryEntityId);
                if (existing) {
                    existing.bytes += bytes;
                    if (!existing.primaryEntityType && row.primaryEntityType) {
                        existing.primaryEntityType = row.primaryEntityType;
                    }
                }
                else {
                    usageByServiceId.set(row.primaryEntityId, {
                        primaryEntityType: row.primaryEntityType,
                        bytes,
                    });
                }
            }
        };
        try {
            if (data.productType === ProductType.Traces) {
                addUsage(await SpanService.groupTelemetryUsageByService({
                    projectId: data.projectId,
                    timestampColumnName: "startTime",
                    startDate: startOfDay,
                    endDate: endOfDay,
                }), averageRowSizeInBytes);
                addUsage(await ExceptionInstanceService.groupTelemetryUsageByService({
                    projectId: data.projectId,
                    timestampColumnName: "time",
                    startDate: startOfDay,
                    endDate: endOfDay,
                }), averageExceptionRowSizeInBytes);
            }
            else if (data.productType === ProductType.Logs) {
                addUsage(await LogService.groupTelemetryUsageByService({
                    projectId: data.projectId,
                    timestampColumnName: "time",
                    startDate: startOfDay,
                    endDate: endOfDay,
                }), averageRowSizeInBytes);
            }
            else if (data.productType === ProductType.Metrics) {
                addUsage(await MetricService.groupTelemetryUsageByService({
                    projectId: data.projectId,
                    timestampColumnName: "time",
                    startDate: startOfDay,
                    endDate: endOfDay,
                }), averageRowSizeInBytes);
            }
            else if (data.productType === ProductType.Profiles) {
                addUsage(await ProfileService.groupTelemetryUsageByService({
                    projectId: data.projectId,
                    timestampColumnName: "startTime",
                    startDate: startOfDay,
                    endDate: endOfDay,
                }), averageRowSizeInBytes);
                addUsage(await ProfileSampleService.groupTelemetryUsageByService({
                    projectId: data.projectId,
                    timestampColumnName: "time",
                    startDate: startOfDay,
                    endDate: endOfDay,
                }), averageProfileSampleRowSizeInBytes);
            }
        }
        catch (error) {
            logger.error(`Failed to aggregate telemetry usage for project ${data.projectId.toString()} (${data.productType}):`);
            logger.error(error);
            /*
             * Rethrow so the failure is loud: nothing has been staged for this
             * day yet (staging below is skipped entirely), so a later retry of
             * the cron can re-stage the full day. Swallowing here would
             * silently bill the day as zero.
             */
            throw error;
        }
        if (usageByServiceId.size === 0) {
            return;
        }
        /*
         * Resolve retention per resource (Service / Host / DockerHost /
         * KubernetesCluster overrides), defaulting to the project default for
         * everything else — including the unattributed bucket. Retention
         * scales the billed cost, so we honor per-resource overrides.
         */
        const retentionByServiceId = await this.buildTelemetryRetentionMap(data.projectId);
        const projectDefaultRetentionInDays = await this.getProjectDefaultRetentionInDays(data.projectId);
        /*
         * Skip serviceIds already staged for this day so re-runs of the cron
         * don't double-count (updateUsageBilling accumulates into the day's
         * row). One query instead of a findOneBy per primaryEntityId.
         */
        const alreadyStaged = await this.findBy({
            query: {
                projectId: data.projectId,
                productType: data.productType,
                day: usageDayString,
            },
            select: {
                primaryEntityId: true,
            },
            skip: 0,
            limit: LIMIT_MAX,
            props: {
                isRoot: true,
            },
        });
        const stagedServiceIds = new Set(alreadyStaged
            .map((row) => {
            var _a;
            return (_a = row.primaryEntityId) === null || _a === void 0 ? void 0 : _a.toString();
        })
            .filter((id) => {
            return Boolean(id);
        }));
        for (const [serviceIdStr, usage] of usageByServiceId) {
            /*
             * Monitor telemetry is billed via the Active Monitoring plan, and
             * Alert/Incident telemetry is OneUptime's own operational data —
             * neither is charged as ingested telemetry volume.
             */
            if (usage.primaryEntityType === ServiceType.Monitor ||
                usage.primaryEntityType === ServiceType.Alert ||
                usage.primaryEntityType === ServiceType.Incident) {
                continue;
            }
            if (stagedServiceIds.has(serviceIdStr)) {
                continue;
            }
            const estimatedGigabytes = DiskSize.byteSizeToGB(usage.bytes);
            if (!Number.isFinite(estimatedGigabytes) || estimatedGigabytes <= 0) {
                continue;
            }
            const retentionInDays = (_a = retentionByServiceId.get(serviceIdStr)) !== null && _a !== void 0 ? _a : projectDefaultRetentionInDays;
            /*
             * Legacy rows (pre-discriminator) and any null primaryEntityType are
             * treated as OpenTelemetry — historically every billed primaryEntityId was
             * a real Service.
             */
            const primaryEntityType = (_b = usage.primaryEntityType) !== null && _b !== void 0 ? _b : ServiceType.OpenTelemetry;
            await this.updateUsageBilling({
                projectId: data.projectId,
                productType: data.productType,
                primaryEntityId: new ObjectID(serviceIdStr),
                primaryEntityType: primaryEntityType,
                dataIngestedInGB: estimatedGigabytes,
                retentionInDays: retentionInDays,
                usageDate: usageDate,
            });
        }
    }
    /*
     * Map of resourceId -> retainTelemetryDataForDays for every resource in
     * the project that can own telemetry (Service, Host, DockerHost,
     * KubernetesCluster). Used to scale billed cost by the actual retention
     * applied to each resource's telemetry. Resources without an override
     * (and the unattributed bucket) fall back to the project default.
     */
    async buildTelemetryRetentionMap(projectId) {
        const retentionByServiceId = new Map();
        const services = await ServiceService.findBy({
            query: { projectId: projectId },
            select: { _id: true, retainTelemetryDataForDays: true },
            skip: 0,
            limit: LIMIT_MAX,
            props: { isRoot: true },
        });
        for (const service of services) {
            if (service.id && service.retainTelemetryDataForDays) {
                retentionByServiceId.set(service.id.toString(), service.retainTelemetryDataForDays);
            }
        }
        const hosts = await HostService.findBy({
            query: { projectId: projectId },
            select: { _id: true, retainTelemetryDataForDays: true },
            skip: 0,
            limit: LIMIT_MAX,
            props: { isRoot: true },
        });
        for (const host of hosts) {
            if (host.id && host.retainTelemetryDataForDays) {
                retentionByServiceId.set(host.id.toString(), host.retainTelemetryDataForDays);
            }
        }
        const dockerHosts = await DockerHostService.findBy({
            query: { projectId: projectId },
            select: { _id: true, retainTelemetryDataForDays: true },
            skip: 0,
            limit: LIMIT_MAX,
            props: { isRoot: true },
        });
        for (const dockerHost of dockerHosts) {
            if (dockerHost.id && dockerHost.retainTelemetryDataForDays) {
                retentionByServiceId.set(dockerHost.id.toString(), dockerHost.retainTelemetryDataForDays);
            }
        }
        const clusters = await KubernetesClusterService.findBy({
            query: { projectId: projectId },
            select: { _id: true, retainTelemetryDataForDays: true },
            skip: 0,
            limit: LIMIT_MAX,
            props: { isRoot: true },
        });
        for (const cluster of clusters) {
            if (cluster.id && cluster.retainTelemetryDataForDays) {
                retentionByServiceId.set(cluster.id.toString(), cluster.retainTelemetryDataForDays);
            }
        }
        return retentionByServiceId;
    }
    async getProjectDefaultRetentionInDays(projectId) {
        const project = await ProjectService.findOneById({
            id: projectId,
            select: { defaultTelemetryRetentionInDays: true },
            props: { isRoot: true },
        });
        return ((project === null || project === void 0 ? void 0 : project.defaultTelemetryRetentionInDays) || DEFAULT_RETENTION_IN_DAYS);
    }
    async updateUsageBilling(data) {
        var _a, _b;
        if (data.productType !== ProductType.Traces &&
            data.productType !== ProductType.Metrics &&
            data.productType !== ProductType.Logs &&
            data.productType !== ProductType.Profiles) {
            throw new BadDataException("This product type is not a telemetry product type.");
        }
        const serverMeteredPlan = MeteredPlanUtil.getMeteredPlanByProductType(data.productType);
        const usageDate = data.usageDate
            ? OneUptimeDate.fromString(data.usageDate)
            : OneUptimeDate.getCurrentDate();
        const usageDayString = OneUptimeDate.getDateString(usageDate);
        const totalCostOfThisOperationInUSD = serverMeteredPlan.getTotalCostInUSD({
            dataIngestedInGB: data.dataIngestedInGB,
            retentionInDays: data.retentionInDays,
        });
        const usageBilling = await this.findOneBy({
            query: {
                projectId: data.projectId,
                productType: data.productType,
                primaryEntityId: data.primaryEntityId,
                isReportedToBillingProvider: false,
                day: usageDayString,
            },
            select: {
                _id: true,
                dataIngestedInGB: true,
                totalCostInUSD: true,
            },
            props: {
                isRoot: true,
            },
            sort: {
                createdAt: SortOrder.Descending,
            },
        });
        if (usageBilling && usageBilling.id) {
            let totalCostInUSD = ((_a = usageBilling.totalCostInUSD) === null || _a === void 0 ? void 0 : _a.value) || 0;
            if (isNaN(totalCostInUSD) ||
                totalCostInUSD === undefined ||
                totalCostInUSD === null ||
                (typeof totalCostInUSD === "string" && totalCostInUSD === "NaN")) {
                totalCostInUSD = 0;
            }
            await this.updateOneById({
                id: usageBilling.id,
                data: {
                    dataIngestedInGB: new Decimal((((_b = usageBilling.dataIngestedInGB) === null || _b === void 0 ? void 0 : _b.value) || 0) + data.dataIngestedInGB),
                    totalCostInUSD: new Decimal(totalCostInUSD + totalCostOfThisOperationInUSD),
                    retainTelemetryDataForDays: data.retentionInDays,
                },
                props: {
                    isRoot: true,
                },
            });
        }
        else {
            const usageBilling = new Model();
            usageBilling.projectId = data.projectId;
            usageBilling.productType = data.productType;
            usageBilling.dataIngestedInGB = new Decimal(data.dataIngestedInGB);
            usageBilling.primaryEntityId = data.primaryEntityId;
            if (data.primaryEntityType) {
                usageBilling.primaryEntityType = data.primaryEntityType;
            }
            usageBilling.retainTelemetryDataForDays = data.retentionInDays;
            usageBilling.isReportedToBillingProvider = false;
            usageBilling.createdAt = usageDate;
            usageBilling.day = usageDayString;
            usageBilling.totalCostInUSD = new Decimal(totalCostOfThisOperationInUSD);
            await this.create({
                data: usageBilling,
                props: {
                    isRoot: true,
                },
            });
        }
    }
    getAverageRowSizeForProduct(productType) {
        var _a;
        const fallbackSize = 1024;
        // Narrow to telemetry product types before indexing to satisfy TypeScript
        if (productType !== ProductType.Traces &&
            productType !== ProductType.Logs &&
            productType !== ProductType.Metrics &&
            productType !== ProductType.Profiles) {
            return fallbackSize;
        }
        const value = (_a = {
            [ProductType.Traces]: AverageSpanRowSizeInBytes,
            [ProductType.Logs]: AverageLogRowSizeInBytes,
            [ProductType.Metrics]: AverageMetricRowSizeInBytes,
            [ProductType.Profiles]: AverageProfileRowSizeInBytes,
        }[productType]) !== null && _a !== void 0 ? _a : fallbackSize;
        if (!Number.isFinite(value) || value <= 0) {
            return fallbackSize;
        }
        return value;
    }
    getAverageExceptionRowSize() {
        const fallbackSize = 1024;
        if (!Number.isFinite(AverageExceptionRowSizeInBytes)) {
            return fallbackSize;
        }
        if (AverageExceptionRowSizeInBytes <= 0) {
            return fallbackSize;
        }
        return AverageExceptionRowSizeInBytes;
    }
    getAverageProfileSampleRowSize() {
        const fallbackSize = 512;
        if (!Number.isFinite(AverageProfileSampleRowSizeInBytes)) {
            return fallbackSize;
        }
        if (AverageProfileSampleRowSizeInBytes <= 0) {
            return fallbackSize;
        }
        return AverageProfileSampleRowSizeInBytes;
    }
}
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object]),
    __metadata("design:returntype", Promise)
], Service.prototype, "getUnreportedUsageBilling", null);
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object]),
    __metadata("design:returntype", Promise)
], Service.prototype, "stageTelemetryUsageForProject", null);
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [ObjectID]),
    __metadata("design:returntype", Promise)
], Service.prototype, "buildTelemetryRetentionMap", null);
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [ObjectID]),
    __metadata("design:returntype", Promise)
], Service.prototype, "getProjectDefaultRetentionInDays", null);
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object]),
    __metadata("design:returntype", Promise)
], Service.prototype, "updateUsageBilling", null);
export default new Service();
//# sourceMappingURL=TelemetryUsageBillingService.js.map