const isUrlHttp = require('is-url-http');
// Database connection
const db = require('./db');

/* Enums */
// Enum for param type
const ParamType = {
    PATH: 'path',
    QUERY: 'query',
    BODY: 'body',
};

/* Helper functions */
// Function checks that a value is a valid boolean
// IN: value : value to check
// OUT: true if value evaluates to true, otherwise false
function checkBoolean(value) {
    return typeof value === 'boolean';
}

// Function checks that a value is a valid integer
// IN: value : value to check
// OUT: true if value is a valid integer, otherwise false
function checkInteger(value) {
    return !isNaN(parseInt(value, 10));
}

// Function checks that a value is a valid number
// IN: value : value to check
// OUT: true if value is a valid number, otherwise false
function checkNumber(value) {
    return typeof value === 'number';
}

// Function checks that a value is a valid string
// IN: value : value to check
// OUT: true if value is a valid string, otherwise false
function checkString(value) {
    return typeof value === 'string';
}

// Function checks that a value is a valid Clemson email address
// IN: value : value to check
// OUT: true if value is a valid Clemson email address, otherwise false
function checkEmailAddress(value) {
    return /^[a-zA-Z0-9._%+-]+@clemson\.edu$/.test(value);
}

// Function checks that a value is a valid date/time string
// IN: value : value to check
// OUT: true if value is a valid date/time string, otherwise false
function checkDate(value) {
    return checkString(value) && !isNaN(new Date(value).getTime());
}

// Function checks that a valid is a valid URL string
// IN: value : value to check
// OUT: true if value is a valid URL string, otherwise false
function checkURL(value) {
    return isUrlHttp(value);
}

// Function returns JSON object representing results of param validation
// IN: value : value whose validity is being checked (for reuse in error messages)
//     is_valid : bool whose value indicates the validity of `value`
//     error_msg : error message (supplied by calling function)
// OUT: validation tuple for use in determining parameter validity
function build_validation_tuple(value, is_valid, error_msg) {
    return {
        value: value,
        is_valid: is_valid,
        error_msg: error_msg,
    };
}

/* Validation functions */
// Function validates the order_id param by validating against the database
// IN: order_id : order_id being validated
//     param_type : enum `ParamType` member indicating 'path', 'query', or 'body' param type
// OUT: validation tuple
async function validate_order_id(order_id, param_type) {
    // If param is a path param or body property, validate type
    if (param_type === ParamType.PATH || param_type === ParamType.BODY) {
        if (!checkInteger(order_id)) {
            return build_validation_tuple(
                order_id,
                false,
                'Failed to convert value ' +
                    order_id +
                    ' for order_id to an integer.'
            );
        }
    }

    // Attempt to validate against the database
    try {
        const result = await db.query(
            'SELECT order_id FROM orders WHERE order_id = $1',
            [order_id]
        );

        if (
            result.rows.length === 0 ||
            result.rows[0].order_id !== Number(order_id)
        ) {
            return build_validation_tuple(
                order_id,
                false,
                'An order with the order_id ' + order_id + ' does not exist.'
            );
        }
    } catch (error) {
        return build_validation_tuple(order_id, false, error.message);
    }

    // If execution reaches here, param is valid
    return build_validation_tuple(order_id, true, null);
}

// Function validates the instructor_id param by validating against the database
// IN: instructor_id : instructor_id being validated
//     param_type : enum `ParamType` member indicating 'path', 'query', or 'body' param type
// OUT: validation tuple
async function validate_instructor_id(instructor_id, param_type) {
    // If param is a path param or body property, validate type
    if (param_type === ParamType.PATH || param_type === ParamType.BODY) {
        if (!checkInteger(instructor_id)) {
            return build_validation_tuple(
                instructor_id,
                false,
                'Failed to convert value ' +
                    instructor_id +
                    ' for instructor_id to an integer.'
            );
        }
    }

    // Attempt to validate against the database
    try {
        const result = await db.query(
            'SELECT instructor_id FROM instructors WHERE instructor_id = $1',
            [instructor_id]
        );

        if (
            result.rows.length === 0 ||
            result.rows[0].instructor_id !== Number(instructor_id)
        ) {
            return build_validation_tuple(
                instructor_id,
                false,
                'An instructor with the instructor_id ' +
                    instructor_id +
                    ' does not exist.'
            );
        }
    } catch (error) {
        return build_validation_tuple(instructor_id, false, error.message);
    }

    // If execution reaches here, param is valid
    return build_validation_tuple(instructor_id, true, null);
}

