const mysql = require('mysql2/promise');
const exec = require('child_process').exec;
const fs = require('fs-extra');
const path = require('path');
const colors = require('colors');
const axios = require('axios');
const yml = require('require-yml');

const selectQuery = fs.readFileSync(`sql/selectQueued.sql`).toString();
const jobArgsQuery = fs.readFileSync(`sql/jobArgs.sql`).toString();
const updateQuery = fs.readFileSync(`sql/updateState.sql`).toString();
const appDir = path.dirname(require.main.filename);

class PrintServer {
    async start() {
        let conf = yml(path.join(__dirname, 'config.yml'));
        const localConfFile = path.join(__dirname, 'config.local.yml');
        if (fs.existsSync(localConfFile))
            conf = Object.assign({}, conf, yml(localConfFile));
        this.conf = conf;

        const decoration = '△▽'.repeat(10)
        console.clear();
        console.log(decoration, `${colors.bgBlack.white.bold(' Print')}${colors.bgBlack.green.bold('Natura ')}`, decoration, '\n')
        await this.getToken();
        await this.init();
    }
    async init() {
        this.conn = await mysql.createConnection(this.conf.db);
        this.conn.on('error', err => this.onDbError(err));
        await this.poll();
    }
    async stop() {
        const salix = this.conf.salix
        await axios.post(`${salix.url}/api/${salix.methods.logout}?access_token=${this.token}`);
        await this.end();
    }
    async end() {
        clearTimeout(this.pollTimeout);
        await this.conn.end();
        if (this.reconnectTimeout) {
            clearTimeout(this.reconnectTimeout);
            this.reconnectTimeout = null;
        }
    }
    async getToken() {
        const salix = this.conf.salix
        let response = await axios.post(`${salix.url}/api/${salix.methods.login}`, {
            user: salix.user,
            password: salix.password
        });
        this.token = response.data.token
    }
    async onDbError(err) {
        switch(err.code) {
            case 1927:
                try {
                    await this.end();
                } catch(e) {}
                await this.reconnect();
                break;
        }
    }
	async reconnect() {
        this.reconnectTimeout = null;
		try {
            await this.init();
		} catch (err) {
			this.reconnectTimeout = setTimeout(reconnect(), this.conf.reconnectTimeout * 1000);
		}
	}
    async poll() {
        try {
            await this.printJob();
        } catch (err) {
            console.error(err)
            this.reconnect();
        }
        this.pollTimeout = setTimeout(() => this.poll(), this.conf.refreshRate);
    }
    async printJob() {
        const conn = this.conn;
        const conf = this.conf;
        let printJob;
        let jobId;

        try {
            await conn.beginTransaction();
            [[printJob]] = await conn.query(selectQuery, 'LabelCollection');
            if (!printJob) return;

            jobId = printJob.id;
            this.method = printJob.method;

            await conn.query(updateQuery, ['printing', null, jobId]);
            await conn.commit();
        } catch (err) {
            conn.rollback();
            throw err;
        }

        try {
            const args = {};
            const res = await conn.query(jobArgsQuery, jobId);
            for (const row of res[0])
                args[row.name] = row.value;
            try {
                let methodPath = this.method.replace(/{\w+}/g, function(match) {
                    const params = {id: args.param};
                    const key = match.substr(1, match.length - 2);
                    const value = params[key];
                    return value !== undefined ? value : match;
                });

                const response = await axios({
                    method: 'get',
                    url: `${conf.salix.url}/api/${methodPath}?access_token=${this.token}`,
                    responseType: 'arraybuffer',
                    headers: {
                        'Content-Type': 'application/json',
                        'Accept': 'application/pdf'
                    }
                });       
                this.pdfData = response.data       
            }
            catch (err) {
                switch(err.response.statusText) {
                    case 'Unauthorized':
                        try {
                            await this.getToken();
                        } catch(err) {
                            throw new Error(`Could not get token: ${err.message}`);
                        }
                        break;
                }
            }
            const printer = printJob.printer;
            const tmpPath = path.join(appDir, 'tmp')
            if (!fs.existsSync(tmpPath))
                fs.mkdirSync(tmpPath)
            const tmpFilePath = path.join(tmpPath, `${Math.random().toString(36).substring(7)}.pdf`);
            await fs.writeFile(tmpFilePath, this.pdfData, 'binary');

            try {
                await pExec(`lp -d "${printer}" "${tmpFilePath}"`);
            } catch(err) {
                await fs.unlink(tmpFilePath);
                throw new Error(`The printer ${printer} is not installed: ${err.message}`);
            } 

            await conn.query(updateQuery, ['printed', null, jobId]);

            if (conf.debug)
                console.debug(`(${colors.yellow(jobId)}) Document has been printed`, `[${args.param}, ${printJob.report}, ${printer}]`.green);

            await fs.unlink(tmpFilePath);
        } catch (err) {
            await this.conn.query(updateQuery, ['error', err.message, jobId]);
            throw new Error(`(${jobId}) ${err.message}`);
        }
    }
}

module.exports = PrintServer;

function pExec(command) {
	return new Promise(function(resolve, reject) {
		exec(command, function(err, stdout, stderr) {
			if (err) return reject(err);
			resolve({stdout, stderr})
		});
	});
}