/* eslint-disable arrow-parens */
const path = require('path');
const get = require('lodash/get');
const minimatch = require('minimatch');
const serveStatic = require('serve-static');
const bearerToken = require('express-bearer-token');
const validators = require('@blocklet/sdk/lib/validators');
const {
  genPermissionName,
  NODE_SERVICES,
  NODE_SERVICES_PREFIX,
  WELLKNOWN_AUTH_PATH_PREFIX,
} = require('@abtnode/constant');
const { BLOCKLET_MODES, BlockletStatus } = require('@blocklet/meta/lib/constants');

const { getBaseUrl } = require('@abtnode/router-adapter');
const logger = require('@abtnode/logger')(require('../package.json').name);

const { name, description, version } = require('../package.json');

const initJwt = require('./libs/jwt');
const initAuth = require('./libs/auth');
const createLoginRoutes = require('./routes/login');
const createInviteRoutes = require('./routes/invite');
const createIssuePassportAuth = require('./routes/issue-passport');
const createLostPassportListAuth = require('./routes/lost-passport-list');
const createLostPassportIssueAuth = require('./routes/lost-passport-issue');
const createSessionRoutes = require('./routes/session');
const createEnvRoutes = require('./routes/env');
const createBlockletRoutes = require('./routes/blocklet-info');
const createPassportRoutes = require('./routes/passport');
const { init: initNotification } = require('./routes/notification');
const { init: initStates } = require('./state/index');

const defaultOptions = {
  dataDir: '',
  sessionSecret: 'your_should_change_this',
  sessionTtl: '7d',
  webWalletUrl: 'https://web.abtwallet.io',
};

