/*global $STM_Config */
import koa_router from 'koa-router';
import koa_body from 'koa-body';
import models from 'db/models';
import findUser from 'db/utils/find_user';
import config from 'config';
import recordWebEvent from 'server/record_web_event';
import { esc, escAttrs } from 'db/models';
import {
    emailRegex,
    getRemoteIp,
    rateLimitReq,
    checkCSRF,
} from 'server/utils/misc';
import coBody from 'co-body';
import Mixpanel from 'mixpanel';
import Tarantool from 'db/tarantool';
import { PublicKey, Signature, hash } from '@steemit/steem-js/lib/auth/ecc';
import { api, broadcast } from '@steemit/steem-js';

const mixpanel = config.get('mixpanel')
    ? Mixpanel.init(config.get('mixpanel'))
    : null;

const _stringval = v => (typeof v === 'string' ? v : JSON.stringify(v));
function logRequest(path, ctx, extra) {
    let d = { ip: getRemoteIp(ctx.req) };
    if (ctx.session) {
        if (ctx.session.user) {
            d.user = ctx.session.user;
        }
        if (ctx.session.uid) {
            d.uid = ctx.session.uid;
        }
        if (ctx.session.a) {
            d.account = ctx.session.a;
        }
    }
    if (extra) {
        Object.keys(extra).forEach(k => {
            const nk = d[k] ? '_' + k : k;
            d[nk] = extra[k];
        });
    }
    const info = Object.keys(d)
        .map(k => `${k}=${_stringval(d[k])}`)
        .join(' ');
    console.log(`-- /${path} --> ${info}`);
}