// Function validates the supplier_id param by validating against the database
// IN: supplier_id : supplier_id being validated
//     param_type : enum `ParamType` member indicating 'path', 'query', or 'body' param type
// OUT: validation tuple
async function validate_supplier_id(supplier_id, param_type) {
    // If param is a path param or body property, validate type
    if (param_type === ParamType.PATH || param_type === ParamType.BODY) {
        if (!checkInteger(supplier_id)) {
            return build_validation_tuple(
                supplier_id,
                false,
                'Failed to convert value ' +
                    supplier_id +
                    ' for supplier_id to an integer.'
            );
        }
    }

    // Attempt to validate against the database
    try {
        const result = await db.query(
            'SELECT supplier_id FROM suppliers WHERE supplier_id = $1',
            [supplier_id]
        );

        if (
            result.rows.length === 0 ||
            result.rows[0].supplier_id !== Number(supplier_id)
        ) {
            return build_validation_tuple(
                supplier_id,
                false,
                'A supplier with the supplier_id ' +
                    supplier_id +
                    ' does not exist.'
            );
        }
    } catch (error) {
        return build_validation_tuple(supplier_id, false, error.message);
    }

    // If execution reaches here, param is valid
    return build_validation_tuple(supplier_id, true, null);
}

// Function validates the shipment_id param by validating against the database
// IN: shipment_id : shipment_id being validated
//     param_type : enum `ParamType` member indicating 'path', 'query', or 'body' param type
// OUT: validation tuple
async function validate_shipment_id(shipment_id, param_type) {
    // If param is a path param or body property, validate type
    if (param_type === ParamType.PATH || param_type === ParamType.BODY) {
        if (!checkInteger(shipment_id)) {
            return build_validation_tuple(
                shipment_id,
                false,
                'Failed to convert value ' +
                    shipment_id +
                    ' for shipment_id to an integer.'
            );
        }
    }

    // Attempt to validate against the database
    try {
        const result = await db.query(
            'SELECT shipment_id FROM shipments WHERE shipment_id = $1',
            [shipment_id]
        );

        if (
            result.rows.length === 0 ||
            result.rows[0].shipment_id !== Number(shipment_id)
        ) {
            return build_validation_tuple(
                shipment_id,
                false,
                'A shipment with the shipment_id ' +
                    shipment_id +
                    ' does not exist.'
            );
        }
    } catch (error) {
        return build_validation_tuple(shipment_id, false, error.message);
    }

    // If execution reaches here, param is valid
    return build_validation_tuple(shipment_id, true, null);
}

// Function validates the user_id param by validating against the database
// IN: user_id : user_id being validated
//     param_type : enum `ParamType` member indicating 'path', 'query', or 'body' param type
// OUT: validation tuple
async function validate_user_id(user_id, param_type) {
    // If param is a path param or body property, validate type
    if (param_type === ParamType.PATH || param_type === ParamType.BODY) {
        if (!checkInteger(user_id)) {
            return build_validation_tuple(
                user_id,
                false,
                'Failed to convert value ' +
                    user_id +
                    ' for user_id to an integer.'
            );
        }
    }

    // Attempt to validate against the database
    try {
        const result = await db.query(
            'SELECT user_id FROM users WHERE user_id = $1',
            [user_id]
        );

        if (
            result.rows.length === 0 ||
            result.rows[0].user_id !== Number(user_id)
        ) {
            return build_validation_tuple(
                user_id,
                false,
                'A user with the user_id ' + user_id + ' does not exist.'
            );
        }
    } catch (error) {
        return build_validation_tuple(user_id, false, error.message);
    }

    // If execution reaches here, param is valid
    return build_validation_tuple(user_id, true, null);
}

// Function validates the ta_id param by validating against the database
// IN: ta_id : ta_id being validated
//     param_type : enum `ParamType` member indicating 'path', 'query', or 'body' param type
// OUT: validation tuple
async function validate_ta_id(ta_id, param_type) {
    // If param is a path param or body property, validate type
    if (param_type === ParamType.PATH || param_type === ParamType.BODY) {
        if (!checkInteger(ta_id)) {
            return build_validation_tuple(
                ta_id,
                false,
                'Failed to convert value ' + ta_id + ' for ta_id to an integer.'
            );
        }
    }

    // Attempt to validate against the database
    try {
        const result = await db.query(
            'SELECT ta_id FROM tas WHERE ta_id = $1',
            [ta_id]
        );

        if (
            result.rows.length === 0 ||
            result.rows[0].ta_id !== Number(ta_id)
        ) {
            return build_validation_tuple(
                ta_id,
                false,
                'A user with the ta_id ' + ta_id + ' does not exist.'
            );
        }
    } catch (error) {
        return build_validation_tuple(ta_id, false, error.message);
    }

    // If execution reaches here, param is valid
    return build_validation_tuple(ta_id, true, null);
}

