import ky from "ky";
import io from "socket.io-client";
import dayjs from "dayjs";
import { keys, isEmpty, values, isNil } from "lodash-es";
import { createNewStore, getStore, removeStore, updateStore, } from "../../store";
import { getLocale, i18n, } from "../../utils/i18n";
export class LsData {
    constructor() {
        /**
         * The URL for the API endpoint.
         */
        this.apiUrl = "https://api-gcp-prod.liftstatus.nl";
        this.store = null;
        /**
         * The computed idKey used for the store. Cached to allow cleanup in disconnectedCallback.
         */
        this.computedIdKey = null;
        /**
         * The list of relevant sensors that is used when performing API requests.
         */
        this.availableSensorIds = [
            10, 23, 104, 105, 106, 210, 212, 500, 501, 502, 503, 513, 514, 515, 524,
            550, 590, 591,
        ];
    }
    syncStoreLocaleFromLanguage(newLanguage) {
        if (this.store === null)
            return;
        this.store.set("locale", getLocale(newLanguage));
    }
    async componentWillLoad() {
        var _a;
        // Check for a valid access token and if so decode it
        const { valid, objectId, objectType } = await this.decodeAccessToken(this.accessToken);
        if (!valid)
            return;
        // Check if the installation has a back door
        const hasBackDoorPromise = this.checkHasBackdoor(objectType, objectId, this.accessToken);
        const mergedSensorIds = [
            // Remove duplicates using a Set
            ...new Set(this.availableSensorIds.concat(this.sensorIds !== undefined
                ? this.sensorIds
                    .split(",")
                    .filter((id) => id.trim() !== "")
                    .map((id) => parseInt(id.trim(), 10))
                : [])),
        ];
        // Get the historical data from the past hour
        const historicalDataPastHourPromise = this.getHistoricalData(dayjs().subtract(1, "hour").format(), dayjs().format(), mergedSensorIds, objectType, objectId, this.accessToken);
        // Connect to the socket server
        const connectedPromise = this.connectToSocketServer(this.accessToken);
        // We stored all the promises, so now we can fire them concurrently
        const [hasBackDoor, historicalDataPastHour, { connected, socket }] = await Promise.all([
            hasBackDoorPromise,
            historicalDataPastHourPromise,
            connectedPromise,
        ]);
        // If something went wrong an error should already be thrown, so simply return here
        if (isNil(hasBackDoor) || isNil(historicalDataPastHour) || !connected) {
            return;
        }
        // We always need a key to identify the store data, so if no key is provided we create one ourselves
        const idKey = (_a = this.idKey) !== null && _a !== void 0 ? _a : `${objectType}-${objectId}`;
        // Store the computed idKey for cleanup in disconnectedCallback
        this.computedIdKey = idKey;
        const currentSensorData = this.getCurrentDataFromHistorical(historicalDataPastHour);
        // Save state in store
        createNewStore(idKey, {
            hasBackDoor,
            historicalSensorData: historicalDataPastHour,
            currentSensorData,
            locale: getLocale(this.language),
        });
        try {
            this.store = await getStore(idKey);
        }
        catch (error) {
            this.throwError("STORE_CREATION_FAILED", "Token", error instanceof Error ? error.message : String(error));
        }
        if (currentSensorData.values[10] === 0) {
            this.throwError("SENSECLOUD_CONNECTION_FAILED", "Sensecloud");
        }
        else if (currentSensorData.values[524] === 0) {
            this.throwError("CONTROLLER_CONNECTION_FAILED", "Controller");
        }
        // Listen to future realtime updates
        this.listenForSensorUpdates(socket, idKey, mergedSensorIds);
    }
    /**
     * Checks for valid access token and decodes it
     * @param accessToken
     * @returns \{ valid: true, objectId, objectType \} if the token is valid
     * @returns \{ valid: false \}  if the token is invalid
     */
    async decodeAccessToken(accessToken) {
        if (isEmpty(accessToken)) {
            this.throwError("NO_ACCESS_TOKEN", "Token", "Add the token to the <ls-data> component like so: <ls-data access-token='yourToken'></ls-data>");
            return {
                valid: false,
            };
        }
        try {
            const response = await ky
                .post(`${this.apiUrl}/liftstatus/auth`, {
                headers: { Authorization: `Bearer ${accessToken}` },
                json: { accessToken },
                throwHttpErrors: true,
            })
                .json();
            const objectId = response.tokenData.oid;
            let objectType;
            switch (response.tokenData.otype) {
                case "int":
                    objectType = "internal";
                    break;
                case "inst":
                    objectType = "installation";
                    break;
                case "ext":
                    objectType = "external";
                    break;
            }
            return {
                valid: true,
                objectId,
                objectType,
            };
        }
        catch (error) {
            this.throwError("INVALID_ACCESS_TOKEN", "Token", error instanceof Error ? error.message : String(error));
            return {
                valid: false,
            };
        }
    }
    /**
     * This function checks if the elevator has a back door.
     * It does this by checking for activity on sensor 517 for the past week.
     */
    async checkHasBackdoor(objectType, objectId, accessToken) {
        var _a, _b;
        const fromTS = dayjs().subtract(7, "day").format();
        try {
            const response = await ky
                .get(`${this.apiUrl}/liftstatus/${objectType}/${objectId}/sensor/517/summary`, {
                headers: { Authorization: `Bearer ${accessToken}` },
                searchParams: { from: fromTS },
                throwHttpErrors: true,
                timeout: 60000,
            })
                .json();
            // Without any rows there is definitely no back door
            if (response.rows.length === 0)
                return false;
            const start = (_a = response.rows[0]) === null || _a === void 0 ? void 0 : _a.start;
            const end = (_b = response.rows[0]) === null || _b === void 0 ? void 0 : _b.end;
            return end - start === 0 ? false : true;
        }
        catch (error) {
            this.throwError("BACK_DOOR_CHECK_FAILED", "API", error instanceof Error ? error.message : String(error));
        }
    }
    /**
     * This function retrieves historical data from the API server for a specified amount of time.
     * @param {string} fromTS A formatted Day.js object.
     * @param {string} toTS A formatted Day.js object.
     */
    async getHistoricalData(fromTS, toTS, sensorList, objectType, objectId, accessToken) {
        try {
            const response = await ky
                .post(`${this.apiUrl}/liftstatus/${objectType}/${objectId}/sensor`, {
                headers: { Authorization: `Bearer ${accessToken}` },
                searchParams: {
                    from: fromTS,
                    to: toTS,
                    sensors: sensorList.map(String).join(),
                },
                throwHttpErrors: true,
                timeout: 60000,
            })
                .json();
            return response;
        }
        catch (error) {
            this.throwError("HISTORIC_DATA_FAILED", "API", error instanceof Error ? error.message : String(error));
        }
    }
    connectToSocketServer(accessToken) {
        // Close any existing connections
        if (!isEmpty(this.socketId)) {
            this.closeSocketConnection();
        }
        return new Promise((resolve) => {
            this.socket = io(this.apiUrl, {
                transports: ["websocket"],
                path: "/liftstatus/socket.io",
                reconnectionAttempts: 5,
                query: {
                    access_token: `${accessToken}`,
                },
            });
            // Wait until the io function returns a connection
            this.socket.on("connect", () => {
                resolve({
                    connected: true,
                    socket: this.socket,
                });
            });
            // Listen for any errors
            this.socket.on("error", (error) => {
                this.throwError("SOCKET_CONNECTION_FAILED", "Socket", error, this.socket);
                resolve({ connected: false });
            });
        });
    }
    listenForSensorUpdates(socket, idKey, sensorIds) {
        // Responds with live updates from the socket server
        socket.on("module", ({ message }) => {
            // Update data values and timestamp
            const data = message.data;
            const dataKey = keys(data)[0];
            const dataValue = values(data)[0];
            const timestamp = message.ts;
            if (dataKey === "10" && dataValue === 0) {
                this.throwError("SENSECLOUD_CONNECTION_FAILED", "Sensecloud");
            }
            else if (dataKey === "524" && dataValue === 0) {
                this.throwError("CONTROLLER_CONNECTION_FAILED", "Controller");
            }
            else if ((dataKey === LsData.SENSOR_ID_S10 ||
                dataKey === LsData.SENSOR_ID_S524) &&
                dataValue > 0) {
                const currentValues = this.store.state.currentSensorData.values;
                // Check if the OTHER sensor is also healthy
                if (dataKey === LsData.SENSOR_ID_S10 &&
                    currentValues[LsData.SENSOR_ID_S524] > 0) {
                    this.resetError();
                }
                else if (dataKey === LsData.SENSOR_ID_S524 &&
                    currentValues[LsData.SENSOR_ID_S10] > 0) {
                    this.resetError();
                }
            }
            // Check if the sensor id is in the list of sensors we want to listen to
            if (!sensorIds.includes(parseInt(dataKey, 10)) &&
                ![LsData.SENSOR_ID_S10, LsData.SENSOR_ID_S524].includes(dataKey))
                return;
            // Overwrite only the updated values
            updateStore({
                lastUpdate: timestamp,
                values: Object.assign(Object.assign({}, this.store.state.currentSensorData.values), { [dataKey]: dataValue }),
                updated: Object.assign(Object.assign({}, this.store.state.currentSensorData.updated), { [dataKey]: timestamp }),
            }, idKey);
        });
        // Listen to socket server updates (this triggers the onModule updates)
        socket.emit("listen", {});
    }
    getCurrentDataFromHistorical(historicalData) {
        let lastUpdate = 0;
        const values = {};
        const updated = {};
        historicalData.forEach((sensorRow) => {
            const sensorId = sensorRow.sensor.id;
            // Skip sensors with no data rows
            if (sensorRow.rows.length === 0)
                return;
            const lastRow = sensorRow.rows[sensorRow.rows.length - 1];
            const timestamp = dayjs(lastRow.ts).valueOf();
            // Track the most recent timestamp across all sensors
            if (timestamp > lastUpdate) {
                lastUpdate = timestamp;
            }
            // Parse value as number if possible, otherwise keep as string
            const numericValue = parseInt(lastRow.val, 10);
            values[sensorId] = isFinite(numericValue) ? numericValue : lastRow.val;
            updated[sensorId] = timestamp;
        });
        return {
            lastUpdate,
            values,
            updated,
        };
    }
    async throwError(errorKey, errorType, errorDescription, additionalInfo) {
        var _a, _b, _c;
        const locale = getLocale((_b = (_a = this.store) === null || _a === void 0 ? void 0 : _a.state.locale) !== null && _b !== void 0 ? _b : this.language);
        const errorMessage = i18n.t(`errors:${errorKey}`, { lng: locale });
        // Log the error for the user
        console.group(`%cError%c [LsData] ${errorMessage}`, "color: #DC2626; background-color: #FEF2F2; display: inline-block; padding: 0 4px; border-radius: 2px;", "");
        if (!isEmpty(errorDescription)) {
            console.log(errorDescription);
        }
        if (!isNil(additionalInfo)) {
            console.log(additionalInfo);
        }
        console.groupEnd();
        if (isNil(this.store)) {
            // We always need a key to identify the store data, so if no key is provided we create one ourselves
            // Normally we use a combination of the objectType and objectId as fallback, but since this is an error we can't do that
            const fallbackKey = (_c = this.idKey) !== null && _c !== void 0 ? _c : "error";
            createNewStore(fallbackKey, {
                locale,
            });
            this.store = await getStore(fallbackKey);
        }
        // Update the state with the error
        this.store.set("error", {
            hasError: true,
            messageKey: errorKey,
            type: errorType,
        });
    }
    async resetError() {
        if (isNil(this.store)) {
            console.warn("[LsData] Cannot reset error: Store is not initialized");
            return false;
        }
        if (this.store.state.error.type === "")
            return;
        try {
            this.store.set("error", {
                hasError: false,
                messageKey: undefined,
                type: "",
            });
            console.log(`%c[LsData]%c ${this.store.t("ui:data.connection_restored")}`, "color: #059669; background-color: #ECFDF5; display: inline-block; padding: 0 4px; border-radius: 2px;", "");
            return true;
        }
        catch (error) {
            console.error("[LsData] Failed to reset error:", error);
            return false;
        }
    }
    closeSocketConnection() {
        this.socket.close();
        this.socket = null;
        this.socketId = null;
    }
    disconnectedCallback() {
        this.closeSocketConnection();
        // Clean up the store when the component is disconnected
        if (this.computedIdKey !== null) {
            removeStore(this.computedIdKey);
            this.computedIdKey = null;
        }
    }
    static get is() { return "ls-data"; }
    static get encapsulation() { return "shadow"; }
    static get properties() {
        return {
            "accessToken": {
                "type": "string",
                "attribute": "access-token",
                "mutable": false,
                "complexType": {
                    "original": "string",
                    "resolved": "string",
                    "references": {}
                },
                "required": false,
                "optional": false,
                "docs": {
                    "tags": [],
                    "text": "The token used to connect to the Socket.IO and API servers."
                },
                "getter": false,
                "setter": false,
                "reflect": false
            },
            "idKey": {
                "type": "string",
                "attribute": "id-key",
                "mutable": false,
                "complexType": {
                    "original": "string",
                    "resolved": "string",
                    "references": {}
                },
                "required": false,
                "optional": false,
                "docs": {
                    "tags": [],
                    "text": "The unique key that is used to identify store data."
                },
                "getter": false,
                "setter": false,
                "reflect": false
            },
            "sensorIds": {
                "type": "string",
                "attribute": "sensor-ids",
                "mutable": false,
                "complexType": {
                    "original": "string",
                    "resolved": "string",
                    "references": {}
                },
                "required": false,
                "optional": false,
                "docs": {
                    "tags": [],
                    "text": "Extra sensor IDs that should be included in the API requests."
                },
                "getter": false,
                "setter": false,
                "reflect": false
            },
            "apiUrl": {
                "type": "string",
                "attribute": "api-url",
                "mutable": false,
                "complexType": {
                    "original": "\"https://api-gcp-prod.liftstatus.nl\"",
                    "resolved": "\"https://api-gcp-prod.liftstatus.nl\"",
                    "references": {}
                },
                "required": false,
                "optional": false,
                "docs": {
                    "tags": [],
                    "text": "The URL for the API endpoint."
                },
                "getter": false,
                "setter": false,
                "reflect": false,
                "defaultValue": "\"https://api-gcp-prod.liftstatus.nl\""
            },
            "language": {
                "type": "string",
                "attribute": "language",
                "mutable": false,
                "complexType": {
                    "original": "string",
                    "resolved": "string",
                    "references": {}
                },
                "required": false,
                "optional": true,
                "docs": {
                    "tags": [],
                    "text": "Global locale for all components in this installation (`nl` or `en`)."
                },
                "getter": false,
                "setter": false,
                "reflect": false
            }
        };
    }
    static get watchers() {
        return [{
                "propName": "language",
                "methodName": "syncStoreLocaleFromLanguage"
            }];
    }
}
LsData.SENSOR_ID_S10 = "10";
LsData.SENSOR_ID_S524 = "524";
//# sourceMappingURL=ls-data.js.map
