// node dependencies
const { v4: uuidv4 } = require('uuid');
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const sharp = require('sharp');
const fs = require('fs');
const path = require('path');
require('dotenv').config({ path: '../.env' });

// variables
const SALT_ROUNDS = parseInt(process.env.SALT_ROUNDS);
const JWT_SECRET = process.env.JWT_SECRET;
const JWT_EXPIRES = '30d';

// file paths
const USER_PIC_DIR = '/mnt/openarchive/.lnx_connect/userPictures';
const TMP_UPLOAD_DIR = '/mnt/openarchive/.lnx_connect/tmp';

// dependencies
const pools = require('../services/database/pools');
const { sendMail } = require('./activator');

// helper
async function saveUserPicture(userId, tmpFilePath) {
    if (!fs.existsSync(USER_PIC_DIR)) fs.mkdirSync(USER_PIC_DIR, { recursive: true });
    const destPath = path.join(USER_PIC_DIR, `${userId}.png`);

    if (tmpFilePath && fs.existsSync(tmpFilePath)) {
        // resize uploaded image
        await sharp(tmpFilePath)
            .resize(250, 250)
            .png({ compressionLevel: 9 })
            .toFile(destPath);

        // delete tmp file
        fs.unlinkSync(tmpFilePath);

        return `${userId}.png`
    } else {
        return `default.png`;
    }
}