// Function validates the student_id param by validating against the database
// IN: student_id : student_id being validated
//     param_type : enum `ParamType` member indicating 'path', 'query', or 'body' param type
// OUT: validation tuple
async function validate_student_id(student_id, param_type) {
    // If param is a path param or body property, validate type
    if (param_type === ParamType.PATH || param_type === ParamType.BODY) {
        if (!checkInteger(student_id)) {
            return build_validation_tuple(
                student_id,
                false,
                'Failed to convert value ' +
                    student_id +
                    ' for student_id to an integer.'
            );
        }
    }

    // Attempt to validate against the database
    try {
        const result = await db.query(
            'SELECT student_id FROM students WHERE student_id = $1',
            [student_id]
        );

        if (
            result.rows.length === 0 ||
            result.rows[0].student_id !== Number(student_id)
        ) {
            return build_validation_tuple(
                student_id,
                false,
                'A student with the student_id ' +
                    student_id +
                    ' does not exist.'
            );
        }
    } catch (error) {
        return build_validation_tuple(student_id, false, error.message);
    }

    // If execution reaches here, param is valid
    return build_validation_tuple(student_id, true, null);
}

// Function validates the team_id param by validating against the database
// IN: team_id : team_id being validated
//     param_type : enum `ParamType` member indicating 'path', 'query', or 'body' param type
// OUT: validation tuple
async function validate_team_id(team_id, param_type) {
    // If param is a path param or body property, validate type
    if (param_type === ParamType.PATH || param_type === ParamType.BODY) {
        if (!checkInteger(team_id)) {
            return build_validation_tuple(
                team_id,
                false,
                'Failed to convert value ' +
                    team_id +
                    ' for team_id to an integer.'
            );
        }
    }

    // Attempt to validate against the database
    try {
        const result = await db.query(
            'SELECT team_id FROM teams WHERE team_id = $1',
            [team_id]
        );

        if (
            result.rows.length === 0 ||
            result.rows[0].team_id !== Number(team_id)
        ) {
            return build_validation_tuple(
                team_id,
                false,
                'A team with the team_id ' + team_id + ' does not exist.'
            );
        }
    } catch (error) {
        return build_validation_tuple(team_id, false, error.message);
    }

    // If execution reaches here, param is valid
    return build_validation_tuple(team_id, true, null);
}

// Function validates the part_id param by validating against the database
// IN: part_id : part_id being validated
//     param_type : enum `ParamType` member indicating 'path', 'query', or 'body' param type
// OUT: validation tuple
async function validate_part_id(part_id, param_type) {
    // If param is a path param or body property, validate type
    if (param_type === ParamType.PATH || param_type === ParamType.BODY) {
        if (!checkInteger(part_id)) {
            return build_validation_tuple(
                part_id,
                false,
                'Failed to convert value ' +
                    part_id +
                    ' for part_id to an integer.'
            );
        }
    }

    // Attempt to validate against the database
    try {
        const result = await db.query(
            'SELECT part_id FROM parts WHERE part_id = $1',
            [part_id]
        );

        if (
            result.rows.length === 0 ||
            result.rows[0].part_id !== Number(part_id)
        ) {
            return build_validation_tuple(
                part_id,
                false,
                'A part with the part_id ' + part_id + ' does not exist.'
            );
        }
    } catch (error) {
        return build_validation_tuple(part_id, false, error.message);
    }

    // If execution reaches here, param is valid
    return build_validation_tuple(part_id, true, null);
}

// Function validates the request_id param by validating against the database
// IN: request_id : request_id being validated
//     param_type : enum `ParamType` member indicating 'path', 'query', or 'body' param type
// OUT: validation tuple
async function validate_request_id(request_id, param_type) {
    // If param is a path param or body property, validate type
    if (param_type === ParamType.PATH || param_type === ParamType.BODY) {
        if (!checkInteger(request_id)) {
            return build_validation_tuple(
                request_id,
                false,
                'Failed to convert value ' +
                    request_id +
                    ' for request_id to an integer.'
            );
        }
    }

    // Attempt to validate against the database
    try {
        const result = await db.query(
            'SELECT request_id FROM requests WHERE request_id = $1',
            [request_id]
        );

        if (
            result.rows.length === 0 ||
            result.rows[0].request_id !== Number(request_id)
        ) {
            return build_validation_tuple(
                request_id,
                false,
                'A request with the request_id ' +
                    request_id +
                    ' does not exist.'
            );
        }
    } catch (error) {
        return build_validation_tuple(request_id, false, error.message);
    }

    // If execution reaches here, param is valid
    return build_validation_tuple(request_id, true, null);
}