export default function useGeneralApi(app) {
    const router = koa_router({ prefix: '/api/v1' });
    app.use(router.routes());
    const koaBody = koa_body();

    router.post('/accounts_wait', koaBody, function*() {
        if (rateLimitReq(this, this.req)) return;
        const params = this.request.body;
        const account =
            typeof params === 'string' ? JSON.parse(params) : params;
        const remote_ip = getRemoteIp(this.req);
        if (!checkCSRF(this, account.csrf)) return;
        logRequest('accounts_wait', this, { account });
        const user_id = this.session.user;
        try {
            models.Account.create(
                escAttrs({
                    user_id,
                    name: account.name,
                    owner_key: account.owner_key,
                    active_key: account.active_key,
                    posting_key: account.posting_key,
                    memo_key: account.memo_key,
                    remote_ip,
                    referrer: this.session.r,
                    created: false,
                })
            ).catch(error => {
                console.error(
                    "!!! Can't create account wait model in /accounts api",
                    this.session.uid,
                    error
                );
            });
            if (mixpanel) {
                mixpanel.track('Signup WaitList', {
                    distinct_id: this.session.uid,
                    ip: remote_ip,
                });
                mixpanel.people.set(this.session.uid, { ip: remote_ip });
            }
        } catch (error) {
            console.error('Error in /accounts_wait', error);
        }
        this.body = JSON.stringify({ status: 'ok' });
        recordWebEvent(
            this,
            'api/accounts_wait',
            account ? account.name : 'n/a'
        );
    });

    router.post('/accounts', koaBody, function*() {
        if (rateLimitReq(this, this.req)) return;
        const params = this.request.body;
        const account =
            typeof params === 'string' ? JSON.parse(params) : params;
        if (!checkCSRF(this, account.csrf)) return;
        logRequest('accounts', this, { account });
        if ($STM_Config.disable_signups) {
            this.body = JSON.stringify({
                error: 'New signups are temporary disabled.',
            });
            this.status = 401;
            return;
        }

        const user_id = this.session.user;
        if (!user_id) {
            // require user to sign in with identity provider
            this.body = JSON.stringify({ error: 'Unauthorized' });
            this.status = 401;
            return;
        }

        // acquire global lock so only one account can be created at a time
        try {
            const lock_entity_res = yield Tarantool.instance().call(
                'lock_entity',
                user_id + ''
            );
            if (!lock_entity_res[0][0]) {
                console.log(
                    '-- /accounts lock_entity -->',
                    user_id,
                    lock_entity_res[0][0]
                );
                this.body = JSON.stringify({ error: 'Conflict' });
                this.status = 409;
                return;
            }
        } catch (e) {
            console.error(
                '-- /accounts tarantool is not available, fallback to another method',
                e
            );
            const rnd_wait_time = Math.random() * 10000;
            console.log('-- /accounts rnd_wait_time -->', rnd_wait_time);
            yield new Promise(resolve =>
                setTimeout(() => resolve(), rnd_wait_time)
            );
        }

        try {
            const user = yield models.User.findOne({
                attributes: ['id'],
                where: { id: user_id, account_status: 'approved' },
            });
            if (!user) {
                throw new Error(
                    "We can't find your sign up request. You either haven't started your sign up application or weren't approved yet."
                );
            }

            // disable session/multi account for now

            // const existing_created_account = yield models.Account.findOne({
            //     attributes: ['id'],
            //     where: {user_id, ignored: false, created: true},
            //     order: 'id DESC'
            // });
            // if (existing_created_account) {
            //     throw new Error("Only one Steem account per user is allowed in order to prevent abuse");
            // }

            const remote_ip = getRemoteIp(this.req);
            // rate limit account creation to one per IP every 10 minutes
            const same_ip_account = yield models.Account.findOne({
                attributes: ['created_at'],
                where: { remote_ip: esc(remote_ip), created: true },
                order: 'id DESC',
            });
            if (same_ip_account) {
                const minutes =
                    (Date.now() - same_ip_account.created_at) / 60000;
                if (minutes < 10) {
                    console.log(
                        `api /accounts: IP rate limit for user ${
                            this.session.uid
                        } #${user_id}, IP ${remote_ip}`
                    );
                    throw new Error(
                        'Only one Steem account allowed per IP address every 10 minutes'
                    );
                }
            }

            yield createAccount({
                signingKey: config.get('registrar.signing_key'),
                fee: config.get('registrar.fee'),
                creator: config.get('registrar.account'),
                new_account_name: account.name,
                delegation: config.get('registrar.delegation'),
                owner: account.owner_key,
                active: account.active_key,
                posting: account.posting_key,
                memo: account.memo_key,
            });
            console.log(
                '-- create_account_with_keys created -->',
                this.session.uid,
                account.name,
                user.id,
                account.owner_key
            );

            this.body = JSON.stringify({ status: 'ok' });

            // update user account status
            yield user.update({ account_status: 'created' });

            // update or create account record
            const account_attrs = escAttrs({
                user_id,
                name: account.name,
                owner_key: account.owner_key,
                active_key: account.active_key,
                posting_key: account.posting_key,
                memo_key: account.memo_key,
                remote_ip,
                referrer: this.session.r,
                created: true,
            });

            const existing_account = yield models.Account.findOne({
                attributes: ['id'],
                where: { user_id, name: account.name },
                order: 'id DESC',
            });
            if (existing_account) {
                yield existing_account.update(account_attrs);
            } else {
                yield models.Account.create(account_attrs);
            }
            if (mixpanel) {
                mixpanel.track('Signup', {
                    distinct_id: this.session.uid,
                    ip: remote_ip,
                });
                mixpanel.people.set(this.session.uid, { ip: remote_ip });
            }
        } catch (error) {
            console.error(
                'Error in /accounts api call',
                this.session.uid,
                error.toString()
            );
            this.body = JSON.stringify({ error: error.message });
            this.status = 500;
        } finally {
            // console.log('-- /accounts unlock_entity -->', user_id);
            // release global lock
            try {
                yield Tarantool.instance().call('unlock_entity', user_id + '');
            } catch (e) {
                /* ram lock */
            }
        }
        recordWebEvent(this, 'api/accounts', account ? account.name : 'n/a');
    });

    router.post('/update_email', koaBody, function*() {
        if (rateLimitReq(this, this.req)) return;
        const params = this.request.body;
        const { csrf, email } =
            typeof params === 'string' ? JSON.parse(params) : params;
        if (!checkCSRF(this, csrf)) return;
        logRequest('update_email', this, { email });
        try {
            if (!emailRegex.test(email.toLowerCase()))
                throw new Error('not valid email: ' + email);
            // TODO: limit by 1/min/ip
            let user = yield findUser({
                user_id: this.session.user,
                email: esc(email),
                uid: this.session.uid,
            });
            if (user) {
                user = yield models.User.update(
                    { email: esc(email), waiting_list: true },
                    { where: { id: user.id } }
                );
            } else {
                user = yield models.User.create({
                    email: esc(email),
                    waiting_list: true,
                });
            }
            this.session.user = user.id;
            this.body = JSON.stringify({ status: 'ok' });
        } catch (error) {
            console.error(
                'Error in /update_email api call',
                this.session.uid,
                error
            );
            this.body = JSON.stringify({ error: error.message });
            this.status = 500;
        }
        recordWebEvent(this, 'api/update_email', email);
    });

    router.post('/login_account', koaBody, function*() {
        // if (rateLimitReq(this, this.req)) return;
        const params = this.request.body;
        const { csrf, account, signatures } =
            typeof params === 'string' ? JSON.parse(params) : params;
        if (!checkCSRF(this, csrf)) return;
        logRequest('login_account', this, { account });
        try {
            const db_account = yield models.Account.findOne({
                attributes: ['user_id'],
                where: { name: esc(account) },
                logging: false,
            });
            if (db_account) this.session.user = db_account.user_id;

            if (signatures) {
                if (!this.session.login_challenge) {
                    console.error(
                        '/login_account missing this.session.login_challenge'
                    );
                } else {
                    const [chainAccount] = yield api.getAccountsAsync([
                        account,
                    ]);
                    if (!chainAccount) {
                        console.error(
                            '/login_account missing blockchain account',
                            account
                        );
                    } else {
                        const auth = { posting: false };
                        const bufSha = hash.sha256(
                            JSON.stringify(
                                { token: this.session.login_challenge },
                                null,
                                0
                            )
                        );
                        const verify = (
                            type,
                            sigHex,
                            pubkey,
                            weight,
                            weight_threshold
                        ) => {
                            if (!sigHex) return;
                            if (weight !== 1 || weight_threshold !== 1) {
                                console.error(
                                    `/login_account login_challenge unsupported ${
                                        type
                                    } auth configuration: ${account}`
                                );
                            } else {
                                const sig = parseSig(sigHex);
                                const public_key = PublicKey.fromString(pubkey);
                                const verified = sig.verifyHash(
                                    bufSha,
                                    public_key
                                );
                                if (!verified) {
                                    console.error(
                                        '/login_account verification failed',
                                        this.session.uid,
                                        account,
                                        pubkey
                                    );
                                }
                                auth[type] = verified;
                            }
                        };
                        const {
                            posting: {
                                key_auths: [[posting_pubkey, weight]],
                                weight_threshold,
                            },
                        } = chainAccount;
                        verify(
                            'posting',
                            signatures.posting,
                            posting_pubkey,
                            weight,
                            weight_threshold
                        );
                        if (auth.posting) this.session.a = account;
                    }
                }
            }

            this.body = JSON.stringify({ status: 'ok' });
            const remote_ip = getRemoteIp(this.req);
            if (mixpanel) {
                mixpanel.people.set(this.session.uid, {
                    ip: remote_ip,
                    $ip: remote_ip,
                });
                mixpanel.people.increment(this.session.uid, 'Logins', 1);
            }
        } catch (error) {
            console.error(
                'Error in /login_account api call',
                this.session.uid,
                error.message
            );
            this.body = JSON.stringify({ error: error.message });
            this.status = 500;
        }
        recordWebEvent(this, 'api/login_account', account);
    });

    router.post('/logout_account', koaBody, function*() {
        // if (rateLimitReq(this, this.req)) return; - logout maybe immediately followed with login_attempt event
        const params = this.request.body;
        const { csrf } =
            typeof params === 'string' ? JSON.parse(params) : params;
        if (!checkCSRF(this, csrf)) return;
        logRequest('logout_account', this);
        try {
            this.session.a = null;
            this.body = JSON.stringify({ status: 'ok' });
        } catch (error) {
            console.error(
                'Error in /logout_account api call',
                this.session.uid,
                error
            );
            this.body = JSON.stringify({ error: error.message });
            this.status = 500;
        }
    });

    router.post('/record_event', koaBody, function*() {
        if (rateLimitReq(this, this.req)) return;
        try {
            const params = this.request.body;
            const { csrf, type, value } =
                typeof params === 'string' ? JSON.parse(params) : params;
            if (!checkCSRF(this, csrf)) return;
            logRequest('record_event', this, { type, value });
            const str_value =
                typeof value === 'string' ? value : JSON.stringify(value);
            if (type.match(/^[A-Z]/)) {
                if (mixpanel) {
                    mixpanel.track(type, {
                        distinct_id: this.session.uid,
                        Page: str_value,
                    });
                    mixpanel.people.increment(this.session.uid, type, 1);
                }
            } else {
                recordWebEvent(this, type, str_value);
            }
            this.body = JSON.stringify({ status: 'ok' });
        } catch (error) {
            console.error('Error in /record_event api call', error.message);
            this.body = JSON.stringify({ error: error.message });
            this.status = 500;
        }
    });

    router.post('/csp_violation', function*() {
        if (rateLimitReq(this, this.req)) return;
        let params;
        try {
            params = yield coBody(this);
        } catch (error) {
            console.log('-- /csp_violation error -->', error);
        }
        if (params && params['csp-report']) {
            const csp_report = params['csp-report'];
            const value = `${csp_report['document-uri']} : ${
                csp_report['blocked-uri']
            }`;
            console.log(
                '-- /csp_violation -->',
                value,
                '--',
                this.req.headers['user-agent']
            );
            recordWebEvent(this, 'csp_violation', value);
        } else {
            console.log(
                '-- /csp_violation [no csp-report] -->',
                params,
                '--',
                this.req.headers['user-agent']
            );
        }
        this.body = '';
    });

    router.post('/page_view', koaBody, function*() {
        const params = this.request.body;
        const { csrf, page, ref } =
            typeof params === 'string' ? JSON.parse(params) : params;
        if (!checkCSRF(this, csrf)) return;
        if (page.match(/\/feed$/)) {
            this.body = JSON.stringify({ views: 0 });
            return;
        }
        const remote_ip = getRemoteIp(this.req);
        logRequest('page_view', this, { page });
        try {
            let views = 1,
                unique = true;
            if (config.has('tarantool') && config.has('tarantool.host')) {
                try {
                    const res = yield Tarantool.instance().call(
                        'page_view',
                        page,
                        remote_ip,
                        this.session.uid,
                        ref
                    );
                    unique = res[0][0];
                } catch (e) {}
            }
            const page_model = yield models.Page.findOne({
                attributes: ['id', 'views'],
                where: { permlink: esc(page) },
                logging: false,
            });
            if (unique) {
                if (page_model) {
                    views = page_model.views + 1;
                    yield yield models.Page.update(
                        { views },
                        { where: { id: page_model.id }, logging: false }
                    );
                } else {
                    yield models.Page.create(
                        escAttrs({ permlink: page, views }),
                        { logging: false }
                    );
                }
            } else {
                if (page_model) views = page_model.views;
            }
            this.body = JSON.stringify({ views });
            if (mixpanel) {
                let referring_domain = '';
                if (ref) {
                    const matches = ref.match(
                        /^https?\:\/\/([^\/?#]+)(?:[\/?#]|$)/i
                    );
                    referring_domain = matches && matches[1];
                }
                const mp_params = {
                    distinct_id: this.session.uid,
                    Page: page,
                    ip: remote_ip,
                    $referrer: ref,
                    $referring_domain: referring_domain,
                };
                mixpanel.track('PageView', mp_params);
                if (!this.session.mp) {
                    mixpanel.track('FirstVisit', mp_params);
                    this.session.mp = 1;
                }
                if (ref)
                    mixpanel.people.set_once(
                        this.session.uid,
                        '$referrer',
                        ref
                    );
                mixpanel.people.set_once(this.session.uid, 'FirstPage', page);
                mixpanel.people.increment(this.session.uid, 'PageView', 1);
            }
        } catch (error) {
            console.error(
                'Error in /page_view api call',
                this.session.uid,
                error.message
            );
            this.body = JSON.stringify({ error: error.message });
            this.status = 500;
        }
    });

    router.post('/save_cords', koaBody, function*() {
        const params = this.request.body;
        const { csrf, x, y } =
            typeof params === 'string' ? JSON.parse(params) : params;
        if (!checkCSRF(this, csrf)) return;
        const user = yield models.User.findOne({
            where: { id: this.session.user },
        });
        if (user) {
            let data = user.sign_up_meta ? JSON.parse(user.sign_up_meta) : {};
            data['button_screen_x'] = x;
            data['button_screen_y'] = y;
            data['last_step'] = 3;
            try {
                user.update({
                    sign_up_meta: JSON.stringify(data),
                });
            } catch (error) {
                console.error(
                    'Error in /save_cords api call',
                    this.session.uid,
                    error.message
                );
                this.body = JSON.stringify({ error: error.message });
                this.status = 500;
            }
        }
        this.body = JSON.stringify({ status: 'ok' });
    });

    router.post('/setUserPreferences', koaBody, function*() {
        const params = this.request.body;
        const { csrf, payload } =
            typeof params === 'string' ? JSON.parse(params) : params;
        if (!checkCSRF(this, csrf)) return;
        console.log(
            '-- /setUserPreferences -->',
            this.session.user,
            this.session.uid,
            payload
        );
        if (!this.session.a) {
            this.body = 'missing logged in account';
            this.status = 500;
            return;
        }
        try {
            const json = JSON.stringify(payload);
            if (json.length > 1024) throw new Error('the data is too long');
            this.session.user_prefs = json;
            this.body = JSON.stringify({ status: 'ok' });
        } catch (error) {
            console.error(
                'Error in /setUserPreferences api call',
                this.session.uid,
                error
            );
            this.body = JSON.stringify({ error: error.message });
            this.status = 500;
        }
    });
}

/**
 @arg signingKey {string|PrivateKey} - WIF or PrivateKey object
 */
function* createAccount({
    signingKey,
    fee,
    creator,
    new_account_name,
    json_metadata = '',
    delegation,
    owner,
    active,
    posting,
    memo,
}) {
    const operations = [
        [
            'account_create_with_delegation',
            {
                fee,
                creator,
                new_account_name,
                json_metadata,
                delegation,
                owner: {
                    weight_threshold: 1,
                    account_auths: [],
                    key_auths: [[owner, 1]],
                },
                active: {
                    weight_threshold: 1,
                    account_auths: [],
                    key_auths: [[active, 1]],
                },
                posting: {
                    weight_threshold: 1,
                    account_auths: [],
                    key_auths: [[posting, 1]],
                },
                memo_key: memo,
            },
        ],
    ];
    yield broadcast.sendAsync(
        {
            extensions: [],
            operations,
        },
        [signingKey]
    );
}

const parseSig = hexSig => {
    try {
        return Signature.fromHex(hexSig);
    } catch (e) {
        return null;
    }
};