// export functions
module.exports = {
    // register function
    register: async (data) => {
        // 0. Declare dbs to use
        const read = pools.users.r.s;
        const write = pools.users.w.s;

        // 1. Get incoming variables
        const {
            userEmail: user_email,
            userName: user_name,
            userPass: user_pass,
            userPicture: user_picture,
            fromProject: from_project
        } = data;

        // 2. Check if email or username already exists
        const existing = await read.query(`
            SELECT user_email, user_name
            FROM users
            WHERE user_email = ? OR user_name = ?    
        `, [user_email, user_name]);

        // If exists throw error
        if (existing.length > 0) {
            const existingUser = existing[0];
            if (existingUser.user_email === user_email) throw new Error('User with that mail already exists.');
            if (existingUser.user_name === user_name) throw new Error('User with that name already exists.');
        }

        // 3. Generate IDs and keys
        const user_id = uuidv4();
        const authKeyPlain = uuidv4();
        const auth_key = await bcrypt.hash(authKeyPlain, SALT_ROUNDS);
        const pass_hash = await bcrypt.hash(user_pass, SALT_ROUNDS);
        const session_key = jwt.sign({ user_id }, JWT_SECRET, { expiresIn: JWT_EXPIRES });

        // 4. Handle user picture
        const tmpFileName = path.basename(user_picture || '');
        const tmpFilePath = path.join(TMP_UPLOAD_DIR, tmpFileName);
        const user_picture_file = await saveUserPicture(user_id, tmpFilePath);

        // 5. Set default role
        const default_role = ['default'];

        // 5. Write data to db
        await write.query(`
            INSERT INTO users (
                user_id,
                auth_key,
                session_key,
                user_name,
                user_email,
                user_pass,
                user_picture,
                from_project,
                assigned_roles,
                state,
                last_active
            ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())
        `, [
            user_id,
            auth_key,
            session_key,
            user_name,
            user_email,
            pass_hash,
            user_picture_file,
            from_project,
            JSON.stringify(default_role),
            'active'
        ]);

        // send activation mail
        sendMail(user_id, user_name, user_email);

        // Objectize result
        const result = {
            userId: user_id,
            authKey: authKeyPlain,
            sessionKey: session_key,
            userName: user_name,
            userEmail: user_email,
            userPicture: user_picture_file,
            fromProject: from_project,
            assignedRoles: default_role
        }

        // Return result
        return result;
    },

    // login function
    login: async (data) => {
        // Dummy hash used to normalize login timing
        const DUMMY_HASH = '$2b$10$nC7jBLRF/HwpZLkt79xCreF5Npsh5NeVFnGzOrRF7sVxlI9yJk99i';

        // 0. Declare dbs to use
        const read = pools.users.r.s;
        const write = pools.users.w.s;

        // 1. Get incoming variables
        const {
            userLogin: user_login,
            userPass: user_pass,
            sessionKey: session_key
        } = data;
        const login = (user_login || '').trim(); // trim login name/mail

        // 2. Check if user exists
        const existing = await read.query(`
            SELECT
            user_id,
            auth_key,
            session_key,
            user_name,
            user_email,
            user_pass,
            user_picture,
            from_project,
            assigned_roles,
            additional_permissions,
            state
            FROM users
            WHERE user_email = ? OR user_name = ?
        `, [login, login]);

        let user = existing[0];

        // Run fake bcrypt compare to match timing
        if (!user) {
            await bcrypt.compare(user_pass || '', DUMMY_HASH);
            throw new Error('Invalid login credentials.');
        }

        // 3. Check user state
        if (user.state === 'banned' || user.state === 'pending') throw new Error(`User ${user.user_name} is ${user.state}.`);

        // 4. Login validation
        let loginValid = false;
        let autoLogin = false;

        // 4.1. Session login (auto login)
        if (session_key) {
            try {
                const decoded = jwt.verify(session_key, JWT_SECRET);
                if (decoded.user_id === user.user_id && session_key === user.session_key) {
                    loginValid = true;
                    autoLogin = true;
                };
            } catch (err) {
                loginValid = false;
                autoLogin = false;
            }
        }

        // 4.2. Password login
        let new_session_key;
        if (!loginValid) {
            // match passwords
            const passMatch = await bcrypt.compare(user_pass || '', user.user_pass);
            if (!passMatch) throw new Error('Invalid login credentials.');
            loginValid = true;

            // generate new session key
            new_session_key = jwt.sign({ user_id: user.user_id }, JWT_SECRET, { expiresIn: JWT_EXPIRES });
        }

        const newAuthKeyPlain = uuidv4();
        const newAuthKeyHash = await bcrypt.hash(newAuthKeyPlain, SALT_ROUNDS);

        // 6. Update session after login
        if (loginValid && !autoLogin) {
            await write.query(`
                UPDATE users
                SET auth_key = ?, session_key = ?, last_active = NOW()
                WHERE user_id = ?
            `, [newAuthKeyHash, new_session_key, user.user_id]);
        }
        if (loginValid && autoLogin) {
            await write.query(`
                UPDATE users
                SET auth_key = ?, last_active = NOW()
                WHERE user_id = ?
            `, [newAuthKeyHash, user.user_id]);
        }

        // Objectize result
        const result = {
            userId: user.user_id,
            authKey: autoLogin ? user.auth_key : newAuthKeyPlain,
            sessionKey: autoLogin ? user.session_key : new_session_key,
            userName: user.user_name,
            userEmail: user.user_email,
            userPicture: user.user_picture,
            fromProject: user.from_project,
            assignedRoles: user.assigned_roles ? JSON.parse(user.assigned_roles) : [],
            additionalPermissions: user.additional_permissions ? JSON.parse(user.additional_permissions) : []
        }

        // Return result
        return result;
    },

    // update function
    update: async (data) => {
        // 0. Declare dbs to use
        const read = pools.users.r.s;
        const write = pools.users.w.s;

        // 1. Get incoming variables
        const {
            userId: user_id,
            authKey: auth_key,
            newUserName: user_name,
            newUserEmail: user_email,
            newUserPass: user_pass,
            newUserPicture: user_picture,
            assignedRoles: assigned_roles,
            additionalPermissions: additional_permissions,
            state: new_state
        } = data;

        // 2. Fetch current user
        const existing = await read.query(`
            SELECT
            user_id,
            auth_key,
            user_name,
            user_email,
            user_pass,
            user_picture,
            assigned_roles,
            additional_permissions,
            state
            FROM users
            WHERE user_id = ?
        `, [user_id]);
        if (!existing[0]) throw new Error('User not found.');
        const user = existing[0];

        // 3. Check auth key
        const authMatch = await bcrypt.compare(auth_key, user.auth_key);
        if (!authMatch) throw new Error('Invalid auth key.');

        // 4. Prepare fields to update
        const updates = {};
        if (user_name && user_name !== user.user_name) updates.user_name = user_name;
        if (user_email && user_email !== user.user_email) updates.user_email = user_email;
        if (user_pass) updates.user_pass = await bcrypt.hash(user_pass, SALT_ROUNDS);
        if (assigned_roles) updates.assigned_roles = JSON.stringify(assigned_roles);
        if (additional_permissions) updates.additional_permissions = JSON.stringify(additional_permissions);
        if (new_state && new_state !== user.state) updates.state = new_state;

        // 5. Handle new user picture
        if (user_picture) {
            const tmpFileName = path.basename(user_picture || '');
            const tmpFilePath = path.join(TMP_UPLOAD_DIR, tmpFileName);
            updates.user_picture = await saveUserPicture(user_id, tmpFilePath);
        }

        // 6. Build dynamic update query
        const fields = Object.keys(updates);
        if (fields.length === 0) return { message: 'Nothing to update.' };

        const placeholders = fields.map(f => `${f} = ?`).join(', ');
        const values = fields.map(f => updates[f]);
        values.push(user_id);

        await write.query(`
            UPDATE users
            SET ${placeholders}, last_active = NOW()
            WHERE user_id = ?
        `, values);

        // 7. Return updated user data
        const updatedUser = await read.query(`
            SELECT
            user_id,
            user_name,
            user_email,
            user_picture,
            from_project,
            assigned_roles,
            additional_permissions,
            state
            FROM users
            WHERE user_id = ?
        `, [user_id]);

        return updatedUser[0];
    },

    // activation function
    activate: async (data) => {
        // 0. Declare dbs to use
        const read = pools.users.r.s;
        const write = pools.users.w.s;

        // 1. Get incoming token
        const { activationToken } = data;
        if (!activationToken) throw new Error('No activation token provided.');
        let payload;
        try {
            // 2. Verify token
            payload = jwt.verify(activationToken, JWT_SECRET);
        } catch (err) {
            throw new Error('Invalid or expired activation token.');
        }

        const userId = payload.user_id;

        // 3. Fetch user
        const existing = await read.query(`
            SELECT user_id, assigned_roles
            FROM users
            WHERE user_id = ?
        `, [userId]);

        if (!existing[0]) throw new Error('User not found.');

        const user = existing[0];

        // 4. Add "activated" role if not present
        const roles = user.assigned_roles ? JSON.parse(user.assigned_roles) : [];
        if (!roles.includes('activated')) roles.push('activated');

        // 5. Update DB
        await write.query(`
            UPDATE users
            SET assigned_roles = ?, last_active = NOW()
            WHERE user_id = ?
        `, [JSON.stringify(roles), userId]);

        return { message: 'Account successfully activated.', assignedRoles: roles };
    },

    // check auth function
    check: async (data) => {},
};