// Function validates the notification_id param by validating against the database
// IN: notification_id : notification_id being validated
//     param_type : enum `ParamType` member indicating 'path', 'query', or 'body' param type
// OUT: validation tuple
async function validate_notification_id(notification_id, param_type) {
    // If param is a path param or body property, validate type
    if (param_type === ParamType.PATH || param_type === ParamType.BODY) {
        if (!checkInteger(notification_id)) {
            return build_validation_tuple(
                notification_id,
                false,
                'Failed to convert value ' +
                    notification_id +
                    ' for notification_id to an integer.'
            );
        }
    }

    // Attempt to validate against the database
    try {
        const result = await db.query(
            'SELECT notification_id FROM notifications WHERE notification_id = $1',
            [notification_id]
        );

        if (
            result.rows.length === 0 ||
            result.rows[0].notification_id !== Number(notification_id)
        ) {
            return build_validation_tuple(
                notification_id,
                false,
                'A request with the notification_id ' +
                    notification_id +
                    ' does not exist.'
            );
        }
    } catch (error) {
        return build_validation_tuple(notification_id, false, error.message);
    }

    // If execution reaches here, param is valid
    return build_validation_tuple(notification_id, true, null);
}

// Function validates the read_notification_id param by validating against the database
// IN: read_notification_id : read_notification_id being validated
//     param_type : enum `ParamType` member indicating 'path', 'query', or 'body' param type
// OUT: validation tuple
async function validate_read_notification_id(read_notification_id, param_type) {
    // If param is a path param or body property, validate type
    if (param_type === ParamType.PATH || param_type === ParamType.BODY) {
        if (!checkInteger(read_notification_id)) {
            return build_validation_tuple(
                read_notification_id,
                false,
                'Failed to convert value ' +
                    read_notification_id +
                    ' for read_notification_id to an integer.'
            );
        }
    }

    // Attempt to validate against the database
    try {
        const result = await db.query(
            'SELECT read_notification_id FROM read_notifications WHERE read_notification_id = $1',
            [read_notification_id]
        );

        if (
            result.rows.length === 0 ||
            result.rows[0].read_notification_id !== Number(read_notification_id)
        ) {
            return build_validation_tuple(
                read_notification_id,
                false,
                'A request with the read_notification_id ' +
                    read_notification_id +
                    ' does not exist.'
            );
        }
    } catch (error) {
        return build_validation_tuple(
            read_notification_id,
            false,
            error.message
        );
    }

    // If execution reaches here, param is valid
    return build_validation_tuple(read_notification_id, true, null);
}

// Function validates the shipment status property against a set of allowed values
// Also validates that status is a valid string
// IN: status : shipment status being validated
// OUT: validation tuple
function validate_shipment_status(status) {
    // Make sure status value is a valid string
    let validation_tuple = build_validation_tuple(
        status,
        checkString(status),
        'Failed to convert value ' + status + ' for status to a string.'
    );
    if (!validation_tuple.is_valid) {
        return validation_tuple;
    }

    // Set of allowed values for shipment status per API spec
    const allowed_values = [
        'unknown',
        'pre_transit',
        'in_transit',
        'out_for_delivery',
        'delivered',
        'available_for_pickup',
        'return_to_sender',
        'failure',
        'cancelled',
        'error',
    ];

    // Make sure status value is in allowed_values
    const isValid = allowed_values.includes(status);
    return build_validation_tuple(
        status,
        isValid,
        isValid ? null : 'The value ' + status + ' for status is invalid.'
    );
}

// Function validates the request status property against a set of allowed values
// Also validates that status is a valid string
// IN: status : request status being validated
// OUT: validation tuple
function validate_request_status(status) {
    // Make sure status value is a valid string
    let validation_tuple = build_validation_tuple(
        status,
        checkString(status),
        'Failed to convert value ' + status + ' for status to a string.'
    );
    if (!validation_tuple.is_valid) {
        return validation_tuple;
    }

    // Set of allowed values for request status per API spec
    const allowed_values = [
        'requested',
        'approved',
        'ordered',
        'delivered',
        'delayed',
        'reordered',
        'on hand',
        'cancelled',
        'other',
    ];

    // Make sure status value is in allowed_values
    const isValid = allowed_values.includes(status);
    return build_validation_tuple(
        status,
        isValid,
        isValid ? null : 'The value ' + status + ' for status is invalid.'
    );
}

