"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.parseTimeRangeParams = exports.parseResolution = exports.HistoryApiHttpRegistry = void 0;
const history_1 = require("@signalk/server-api/history");
const polyfill_1 = require("@js-temporal/polyfill");
const debug_1 = require("../../debug");
const __1 = require("../");
const debug = (0, debug_1.createDebug)('signalk-server:api:history');
const HISTORY_API_PATH = `/signalk/v2/api/history`;
class HistoryApiHttpRegistry {
    app;
    historyProviders = new Map();
    defaultProviderId;
    proxy;
    constructor(app) {
        this.app = app;
        this.proxy = {
            getValues: (query) => {
                return this.defaultProvider().getValues(query);
            },
            getContexts: (query) => {
                return this.defaultProvider().getContexts(query);
            },
            getPaths: (query) => {
                return this.defaultProvider().getPaths(query);
            }
        };
        app.getHistoryApi = (providerId) => {
            if (providerId !== undefined) {
                const provider = this.historyProviders.get(providerId);
                return provider
                    ? Promise.resolve(provider)
                    : Promise.reject(new Error(`History api provider '${providerId}' not found`));
            }
            return this.defaultProviderId &&
                this.historyProviders.has(this.defaultProviderId)
                ? Promise.resolve(this.proxy)
                : Promise.reject(new Error('No history api provider configured'));
        };
    }
    registerHistoryApiProvider(pluginId, provider) {
        if (!(0, history_1.isHistoryProvider)(provider)) {
            throw new Error('Invalid history api provider');
        }
        if (!this.historyProviders.has(pluginId)) {
            this.historyProviders.set(pluginId, provider);
        }
        if (this.historyProviders.size === 1) {
            this.defaultProviderId = pluginId;
        }
        debug(`Registered history api provider ${pluginId},`, `total=${this.historyProviders.size},`, `default=${this.defaultProviderId}`);
    }
    unregisterHistoryApiProvider(pluginId) {
        if (!pluginId || !this.historyProviders.has(pluginId)) {
            return;
        }
        this.historyProviders.delete(pluginId);
        if (pluginId === this.defaultProviderId) {
            this.defaultProviderId = this.historyProviders.keys().next().value;
        }
        debug(`Unregistered history api provider ${pluginId},`, `total=${this.historyProviders.size},`, `default=${this.defaultProviderId}`);
    }
    start() {
        // return list of history providers
        this.app.get(`${HISTORY_API_PATH}/_providers`, async (req, res) => {
            debug(`**route = ${req.method} ${req.path}`);
            try {
                const r = {};
                this.historyProviders.forEach((_v, k) => {
                    r[k] = {
                        isDefault: k === this.defaultProviderId
                    };
                });
                res.status(200).json(r);
            }
            catch (err) {
                res.status(400).json({
                    statusCode: 400,
                    state: 'FAILED',
                    message: err instanceof Error ? err.message : 'Unknown error'
                });
            }
        });
        // return default history provider identifier
        this.app.get(`${HISTORY_API_PATH}/_providers/_default`, async (req, res) => {
            debug(`**route = ${req.method} ${req.path}`);
            try {
                res.status(200).json({
                    id: this.defaultProviderId
                });
            }
            catch (err) {
                res.status(400).json({
                    statusCode: 400,
                    state: 'FAILED',
                    message: err instanceof Error ? err.message : 'Unknown error'
                });
            }
        });
        // change default history provider
        this.app.post(`${HISTORY_API_PATH}/_providers/_default/:id`, async (req, res) => {
            debug(`**route = ${req.method} ${req.path}`);
            try {
                if (!this.app.securityStrategy.shouldAllowPut(req, 'vessels.self', null, 'history')) {
                    res.status(403).json(__1.Responses.unauthorised);
                    return;
                }
                if (!req.params.id) {
                    throw new Error('Provider id not supplied!');
                }
                if (this.historyProviders.has(req.params.id)) {
                    this.defaultProviderId = req.params.id;
                    res.status(200).json({
                        statusCode: 200,
                        state: 'COMPLETED',
                        message: `Default provider set to ${req.params.id}.`
                    });
                }
                else {
                    throw new Error(`Provider ${req.params.id} not found!`);
                }
            }
            catch (err) {
                res.status(400).json({
                    statusCode: 400,
                    state: 'FAILED',
                    message: err instanceof Error ? err.message : 'Unknown error'
                });
            }
        });
        this.app.get(`${HISTORY_API_PATH}/values`, (req, res) => respondWith(() => this.useProvider(req), (provider) => {
            return provider.getValues(parseValuesQuery(req.query));
        }, res));
        this.app.get(`${HISTORY_API_PATH}/contexts`, (req, res) => respondWith(() => this.useProvider(req), (provider) => {
            const { timeRangeParams, errors } = (0, exports.parseTimeRangeParams)(req.query);
            if (errors.length > 0) {
                throw new Error(`Validation errors: ${errors.join(', ')}`);
            }
            debug.enabled && debug(JSON.stringify(timeRangeParams, null, 2));
            return provider.getContexts(timeRangeParams);
        }, res));
        this.app.get(`${HISTORY_API_PATH}/paths`, (req, res) => respondWith(() => this.useProvider(req), (provider) => {
            const { timeRangeParams, errors } = (0, exports.parseTimeRangeParams)(req.query);
            if (errors.length > 0) {
                throw new Error(`Validation errors: ${errors.join(', ')}`);
            }
            debug.enabled && debug(JSON.stringify(timeRangeParams, null, 2));
            return provider.getPaths(timeRangeParams);
        }, res));
    }
    defaultProvider() {
        if (this.defaultProviderId &&
            this.historyProviders.has(this.defaultProviderId)) {
            return this.historyProviders.get(this.defaultProviderId);
        }
        throw new Error('No history api provider configured');
    }
    useProvider(req) {
        if (req.query.provider) {
            const provider = this.historyProviders.get(req.query.provider);
            if (!provider) {
                throw new Error(`Requested provider not found! (${req.query.provider})`);
            }
            return provider;
        }
        return this.defaultProviderId
            ? this.historyProviders.get(this.defaultProviderId)
            : undefined;
    }
}
exports.HistoryApiHttpRegistry = HistoryApiHttpRegistry;
async function respondWith(getProvider, handler, res) {
    try {
        const provider = getProvider();
        if (!provider) {
            return res
                .status(501)
                .json({ error: 'No history api provider configured' });
        }
        res.json(await handler(provider));
    }
    catch (error) {
        res.status(400).json({
            error: error instanceof Error ? error.message : 'Invalid request'
        });
    }
}
const parseValuesQuery = (query) => {
    const { timeRangeParams, errors } = (0, exports.parseTimeRangeParams)(query);
    const context = query.context;
    let resolution;
    try {
        resolution = (0, exports.parseResolution)(query.resolution);
    }
    catch (error) {
        errors.push(error instanceof Error ? error.message : 'Invalid resolution');
    }
    const paths = query.paths;
    if (!paths) {
        errors.push('paths parameter is required and must be a string');
    }
    if (errors.length > 0) {
        throw new Error(`Validation errors: ${errors.join(', ')}`);
    }
    const pathExpressions = (query.paths || '')
        .replace(/[^0-9a-z.,:]/gi, '')
        .split(',');
    const pathSpecs = pathExpressions.map(splitPathExpression);
    const parsed = {
        ...timeRangeParams,
        context,
        resolution,
        pathSpecs
    };
    debug.enabled && debug(JSON.stringify(parsed, null, 2));
    return parsed;
};
// Maps the single-letter unit suffix in a resolution time expression to seconds.
const RESOLUTION_UNIT_SECONDS = {
    s: 1,
    m: 60,
    h: 3_600,
    d: 86_400
};
// Matches resolution time expressions per the History API spec, e.g.
// '1s' -> 1, '15m' -> 900, '2h' -> 7200, '1d' -> 86400.
const RESOLUTION_TIME_EXPRESSION = /^(\d+)([smhd])$/;
// Parses the `resolution` query parameter into seconds.
// Accepts a number (already in seconds), a numeric string, or a time
// expression of the form `<integer><unit>` where unit is s|m|h|d.
// Returns undefined when the parameter is absent.
// Exported for unit testing.
const parseResolution = (value) => {
    if (value === undefined || value === null || value === '')
        return undefined;
    if (typeof value === 'number')
        return value;
    if (typeof value !== 'string')
        return undefined;
    const trimmed = value.trim();
    const match = RESOLUTION_TIME_EXPRESSION.exec(trimmed);
    if (match) {
        return Number(match[1]) * RESOLUTION_UNIT_SECONDS[match[2]];
    }
    const asNumber = Number(trimmed);
    if (!Number.isNaN(asNumber))
        return asNumber;
    throw new Error(`resolution parameter must be a number of seconds or a time expression like '1s', '1m', '1h', '1d'`);
};
exports.parseResolution = parseResolution;
const splitPathExpression = (pathExpression) => {
    const parts = pathExpression.split(':');
    let aggregateMethod = (parts[1] || 'average');
    if (parts[0] === 'navigation.position') {
        aggregateMethod = 'first';
    }
    // Extract all parameters from parts[2] onwards
    const parameters = parts.slice(2).filter((p) => p.length > 0);
    const pathSpec = {
        path: parts[0],
        aggregate: aggregateMethod,
        parameter: parameters
    };
    return pathSpec;
};
// Exported for unit testing.
const parseTimeRangeParams = (query) => {
    const errors = [];
    const fromStr = query.from;
    let from;
    if (fromStr) {
        try {
            from = polyfill_1.Temporal.Instant.from(fromStr);
        }
        catch (error) {
            errors.push(`from parameter must be a valid ISO 8601 timestamp: ${error instanceof Error ? error.message : 'Invalid format'}`);
        }
    }
    const durationStr = query.duration;
    let duration;
    if (durationStr) {
        try {
            duration = polyfill_1.Temporal.Duration.from(durationStr);
        }
        catch {
            // Strict non-negative decimal integer only: rule out negatives, hex
            // (0x10), exponential (9e2), surrounding whitespace, and fractional
            // forms that Number() would otherwise silently accept.
            if (/^\d+$/.test(durationStr)) {
                duration = polyfill_1.Temporal.Duration.from({ seconds: Number(durationStr) });
            }
            else {
                errors.push(`duration parameter must be an ISO 8601 duration string (e.g. 'PT15M') or an integer number of seconds`);
            }
        }
    }
    if (!from && !duration) {
        errors.push('Either from or duration parameter is required at minimum');
    }
    const toStr = query.to;
    let to;
    if (toStr) {
        try {
            to = polyfill_1.Temporal.Instant.from(toStr);
        }
        catch (error) {
            errors.push(`to parameter must be a valid ISO 8601 timestamp: ${error instanceof Error ? error.message : 'Invalid format'}`);
        }
    }
    if (from && to && duration) {
        errors.push('Cannot specify all of from, to, and duration together; choose either from+to or from+duration or to+duration');
    }
    if (from && to && polyfill_1.Temporal.Instant.compare(from, to) >= 0) {
        errors.push('from parameter must be before to parameter');
    }
    if (errors.length > 0) {
        throw new Error(`Validation errors: ${errors.join(', ')}`);
    }
    return { timeRangeParams: { from, to, duration }, errors };
};
exports.parseTimeRangeParams = parseTimeRangeParams;
//# sourceMappingURL=index.js.map