// site-ioserver.js
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0

'use strict';

const DTP_COMPONENT = { name: 'I/O Server', slug: 'ioserver', prefix: 'srv' };
const path = require('path');

const Redis = require('ioredis');

const mongoose = require('mongoose');
const ConnectToken = mongoose.model('ConnectToken');

const marked = require('marked');

const { SiteLog } = require(path.join(__dirname, 'site-log'));

const Events = require('events');
class SiteIoServer extends Events {

  constructor (dtp) {
    super();
    this.dtp = dtp;
    this.log = new SiteLog(dtp, DTP_COMPONENT);
  }

  async start (httpServer) {
    const { Server } = require('socket.io');
    const { createAdapter } = require('@socket.io/redis-adapter');

    this.markedRenderer = new marked.Renderer();
    this.markedRenderer.link = (href, title, text) => { return text; };
    this.markedRenderer.image = (href, title, text) => { return text; };
    this.markedRenderer.image = (href, title, text) => { return text; };

    const hljs = require('highlight.js');
    this.markedConfig = {
      renderer: this.markedRenderer,
      highlight: function(code, lang) {
        const language = hljs.getLanguage(lang) ? lang : 'plaintext';
        return hljs.highlight(code, { language }).value;
      },
      langPrefix: 'hljs language-', // highlight.js css expects a top-level 'hljs' class.
      pedantic: false,
      gfm: true,
      breaks: false,
      sanitize: false,
      smartLists: true,
      smartypants: false,
      xhtml: false,
    };

    const pubClient = new Redis({
      host: process.env.REDIS_HOST,
      port: process.env.REDIS_PORT,
      password: process.env.REDIS_PASSWORD,
      key: process.env.REDIS_PREFIX,
    });
    pubClient.on('error', this.onRedisError.bind(this));

    const subClient = pubClient.duplicate();
    subClient.on('error', this.onRedisError.bind(this));

    const transports = ['websocket'/*, 'polling'*/];
    const adapter = createAdapter(pubClient, subClient);
    this.io = new Server(httpServer, { adapter, transports });

    this.io.on('connection', this.onSocketConnect.bind(this));
  }

  async onRedisError (error) {
    this.log.error('Redis error', { error });
  }

  async stop ( ) {

  }

  async onSocketConnect (socket) {
    this.log.debug('socket connection', { sid: socket.id });
    const token = await ConnectToken.findOne({ token: socket.handshake.auth.token }).populate('user').lean();
    if (!token) {
      this.log.alert('rejecting invalid socket token', {
        sid: socket.sid,
        handshake: socket.handshake,
      });
      socket.close();
      return;
    }

    if (token.claimed) {
      this.log.alert('rejecting use of claimed connect token', {
        sid: socket.id,
        handshake: socket.handshake,
      });
      socket.close();
      return;
    }

    await ConnectToken.updateOne(
      { _id: token._id },
      { $set: { claimed: new Date() } },
    );
    this.log.debug('token claimed', {
      sid: socket.id,
      token: socket.handshake.auth.token,
      user: token.user._id,
    });

    const session = {
      user: {
        _id: token.user._id,
        type: token.userType,
        created: token.user.created,
        username: token.user.username,
        displayName: token.user.displayName,
        picture: token.user.picture,
      },
      socket,
    };

    session.onSocketDisconnect = this.onSocketDisconnect.bind(this, session);
    session.onJoinChannel = this.onJoinChannel.bind(this, session);
    session.onLeaveChannel = this.onLeaveChannel.bind(this, session);
    session.onUserChat = this.onUserChat.bind(this, session);
    session.onUserReact = this.onUserReact.bind(this, session);

    socket.on('disconnect', session.onSocketDisconnect);
    socket.on('join', session.onJoinChannel);
    socket.on('leave', session.onLeaveChannel);
    socket.on('user-chat', session.onUserChat);
    socket.on('user-react', session.onUserReact);

    socket.emit('authenticated', {
      message: 'token verified',
      user: session.user,
    });
  }

  async onSocketDisconnect (session, reason) {
    this.log.debug('socket disconnect', { sid: session.socket.id, user: session.user._id, reason });
    session.socket.off('disconnect', session.onSocketDisconnect);
    session.socket.off('join', session.onJoinChannel);
    session.socket.off('leave', session.onLeaveChannel);
  }

  async onJoinChannel (session, message) {
    const { channelId } = message;
    this.log.debug('socket joins channel', { sid: session.socket.id, user: session.user._id, channelId });
    session.socket.join(channelId);
    session.socket.emit('join-result', { channelId });
  }

  async onLeaveChannel (session, message) {
    const { channelId } = message;
    this.log.debug('socket leaves channel', { sid: session.socket.id, user: session.user._id, channelId });
    session.socket.leave(channelId);
  }

  async onUserChat (session, messageDefinition) {
    const { chat: chatService } = this.dtp.services;

    if (!messageDefinition.content || (messageDefinition.content.length === 0)) {
      return;
    }

    try {
      const { message, payload } = await chatService.createMessage(session.user, messageDefinition);
      message.author = session.user;
      payload.html = await chatService.renderTemplate('chatMessage', { user: session.user, message });
      await chatService.sendMessage(message.channel, 'user-chat', payload);
    } catch (error) {
      this.log.error('failed to process user chat message', { error });
      await chatService.sendSystemMessage(`Failed to send chat: ${error.message}`, {
        type: 'error',
        userId: session.user._id.toString(),
      });
      return;
    }
  }

  async onUserReact (session, message) {
    const { chat: chatService } = this.dtp.services;
    try {
      const reaction = await chatService.createEmojiReaction(session.user, message);
      reaction.user = session.user;

      const payload = { reaction };
      const channelId = reaction.subject.toString();

      await chatService.sendMessage(channelId, 'user-react', payload);
    } catch (error) {
      this.log.error('failed to process reaction', { message, error });
      session.socket.emit('system-message', {
        created: new Date(),
        content: `You are not allowed to chat on ${this.dtp.config.site.name}.`,
      });
      return;
    }
  }
}

module.exports.SiteIoServer = SiteIoServer;