const cls = require("../services/cls");
const sql = require("../services/sql");
const log = require("../services/log");
const becca = require("../becca/becca");
const etapiTokenService = require("../services/etapi_tokens");
const config = require("../services/config");
const GENERIC_CODE = "GENERIC";

const noAuthentication = config.General && config.General.noAuthentication === true;

class EtapiError extends Error {
    constructor(statusCode, code, message) {
        super();

        this.statusCode = statusCode;
        this.code = code;
        this.message = message;
    }
}

function sendError(res, statusCode, code, message) {
    return res
        .set('Content-Type', 'application/json')
        .status(statusCode)
        .send(JSON.stringify({
            "status": statusCode,
            "code": code,
            "message": message
        }));
}

function checkEtapiAuth(req, res, next) {
    if (noAuthentication || etapiTokenService.isValidAuthHeader(req.headers.authorization)) {
        next();
    }
    else {
        sendError(res, 401, "NOT_AUTHENTICATED", "Not authenticated");
    }
}

function processRequest(req, res, routeHandler, next, method, path) {
    try {
        cls.namespace.bindEmitter(req);
        cls.namespace.bindEmitter(res);

        cls.init(() => {
            cls.set('componentId', "etapi");
            cls.set('localNowDateTime', req.headers['trilium-local-now-datetime']);

            const cb = () => routeHandler(req, res, next);

            return sql.transactional(cb);
        });
    } catch (e) {
        log.error(`${method} ${path} threw exception ${e.message} with stacktrace: ${e.stack}`);

        if (e instanceof EtapiError) {
            sendError(res, e.statusCode, e.code, e.message);
        } else {
            sendError(res, 500, GENERIC_CODE, e.message);
        }
    }
}

function route(router, method, path, routeHandler) {
    router[method](path, checkEtapiAuth, (req, res, next) => processRequest(req, res, routeHandler, next, method, path));
}

function NOT_AUTHENTICATED_ROUTE(router, method, path, routeHandler) {
    router[method](path, (req, res, next) => processRequest(req, res, routeHandler, next, method, path));
}

function getAndCheckNote(noteId) {
    const note = becca.getNote(noteId);

    if (note) {
        return note;
    }
    else {
        throw new EtapiError(404, "NOTE_NOT_FOUND", `Note '${noteId}' not found`);
    }
}

function getAndCheckBranch(branchId) {
    const branch = becca.getBranch(branchId);

    if (branch) {
        return branch;
    }
    else {
        throw new EtapiError(404, "BRANCH_NOT_FOUND", `Branch '${branchId}' not found`);
    }
}

function getAndCheckAttribute(attributeId) {
    const attribute = becca.getAttribute(attributeId);

    if (attribute) {
        return attribute;
    }
    else {
        throw new EtapiError(404, "ATTRIBUTE_NOT_FOUND", `Attribute '${attributeId}' not found`);
    }
}

function validateAndPatch(target, source, allowedProperties) {
    for (const key of Object.keys(source)) {
        if (!(key in allowedProperties)) {
            throw new EtapiError(400, "PROPERTY_NOT_ALLOWED", `Property '${key}' is not allowed for PATCH.`);
        }
        else {
            for (const validator of allowedProperties[key]) {
                const validationResult = validator(source[key]);

                if (validationResult) {
                    throw new EtapiError(400, "PROPERTY_VALIDATION_ERROR", `Validation failed on property '${key}': ${validationResult}`);
                }
            }
        }
    }

    // validation passed, let's patch
    for (const propName of Object.keys(source)) {
        target[propName] = source[propName];
    }
}

module.exports = {
    EtapiError,
    sendError,
    route,
    NOT_AUTHENTICATED_ROUTE,
    GENERIC_CODE,
    validateAndPatch,
    getAndCheckNote,
    getAndCheckBranch,
    getAndCheckAttribute
}
