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 UserMiddleware from "../Middleware/UserAuthorization";
import Express from "../Utils/Express";
import Response from "../Utils/Response";
import CommonAPI from "./CommonAPI";
import AnalyticsDataModel from "../../Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel";
import { DEFAULT_LIMIT, LIMIT_PER_PROJECT, } from "../../Types/Database/LimitMax";
import BadRequestException from "../../Types/Exception/BadRequestException";
import JSONFunctions from "../../Types/JSONFunctions";
import ObjectID from "../../Types/ObjectID";
import PositiveNumber from "../../Types/PositiveNumber";
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
import GlobalCache from "../Infrastructure/GlobalCache";
import logger from "../Utils/Logger";
/*
 * Aggregate cache TTL. Dashboards typically auto-refresh every 30s+,
 * so an 8s window collapses bursts of identical requests (e.g. 12
 * widgets loading on the same page) onto a single ClickHouse query
 * while still looking real-time to humans.
 *
 * Project-scoped only: analytics data is project-wide and the
 * service layer enforces project-scoped read permissions, so
 * caching across users within the same project is safe. Endpoints
 * with row-level access scoping should override `getAggregate` to
 * skip the cache (or shape the key to include the access scope).
 */
const ANALYTICS_AGGREGATE_CACHE_TTL_SECONDS = 8;
export default class BaseAnalyticsAPI {
    constructor(type, service) {
        var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
        this.entityType = type;
        const router = Express.getRouter();
        /*
         * BaseAnalyticsAPI only makes sense for models that expose CRUD over
         * HTTP. Internal-only tables (e.g. MV target tables) are constructed
         * with `crudApiPath: undefined` and must never be wired here — fail
         * fast if a caller tries.
         */
        const crudApiPath = (_a = new this.entityType().crudApiPath) === null || _a === void 0 ? void 0 : _a.toString();
        if (!crudApiPath) {
            throw new Error(`BaseAnalyticsAPI cannot be constructed for ${type.name}: model has no crudApiPath.`);
        }
        // Create
        router.post(`${crudApiPath}`, UserMiddleware.getUserMiddleware, async (req, res, next) => {
            try {
                await this.createItem(req, res);
            }
            catch (err) {
                next(err);
            }
        });
        // List
        router.post(`${(_b = new this.entityType().crudApiPath) === null || _b === void 0 ? void 0 : _b.toString()}/get-list`, UserMiddleware.getUserMiddleware, async (req, res, next) => {
            try {
                await this.getList(req, res);
            }
            catch (err) {
                next(err);
            }
        });
        // List
        router.get(`${(_c = new this.entityType().crudApiPath) === null || _c === void 0 ? void 0 : _c.toString()}/get-list`, UserMiddleware.getUserMiddleware, async (req, res, next) => {
            try {
                await this.getList(req, res);
            }
            catch (err) {
                next(err);
            }
        });
        // Aggregate
        router.post(`${(_d = new this.entityType().crudApiPath) === null || _d === void 0 ? void 0 : _d.toString()}/aggregate`, UserMiddleware.getUserMiddleware, async (req, res, next) => {
            try {
                await this.getAggregate(req, res);
            }
            catch (err) {
                next(err);
            }
        });
        // Aggregate
        router.get(`${(_e = new this.entityType().crudApiPath) === null || _e === void 0 ? void 0 : _e.toString()}/aggregate`, UserMiddleware.getUserMiddleware, async (req, res, next) => {
            try {
                await this.getAggregate(req, res);
            }
            catch (err) {
                next(err);
            }
        });
        // count
        router.post(`${(_f = new this.entityType().crudApiPath) === null || _f === void 0 ? void 0 : _f.toString()}/count`, UserMiddleware.getUserMiddleware, async (req, res, next) => {
            try {
                await this.count(req, res);
            }
            catch (err) {
                next(err);
            }
        });
        // Get Item
        router.post(`${(_g = new this.entityType().crudApiPath) === null || _g === void 0 ? void 0 : _g.toString()}/:id/get-item`, UserMiddleware.getUserMiddleware, async (req, res, next) => {
            try {
                await this.getItem(req, res);
            }
            catch (err) {
                next(err);
            }
        });
        // Get Item
        router.get(`${(_h = new this.entityType().crudApiPath) === null || _h === void 0 ? void 0 : _h.toString()}/:id/get-item`, UserMiddleware.getUserMiddleware, async (req, res, next) => {
            try {
                await this.getItem(req, res);
            }
            catch (err) {
                next(err);
            }
        });
        // Update
        router.put(`${(_j = new this.entityType().crudApiPath) === null || _j === void 0 ? void 0 : _j.toString()}/:id`, UserMiddleware.getUserMiddleware, async (req, res, next) => {
            try {
                await this.updateItem(req, res);
            }
            catch (err) {
                next(err);
            }
        });
        // Delete
        router.delete(`${(_k = new this.entityType().crudApiPath) === null || _k === void 0 ? void 0 : _k.toString()}/:id`, UserMiddleware.getUserMiddleware, async (req, res, next) => {
            try {
                await this.deleteItem(req, res);
            }
            catch (err) {
                next(err);
            }
        });
        this.router = router;
        this.service = service;
    }
    async getPermissionsForTenant(req) {
        var _a, _b, _c;
        const permissions = [];
        const props = await CommonAPI.getDatabaseCommonInteractionProps(req);
        if (props &&
            props.userTenantAccessPermission &&
            props.userTenantAccessPermission[((_a = props.tenantId) === null || _a === void 0 ? void 0 : _a.toString()) || ""]) {
            return (((_c = props.userTenantAccessPermission[((_b = props.tenantId) === null || _b === void 0 ? void 0 : _b.toString()) || ""]) === null || _c === void 0 ? void 0 : _c.permissions) || []);
        }
        return permissions;
    }
    getTenantId(req) {
        if (req.tenantId) {
            return req.tenantId;
        }
        return null;
    }
    async getList(req, res) {
        await this.onBeforeList(req, res);
        const skip = req.query["skip"]
            ? new PositiveNumber(req.query["skip"])
            : new PositiveNumber(0);
        const limit = req.query["limit"]
            ? new PositiveNumber(req.query["limit"])
            : new PositiveNumber(DEFAULT_LIMIT);
        if (limit.toNumber() > LIMIT_PER_PROJECT) {
            throw new BadRequestException("Limit should be less than " + LIMIT_PER_PROJECT);
        }
        let query = {};
        let select = {};
        let sort = {};
        let groupBy = {};
        if (req.body) {
            query = JSONFunctions.deserialize(req.body["query"]);
            select = JSONFunctions.deserialize(req.body["select"]);
            sort = JSONFunctions.deserialize(req.body["sort"]);
            groupBy = JSONFunctions.deserialize(req.body["groupBy"]);
        }
        const databaseProps = await CommonAPI.getDatabaseCommonInteractionProps(req);
        /*
         * Skip the parallel countBy on analytics tables. countBy on Log /
         * Span / Metric over wide time ranges scans every matching block
         * (no LIMIT) and routinely dominates list-endpoint latency under
         * heavy ingest. Instead we over-fetch by one row and derive
         * `hasMore` from whether the extra row showed up. `count` is
         * emitted as a lower bound (`skip + data.length + hasMore`) so
         * older clients that read `count` keep rendering something
         * sensible while newer clients use `hasMore` for prev/next.
         */
        const overfetchLimit = new PositiveNumber(limit.toNumber() + 1);
        const list = await this.service.findBy({
            query,
            select,
            skip: skip,
            limit: overfetchLimit,
            sort: sort,
            groupBy: groupBy,
            props: databaseProps,
        });
        const hasMore = list.length > limit.toNumber();
        if (hasMore) {
            list.length = limit.toNumber();
        }
        const lowerBoundCount = new PositiveNumber(skip.toNumber() + list.length + (hasMore ? 1 : 0));
        return Response.sendEntityArrayResponse(req, res, list, lowerBoundCount, this.entityType, { hasMore });
    }
    async getAggregate(req, res) {
        var _a;
        await this.onBeforeList(req, res);
        let aggregateBy = null;
        if (req.body && req.body["aggregateBy"]) {
            aggregateBy = JSONFunctions.deserialize(req.body["aggregateBy"]);
        }
        let groupBy = req.body["groupBy"] || null;
        if (groupBy && Object.keys(groupBy).length > 0) {
            groupBy = JSONFunctions.deserialize(groupBy);
        }
        if (groupBy && Object.keys(groupBy).length === 0) {
            groupBy = null;
        }
        if (!aggregateBy) {
            throw new BadRequestException("AggregateBy is required");
        }
        const databaseProps = await CommonAPI.getDatabaseCommonInteractionProps(req);
        /*
         * Short-lived project-scoped cache. A dashboard refresh fires
         * one /aggregate call per widget — typically 10+ identical or
         * near-identical aggregations against the same time window
         * inside a few hundred milliseconds. Cache the result for 8s
         * so the underlying ClickHouse aggregation runs once per
         * burst. On cache outage (Redis down, parse error, …) we fall
         * through to a live query so behavior degrades to today's.
         */
        const projectId = (_a = databaseProps.tenantId) === null || _a === void 0 ? void 0 : _a.toString();
        const cacheNamespace = `${this.getEntityName()}-aggregate`;
        const cacheKey = projectId
            ? `${projectId}:${this.buildAggregateCacheKey(aggregateBy)}`
            : null;
        if (cacheKey) {
            try {
                const cached = await GlobalCache.getJSONObject(cacheNamespace, cacheKey);
                if (cached) {
                    return Response.sendJsonObjectResponse(req, res, cached);
                }
            }
            catch (err) {
                logger.debug(`${cacheNamespace} cache read failed`);
                logger.debug(err);
            }
        }
        const aggregateResult = await this.service.aggregateBy(Object.assign(Object.assign({}, aggregateBy), { props: databaseProps }));
        const responseBody = Object.assign({}, aggregateResult);
        if (cacheKey) {
            try {
                await GlobalCache.setJSON(cacheNamespace, cacheKey, responseBody, {
                    expiresInSeconds: ANALYTICS_AGGREGATE_CACHE_TTL_SECONDS,
                });
            }
            catch (err) {
                logger.debug(`${cacheNamespace} cache write failed`);
                logger.debug(err);
            }
        }
        return Response.sendJsonObjectResponse(req, res, responseBody);
    }
    /*
     * Stable serialization for the aggregate cache key. Date instances
     * are normalized to ISO so two logically-equal time windows hit
     * the same cache slot, and we sort object keys so the ordering is
     * deterministic across clients and across V8 versions.
     */
    buildAggregateCacheKey(aggregateBy) {
        return JSON.stringify(aggregateBy, (_key, value) => {
            if (value instanceof Date) {
                return value.toISOString();
            }
            if (value &&
                typeof value === "object" &&
                !Array.isArray(value) &&
                value.constructor === Object) {
                const sorted = {};
                for (const k of Object.keys(value).sort()) {
                    sorted[k] = value[k];
                }
                return sorted;
            }
            return value;
        });
    }
    async count(req, res) {
        let query = {};
        await this.onBeforeCount(req, res);
        if (req.body) {
            query = JSONFunctions.deserialize(req.body["query"]);
        }
        const databaseProps = await CommonAPI.getDatabaseCommonInteractionProps(req);
        const count = await this.service.countBy({
            query,
            props: databaseProps,
        });
        return Response.sendJsonObjectResponse(req, res, {
            count: count.toNumber(),
        });
    }
    async getItem(req, res) {
        const objectId = new ObjectID(req.params["id"]);
        await this.onBeforeGet(req, res);
        let select = {};
        if (req.body) {
            select = JSONFunctions.deserialize(req.body["select"]);
        }
        const item = await this.service.findOneById({
            id: objectId,
            select,
            props: await CommonAPI.getDatabaseCommonInteractionProps(req),
        });
        return Response.sendEntityResponse(req, res, item, this.entityType);
    }
    async deleteItem(req, res) {
        await this.onBeforeDelete(req, res);
        const objectId = new ObjectID(req.params["id"]);
        await this.service.deleteBy({
            query: {
                _id: objectId.toString(),
            },
            props: await CommonAPI.getDatabaseCommonInteractionProps(req),
        });
        return Response.sendEmptySuccessResponse(req, res);
    }
    async updateItem(req, res) {
        await this.onBeforeUpdate(req, res);
        const objectId = new ObjectID(req.params["id"]);
        const objectIdString = objectId.toString();
        const body = req.body;
        const item = AnalyticsDataModel.fromJSON(body["data"], this.entityType);
        delete item["_id"];
        delete item["createdAt"];
        delete item["updatedAt"];
        await this.service.updateBy({
            query: {
                _id: objectIdString,
            },
            data: item,
            props: await CommonAPI.getDatabaseCommonInteractionProps(req),
        });
        return Response.sendEmptySuccessResponse(req, res);
    }
    async createItem(req, res) {
        await this.onBeforeCreate(req, res);
        const body = req.body;
        const item = AnalyticsDataModel.fromJSON(body["data"], this.entityType);
        const createBy = {
            data: item,
            props: await CommonAPI.getDatabaseCommonInteractionProps(req),
        };
        const savedItem = await this.service.create(createBy);
        return Response.sendEntityResponse(req, res, savedItem, this.entityType);
    }
    getRouter() {
        return this.router;
    }
    getEntityName() {
        return this.entityType.name;
    }
    async onBeforeList(_req, _res) {
        return Promise.resolve(true);
    }
    async onBeforeCreate(_req, _res) {
        return Promise.resolve(true);
    }
    async onBeforeGet(_req, _res) {
        return Promise.resolve(true);
    }
    async onBeforeUpdate(_req, _res) {
        return Promise.resolve(true);
    }
    async onBeforeDelete(_req, _res) {
        return Promise.resolve(true);
    }
    async onBeforeCount(_req, _res) {
        return Promise.resolve(true);
    }
}
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object]),
    __metadata("design:returntype", Promise)
], BaseAnalyticsAPI.prototype, "getPermissionsForTenant", null);
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object, Object]),
    __metadata("design:returntype", Promise)
], BaseAnalyticsAPI.prototype, "getList", null);
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object, Object]),
    __metadata("design:returntype", Promise)
], BaseAnalyticsAPI.prototype, "getAggregate", null);
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object, Object]),
    __metadata("design:returntype", Promise)
], BaseAnalyticsAPI.prototype, "count", null);
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object, Object]),
    __metadata("design:returntype", Promise)
], BaseAnalyticsAPI.prototype, "getItem", null);
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object, Object]),
    __metadata("design:returntype", Promise)
], BaseAnalyticsAPI.prototype, "deleteItem", null);
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object, Object]),
    __metadata("design:returntype", Promise)
], BaseAnalyticsAPI.prototype, "updateItem", null);
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object, Object]),
    __metadata("design:returntype", Promise)
], BaseAnalyticsAPI.prototype, "createItem", null);
//# sourceMappingURL=BaseAnalyticsAPI.js.map