"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.WeatherApi = void 0;
/* eslint-disable @typescript-eslint/no-explicit-any */
const debug_1 = require("../../debug");
const debug = (0, debug_1.createDebug)('signalk-server:api:weather');
const __1 = require("../");
const server_api_1 = require("@signalk/server-api");
const WEATHER_API_PATH = `/signalk/v2/api/weather`;
class WeatherApi {
    app;
    weatherProviders = new Map();
    defaultProviderId;
    constructor(app) {
        this.app = app;
    }
    async start() {
        this.initApiEndpoints();
        return Promise.resolve();
    }
    // ***** Plugin Interface methods *****
    // Register plugin as provider.
    register(pluginId, provider) {
        debug(`** Registering provider(s)....${pluginId} ${provider}`);
        if (!pluginId || !provider) {
            throw new Error(`Error registering provider ${pluginId}!`);
        }
        if (!(0, server_api_1.isWeatherProvider)(provider)) {
            throw new Error(`${pluginId} is missing WeatherProvider properties/methods!`);
        }
        else {
            if (!this.weatherProviders.has(pluginId)) {
                this.weatherProviders.set(pluginId, provider);
            }
            if (this.weatherProviders.size === 1) {
                this.defaultProviderId = pluginId;
            }
        }
        debug(`No. of WeatherProviders registered =`, this.weatherProviders.size);
    }
    // Unregister plugin as provider.
    unRegister(pluginId) {
        if (!pluginId) {
            return;
        }
        debug(`** Request to un-register plugin.....${pluginId}`);
        if (!this.weatherProviders.has(pluginId)) {
            debug(`** NOT FOUND....${pluginId}... cannot un-register!`);
            return;
        }
        debug(`** Un-registering weather provider....${pluginId}`);
        this.weatherProviders.delete(pluginId);
        if (pluginId === this.defaultProviderId) {
            this.defaultProviderId = undefined;
        }
        // update defaultProviderId if required
        if (this.weatherProviders.size !== 0 && !this.defaultProviderId) {
            this.defaultProviderId = this.weatherProviders.keys().next().value;
        }
        debug(`Remaining number of Weather Providers registered =`, this.weatherProviders.size, 'defaultProvider =', this.defaultProviderId);
    }
    /** Server API methods */
    /**
     * @description Return array of observations for the provided location
     * @param position Location for which to retrieve observation data
     * @param options Parameters to filter the returned data
     */
    async getObservations(position, options) {
        const q = this.parseQueryOptions({
            position: position,
            options: options
        });
        if (this.defaultProviderId &&
            this.weatherProviders.has(this.defaultProviderId)) {
            debug(`Using default provider...${this.defaultProviderId}`);
            return await this.weatherProviders
                .get(this.defaultProviderId)
                ?.methods.getObservations(q.position, q.options);
        }
        else {
            throw new Error(`Provider not found!`);
        }
    }
    /**
     * @description Return array of forecasts for the provided location
     * @param position Location for which to retrieve forecast data
     * @param type Type of forecast point | daily
     * @param options Parameters to filter the returned data
     */
    async getForecasts(position, type, options) {
        const q = this.parseQueryOptions({
            position: position,
            options: options
        });
        if (this.defaultProviderId &&
            this.weatherProviders.has(this.defaultProviderId)) {
            debug(`Using default provider...${this.defaultProviderId}`);
            const fdata = await this.weatherProviders
                .get(this.defaultProviderId)
                ?.methods.getForecasts(q.position, type, q.options);
            return fdata
                ? fdata.filter((i) => {
                    return i.type === type;
                })
                : [];
        }
        else {
            throw new Error(`Provider not found!`);
        }
    }
    /**
     * @description Return array of warnings for the provided location
     * @param position Location for which to retrieve warning data
     */
    async getWarnings(position) {
        const q = this.parseQueryOptions({
            position: position
        });
        if (this.defaultProviderId &&
            this.weatherProviders.has(this.defaultProviderId)) {
            debug(`Using default provider...${this.defaultProviderId}`);
            return await this.weatherProviders
                .get(this.defaultProviderId)
                ?.methods.getWarnings(q.position);
        }
        else {
            throw new Error(`Provider not found!`);
        }
    }
    // *************************************
    updateAllowed(request) {
        return this.app.securityStrategy.shouldAllowPut(request, 'vessels.self', null, 'weather');
    }
    /** @returns 1= OK, 0= invalid location, -1= location not provided */
    checkLocation(req) {
        if (req.query.lat && req.query.lon) {
            return isNaN(Number(req.query.lat)) || isNaN(Number(req.query.lon))
                ? 0
                : 1;
        }
        else {
            return -1;
        }
    }
    parseRequest(req, res, next) {
        try {
            debug(`Weather`, req.method, req.path, req.query, req.body);
            if (['PUT', 'POST'].includes(req.method)) {
                if (!this.updateAllowed(req)) {
                    res.status(403).json(__1.Responses.unauthorised);
                }
                else {
                    next();
                }
            }
            else {
                if (req.path === `/` || req.path === `/forecasts`) {
                    next();
                    return;
                }
                const l = this.checkLocation(req);
                if (l === 1) {
                    next();
                }
                else if (l === 0) {
                    res.status(400).json({
                        statusCode: 400,
                        state: 'FAILED',
                        message: 'Invalid position data!'
                    });
                }
                else {
                    res.status(400).json({
                        statusCode: 400,
                        state: 'FAILED',
                        message: 'Location not supplied!'
                    });
                }
            }
        }
        catch (err) {
            res.status(500).json({
                state: 'FAILED',
                statusCode: 500,
                message: err.message
            });
        }
    }
    initApiEndpoints() {
        debug(`** Initialise ${WEATHER_API_PATH} endpoints. **`);
        this.app.use(`${WEATHER_API_PATH}`, (req, res, next) => {
            debug(`Using... ${WEATHER_API_PATH}`);
            if (req.path.includes('providers')) {
                next();
            }
            else {
                return this.parseRequest(req, res, next);
            }
        });
        this.app.get(`${WEATHER_API_PATH}`, async (req, res) => {
            debug(`** ${req.method} ${req.path}`);
            try {
                res.status(200).json({
                    forecasts: {
                        description: 'Forecast data for the supplied location.'
                    },
                    observations: {
                        description: 'Observation data for the supplied location.'
                    },
                    warnings: {
                        description: 'Weather warnings for the supplied location.'
                    }
                });
            }
            catch (err) {
                res.status(400).json({
                    statusCode: 400,
                    state: 'FAILED',
                    message: err.message
                });
            }
        });
        // return list of weather providers
        this.app.get(`${WEATHER_API_PATH}/_providers`, async (req, res) => {
            debug(`**route = ${req.method} ${req.path}`);
            try {
                const r = {};
                this.weatherProviders.forEach((v, k) => {
                    r[k] = {
                        name: v.name,
                        isDefault: k === this.defaultProviderId
                    };
                });
                res.status(200).json(r);
            }
            catch (err) {
                res.status(400).json({
                    statusCode: 400,
                    state: 'FAILED',
                    message: err.message
                });
            }
        });
        // return default weather provider identifier
        this.app.get(`${WEATHER_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.message
                });
            }
        });
        // change weather provider
        this.app.post(`${WEATHER_API_PATH}/_providers/_default/:id`, async (req, res) => {
            debug(`**route = ${req.method} ${req.path}`);
            try {
                if (!req.params.id) {
                    throw new Error('Provider id not supplied!');
                }
                if (this.weatherProviders.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.message
                });
            }
        });
        // return observation data at the provided lat / lon
        this.app.get(`${WEATHER_API_PATH}/observations`, async (req, res) => {
            debug(`** ${req.method} ${req.path}`);
            try {
                const q = this.parseQueryOptions(req.query);
                const r = await this.useProvider(req).getObservations(q.position, q.options);
                res.status(200).json(r);
            }
            catch (err) {
                res.status(400).json({
                    statusCode: 400,
                    state: 'FAILED',
                    message: err.message
                });
            }
        });
        this.app.get(`${WEATHER_API_PATH}/forecasts`, async (req, res) => {
            debug(`** ${req.method} ${req.path}`);
            try {
                res.status(200).json({
                    daily: {
                        description: 'Daily forecast data for the requested number of days.'
                    },
                    point: {
                        description: 'Point forecast data for the requested number of intervals.'
                    }
                });
            }
            catch (err) {
                res.status(400).json({
                    statusCode: 400,
                    state: 'FAILED',
                    message: err.message
                });
            }
        });
        /**
         * Return daily forecast data at the provided lat / lon for the supplied number of days
         * ?days=x
         *
         */
        this.app.get(`${WEATHER_API_PATH}/forecasts/daily`, async (req, res) => {
            debug(`** ${req.method} ${req.path}`);
            try {
                const q = this.parseQueryOptions(req.query);
                const r = await this.useProvider(req).getForecasts(q.position, 'daily', q.options);
                const df = r.filter((i) => {
                    return i.type === 'daily';
                });
                res.status(200).json(df);
            }
            catch (err) {
                res.status(400).json({
                    statusCode: 400,
                    state: 'FAILED',
                    message: err.message
                });
            }
        });
        // return point forecast data at the provided lat / lon
        this.app.get(`${WEATHER_API_PATH}/forecasts/point`, async (req, res) => {
            debug(`** ${req.method} ${req.path}`);
            try {
                const q = this.parseQueryOptions(req.query);
                const r = await this.useProvider(req).getForecasts(q.position, 'point', q.options);
                const pf = r.filter((i) => {
                    return i.type === 'point';
                });
                res.status(200).json(pf);
            }
            catch (err) {
                res.status(400).json({
                    statusCode: 400,
                    state: 'FAILED',
                    message: err.message
                });
            }
        });
        // return warning data at the provided lat / lon
        this.app.get(`${WEATHER_API_PATH}/warnings`, async (req, res) => {
            debug(`** route = ${req.method} ${req.path}`);
            try {
                const q = this.parseQueryOptions(req.query);
                const r = await this.useProvider(req).getWarnings(q.position);
                res.status(200).json(r);
            }
            catch (err) {
                res.status(400).json({
                    statusCode: 400,
                    state: 'FAILED',
                    message: err.message
                });
            }
        });
        // error response
        this.app.use(`${WEATHER_API_PATH}/*`, (err, req, res, next) => {
            debug(`** route = error path **`);
            const msg = {
                state: err.state ?? 'FAILED',
                statusCode: err.statusCode ?? 500,
                message: err.message ?? 'Weather provider error!'
            };
            if (res.headersSent) {
                console.log('EXCEPTION: headersSent');
                return next(msg);
            }
            res.status(500).json(msg);
        });
    }
    /** Returns provider to use as data source.
     * @param req API request.
     */
    useProvider(req) {
        debug('** useProvider()');
        if (this.weatherProviders.size === 0) {
            throw new Error('No providers registered!');
        }
        if (req.query.provider) {
            debug(`Use requested provider... ${req.query.provider}`);
            if (this.weatherProviders.has(req.query.provider)) {
                debug(`Requested provider found...using ${req.query.provider}`);
                return this.weatherProviders.get(req.query.provider)
                    ?.methods;
            }
            else {
                throw new Error(`Requested provider not found! (${req.query.provider})`);
            }
        }
        else {
            if (this.defaultProviderId &&
                this.weatherProviders.has(this.defaultProviderId)) {
                debug(`Using default provider...${this.defaultProviderId}`);
                return this.weatherProviders.get(this.defaultProviderId)
                    ?.methods;
            }
            else {
                throw new Error(`Default provider not found!`);
            }
        }
    }
    /**
     * Parse request.query into weather provider options
     * @param query req.query
     */
    parseQueryOptions(query) {
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        const { lat, lon, provider, count, date, ...custom } = query;
        const q = {
            position: {
                latitude: this.parseValueAsNumber(lat) ?? 0,
                longitude: this.parseValueAsNumber(lon) ?? 0
            },
            options: { custom }
        };
        if (count) {
            const n = this.parseValueAsNumber(count);
            if (typeof n === 'number') {
                q.options.maxCount = n;
            }
        }
        if (date) {
            const pattern = /[0-9]{4}-[0-9]{2}-[0-9]{2}/;
            if (date.match(pattern)) {
                q.options.startDate = date?.toString();
            }
        }
        return q;
    }
    /**
     * Ensure the query parameter value is a number
     * @param q Query param value
     */
    parseValueAsNumber(value) {
        const n = Number(value);
        return isNaN(n) ? undefined : n;
    }
}
exports.WeatherApi = WeatherApi;
//# sourceMappingURL=index.js.map