// Function validates the user type property against a set of allowed values
// Also validates that type is a valid string
// IN: type : user type being validated
// OUT: validation tuple
function validate_user_type(type) {
    // Make sure type value is a valid string
    let validation_tuple = build_validation_tuple(
        type,
        checkString(type),
        'Failed to convert value ' + type + ' for type to a string.'
    );
    if (!validation_tuple.is_valid) {
        return validation_tuple;
    }

    // Set of allowed values for user type per API spec
    const allowed_values = ['instructor', 'ta', 'student'];

    // Make sure status value is in allowed_values
    const isValid = allowed_values.includes(type);
    return build_validation_tuple(
        type,
        isValid,
        isValid ? null : 'The value ' + type + ' for type is invalid.'
    );
}

// Function validates the supplier name
// Checks that name is just the "domain" part of a URL
// IN: url : supplier URL (previously validated)
//     type : supplier name being validated
// OUT: validation tuple
function validate_supplier_name(url, name) {
    // Make sure name value is a valid string
    let validation_tuple = build_validation_tuple(
        name,
        checkString(name),
        'Failed to convert value ' + name + ' for name to a string.'
    );
    if (!validation_tuple.is_valid) {
        return validation_tuple;
    }

    // Make sure name is equal to the URL's hostname
    let hostname = new URL(url).hostname.split('.');
    if (hostname.length > 2) {
        hostname.shift();
    }
    hostname = hostname[0];
    const isValid = name === hostname;
    return build_validation_tuple(
        name,
        isValid,
        isValid ? null : 'The value ' + name + ' for name is invalid.'
    );
}

// Function validates the instructor is_admin property against a set of allowed values
// Also validates that is_admin is a valid string
// IN: is_admin : instructor is_admin being validated
// OUT: validation tuple
function validate_instructor_is_admin(is_admin) {
    // Make sure is_admin value is a valid string
    let validation_tuple = build_validation_tuple(
        is_admin,
        checkString(is_admin),
        'Failed to convert value ' + is_admin + ' for is_admin to a string.'
    );
    if (!validation_tuple.is_valid) {
        return validation_tuple;
    }

    // Set of allowed values for instructor is_admin per API spec
    const allowed_values = ['true', 'incoming', 'false'];

    // Make sure status value is in allowed_values
    const isValid = allowed_values.includes(is_admin);
    return build_validation_tuple(
        is_admin,
        isValid,
        isValid ? null : 'The value ' + is_admin + ' for is_admin is invalid.'
    );
}

// Export functions for use in other modules
module.exports = {
    ParamType: ParamType,
    checkBoolean: (value) => checkBoolean(value),
    checkInteger: (value) => checkInteger(value),
    checkNumber: (value) => checkNumber(value),
    checkString: (value) => checkString(value),
    checkDate: (value) => checkDate(value),
    checkEmailAddress: (value) => checkEmailAddress(value),
    checkURL: (value) => checkURL(value),
    build_validation_tuple: (value, is_valid, error_msg) =>
        build_validation_tuple(value, is_valid, error_msg),
    validate_order_id: (order_id, param_type) =>
        validate_order_id(order_id, param_type),
    validate_instructor_id: (instructor_id, param_type) =>
        validate_instructor_id(instructor_id, param_type),
    validate_supplier_id: (supplier_id, param_type) =>
        validate_supplier_id(supplier_id, param_type),
    validate_shipment_id: (shipment_id, param_type) =>
        validate_shipment_id(shipment_id, param_type),
    validate_user_id: (user_id, param_type) =>
        validate_user_id(user_id, param_type),
    validate_ta_id: (ta_id, param_type) => validate_ta_id(ta_id, param_type),
    validate_student_id: (student_id, param_type) =>
        validate_student_id(student_id, param_type),
    validate_team_id: (team_id, param_type) =>
        validate_team_id(team_id, param_type),
    validate_part_id: (part_id, param_type) =>
        validate_part_id(part_id, param_type),
    validate_request_id: (request_id, param_type) =>
        validate_request_id(request_id, param_type),
    validate_notification_id: (notification_id, param_type) =>
        validate_notification_id(notification_id, param_type),
    validate_read_notification_id: (read_notification_id, param_type) =>
        validate_read_notification_id(read_notification_id, param_type),
    validate_shipment_status: (status) => validate_shipment_status(status),
    validate_request_status: (status) => validate_request_status(status),
    validate_user_type: (type) => validate_user_type(type),
    validate_supplier_name: (url, name) => validate_supplier_name(url, name),
    validate_instructor_is_admin: (is_admin) =>
        validate_instructor_is_admin(is_admin),
};