const init = ({ node, httpRouter, serviceHttpRouter, wsRouter, options }) => {
  if (!options.dataDir) {
    throw new Error('Auth service requires dataDir to start');
  }
  if (!options.sessionSecret) {
    throw new Error('Auth service requires sessionSecret to start');
  }

  const isProduction = process.env.NODE_ENV === 'production' || process.env.ABT_NODE_SERVICE_ENV === 'production';
  const isE2E = process.env.NODE_ENV === 'e2e';
  const distDir = path.resolve(__dirname, '../build');

  logger.info('init params', { isProduction, distDir });

  const opts = { ...defaultOptions, ...options, name, description, version, prefix: NODE_SERVICES_PREFIX.AUTH_SERVICE };
  if (opts.sessionSecret === defaultOptions.sessionSecret) {
    logger.warn('session secret should be set to make things secure');
  }

  /**
   * check auth
   * set user info to req.user and req.headers[x-user-xxx]
   *
   * @returns {object} res
   *  {boolean} res.blocked
   *  {boolean} res.authenticated
   *  {boolean} res.authorized
   */
  const ensureAuth = async ({ req, token } = {}) => {
    try {
      if (token) {
        const teamDid = req.getBlockletDid();
        const result = await verify(token, teamDid);
        req.user = result;
      }
    } catch {
      // Do nothing
    }

    if (req.user) {
      // set headers
      req.headers['x-user-did'] = req.user.did;
      req.headers['x-user-fullname'] = encodeURIComponent(req.user.fullName);
      req.headers['x-user-role'] = req.user.role;
      req.headers['x-connected-did'] = req.user.did;
    } else {
      delete req.headers['x-user-did'];
      delete req.headers['x-user-fullname'];
      delete req.headers['x-user-role'];

      if (req.cookies.connected_did) {
        req.headers['x-connected-did'] = req.cookies.connected_did;
      } else {
        delete req.headers['x-connected-did'];
      }
    }

    if (!isProduction && !isE2E) {
      return {};
    }

    const config = (await req.getServiceConfig(NODE_SERVICES.AUTH_SERVICE)) || {};

    const ignoreUrls = (get(config, 'ignoreUrls') || []).filter(Boolean);

    const shouldIgnore = ignoreUrls.some((s) => minimatch(req.url, s)) || req.url.startsWith(`${opts.prefix}/api`);
    if (shouldIgnore) {
      return {};
    }

    if (config.blockUnauthenticated === false) {
      return {};
    }

    if (!req.user) {
      return { blocked: true, authenticated: false };
    }

    if (!config.blockUnauthorized) {
      return {};
    }

    const rule = await req.getRoutingRule();
    if (rule.to && rule.to.interfaceName) {
      const permissionName = genPermissionName(rule.to.interfaceName);
      const teamDid = req.getBlockletDid();
      const rbac = await node.getRBAC(teamDid);

      if (await rbac.can(req.user.role, ...permissionName.split('_'))) {
        return {};
      }

      return { blocked: true, authenticated: true, authorized: false };
    }

    return {};
  };

  initStates(opts.dataDir);

  initNotification(node, httpRouter, wsRouter, opts);

  const { login, verify } = initJwt(node, opts);
  const { authenticator, handlers } = initAuth(node, opts);

  // Serve static assets from the service
  if (isProduction || isE2E) {
    httpRouter.use(opts.prefix, serveStatic(distDir, { index: false }));
  }

  // public http api
  createEnvRoutes.init(httpRouter, node, opts);
  createBlockletRoutes.init(httpRouter, node, opts);
  createPassportRoutes.init(httpRouter, node, opts);

  // public page
  [`${WELLKNOWN_AUTH_PATH_PREFIX}/issue-passport`, `${WELLKNOWN_AUTH_PATH_PREFIX}/lost-passport`].forEach((x) => {
    serviceHttpRouter.get(x, (req, res) => {
      res.sendFile(path.join(distDir, 'index.html'));
    });
  });

  // public did-auth api
  handlers.attach(Object.assign({ app: serviceHttpRouter }, createLoginRoutes(node, authenticator, login, opts)));
  handlers.attach(Object.assign({ app: serviceHttpRouter }, createInviteRoutes(node, authenticator, login, opts)));
  handlers.attach(Object.assign({ app: serviceHttpRouter }, createIssuePassportAuth(node, authenticator, login, opts)));
  handlers.attach(
    Object.assign({ app: serviceHttpRouter }, createLostPassportListAuth(node, authenticator, login, opts))
  );
  handlers.attach(
    Object.assign({ app: serviceHttpRouter }, createLostPassportIssueAuth(node, authenticator, login, opts))
  );

  // auth middleware for websocket connect
  wsRouter.use('**', async (req, socket, head, next) => {
    const { searchParams } = new URL(req.url, `http://${req.headers.host || 'unknown'}`);
    const token = searchParams.get('token');

    // ignore some dev path in dev mode
    const blocklet = await req.getBlocklet();
    const devUrls = ['/sockjs-node'];
    const mode = get(blocklet, 'mode');
    if (mode === BLOCKLET_MODES.DEVELOPMENT && devUrls.includes(req.url)) {
      next();
      return;
    }

    const { blocked, authenticated, authorized } = await ensureAuth({ req, token });
    if (blocked) {
      let message = '401 Unauthorized';
      if (authenticated && !authorized) {
        message = '403 Forbidden';
      }
      socket.write(`HTTP/1.1 ${message}\r\n\r\n`);
      socket.destroy();
      return;
    }

    next();
  });

  // check blocklet running
  serviceHttpRouter.use(async (req, res, next) => {
    const blocklet = await req.getBlocklet();
    if (blocklet.status !== BlockletStatus.running) {
      res.status(500).json({ code: 'error', error: 'blocklet is not running' });
      return;
    }

    next();
  });

  // auth middleware for http request
  // https://www.npmjs.com/package/express-bearer-token
  serviceHttpRouter.use(
    bearerToken({
      headerKey: 'DISABLED',
      cookie: {
        signed: true,
        secret: opts.sessionSecret,
        key: 'login_token',
      },
    })
  );

  serviceHttpRouter.use(async (req, res, next) => {
    const { token } = req;
    const { blocked, authenticated, authorized } = await ensureAuth({ req, token });

    if (blocked) {
      if (!authenticated) {
        if (req.path !== '/') {
          // redirect to root path for login
          // redirect to target path after login
          const baseUrl = getBaseUrl(req);
          const redirect = encodeURIComponent(`${baseUrl.replace(/\/+$/, '')}/${req.url.replace(/^\/+/, '')}`);
          res.redirect(`${baseUrl.replace(/\/+$/, '')}/?redirect=${redirect}`);
        } else {
          res.sendFile(path.join(distDir, 'index.html'));
        }
        return;
      }

      if (!authorized) {
        res.status(403).json({ code: 'forbidden', error: 'not allowed' });
        return;
      }
    }

    next();
  });

  // Note: this api depends on the user decoding logic
  createSessionRoutes.init(serviceHttpRouter, node, opts);

  // Block /.service/@abtnode/auth-service/<invalid_path>
  serviceHttpRouter.use((req, res, next) => {
    if (req.path.startsWith(opts.prefix)) {
      res.status(400).send('Bad request');
      return;
    }

    next();
  });
};

module.exports = {
  name,
  description,
  version,
  init,
  validators,
};
