import * as uws from 'uws';
import * as http from 'http';
import * as https from 'https';
import * as uuid from 'uuid';
import * as querystring from 'querystring';
import { Observable, Subscription, merge } from 'rxjs';
import { filter } from 'rxjs/operators';
import { logger, LogMessageType } from './logger';
import { getContainersStats } from './docker-stats';
import {
  processes,
  startBuild,
  jobEvents,
  restartJob,
  stopJob,
  debugJob,
  restartBuild,
  stopBuild,
  terminalEvents
} from './process-manager';
import { imageBuilderObs, buildDockerImage, deleteImage } from './image-builder';
import { getConfig } from './setup';
import { readFileSync } from 'fs';
import * as express from 'express';
import { sessionParser } from './server';
import { IMemoryData, memory } from './stats/memory';
import { ICpuData, cpu } from './stats/cpu';
import { decodeJwt } from './security';
import { getLastBuild } from './db/build';

export interface ISocketServerOptions {
  app: express.Application;
}

export interface IOutput {
  type: string;
  data: IMemoryData | ICpuData;
}

export interface Client {
  sessionID: string;
  session: { cookie: any, ip: string, userId: number, email: string, isAdmin: boolean };
  socket: uws.Socket;
  send: Function;
  subscriptions: { stats: Subscription, jobOutput: Subscription, logs: Subscription };
}

export class SocketServer {
  options: ISocketServerOptions;
  connections: Observable<any>;
  clients: Client[];

  constructor(options: ISocketServerOptions) {
    this.options = options;
    this.clients = [];
  }

  start(): Observable<string> {
    return new Observable(observer => this.setupServer(this.options.app));
  }

  private setupServer(application: any): void {
    let config: any = getConfig();
    let server = null;

    if (config.ssl) {
      server = https.createServer({
        cert: readFileSync(config.sslcert),
        key: readFileSync(config.sslkey)
      }, application);
    } else {
      server = http.createServer(application);
    }

    let wss: uws.Server = new uws.Server({
      verifyClient: (info: any, done) => {
        let ip = info.req.headers['x-forwarded-for'] || info.req.connection.remoteAddress;
        let query = querystring.parse(info.req.url.substring(2));
        let user = { id: null, email: 'anonymous', isAdmin: false };

        if (query.token) {
          let userData = decodeJwt(query.token as string);
          if (userData) {
            user.id = userData.id;
            user.email = userData.email;
            user.isAdmin = userData.isAdmin;
          }
        }

        let msg: LogMessageType = {
          message: `[socket]: user ${user.email} connected from ${ip}`,
          type: 'info',
          notify: false
        };
        logger.next(msg);

        sessionParser(info.req, {} as any, () => {
          info.req.session.ip = ip;
          info.req.session.userId = user.id;
          info.req.session.email = user.email;
          done(info.req.session);
        });
      },
      server: server
    });

    wss.on('connection', socket => {
      let client: Client = {
        sessionID: socket.upgradeReq.sessionID,
        session: socket.upgradeReq.session,
        socket: socket,
        send: (message: any) => client.socket.send(JSON.stringify(message)),
        subscriptions: { stats: null, jobOutput: null, logs: null }
      };
      this.addClient(client);

      client.send({ type: 'time', data: new Date().getTime() });
      jobEvents.subscribe(event => {
        if (event.data === 'build added') {
          getLastBuild(client.session.userId)
            .then(lastBuild => {
              event.additionalData = lastBuild;
              client.send(event);
            });
        } else {
          client.send(event);
        }
      });

      socket.on('message', event => this.handleEvent(JSON.parse(event), client));
      socket.on('close', (code, message) => this.removeClient(socket));
    });

    server.listen(config.port, () => {
      let msg: LogMessageType = {
        message: `[server]: API and Socket Server running at port ${config.port}`,
        type: 'info',
        notify: false
      };
      logger.next(msg);
    });
  }

  private addClient(client: Client): void {
    this.clients.push(client);
  }

  private removeClient(socket: uws.Socket): void {
    let index = this.clients.findIndex(c => c.socket === socket);
    let client = this.clients[index];

    Object.keys(client.subscriptions).forEach(sub => {
      if (client.subscriptions[sub]) {
        client.subscriptions[sub].unsubscribe();
      }
    });

    let msg: LogMessageType = {
      message: `[socket]: user ${client.session.email} from ${client.session.ip} disconnected`,
      type: 'info',
      notify: false
    };
    logger.next(msg);

    this.clients.splice(index, 1);
  }

  private handleEvent(event: any, client: Client): void {
    switch (event.type) {
      case 'login': {
        let token = event.data;
        let decoded = !!token ? decodeJwt(token) : false;
        client.session.userId = decoded ? decoded.id : null;
        client.session.email = decoded ? decoded.email : 'anonymous';
        client.session.isAdmin = decoded ? decoded.admin : false;
      }
        break;

      case 'logout': {
        let email = client.session.email;
        let userId = client.session.userId;
        client.session.userId = null;
        client.session.email = 'anonymous';
        client.session.isAdmin = false;
      }
        break;

      case 'buildImage': {
        if (client.session.email === 'anonymous') {
          client.send({ type: 'error', data: 'not authorized' });
        } else {
          client.send({ type: 'request_received' });
          let imageData = event.data;
          buildDockerImage(imageData);
        }
      }
        break;

      case 'deleteImage': {
        if (client.session.email === 'anonymous') {
          client.send({ type: 'error', data: 'not authorized' });
        } else {
          client.send({ type: 'request_received' });
          let imageData = event.data;
          deleteImage(imageData);
        }
      }
        break;

      case 'subscribeToImageBuilder': {
        if (client.session.email === 'anonymous') {
          client.send({ type: 'error', data: 'not authorized' });
        } else {
          client.send({ type: 'request_received' });
          imageBuilderObs.subscribe(e => {
            client.send({ type: 'imageBuildProgress', data: e });
          });
        }
      }
        break;

      case 'stopBuild':
        if (client.session.email === 'anonymous') {
          client.send({ type: 'error', data: 'not authorized' });
        } else {
          client.send({ type: 'request_received' });
          stopBuild(event.data.buildId)
            .then(() => {
              client.send({ type: 'build stopped', data: event.data.buildId });
            });
        }
        break;

      case 'restartBuild':
        if (client.session.email === 'anonymous') {
          client.send({ type: 'error', data: 'not authorized' });
        } else {
          client.send({ type: 'request_received' });
          restartBuild(event.data.buildId)
            .then(() => {
              client.send({ type: 'build restarted', data: event.data.buildId });
            });
        }
        break;

      case 'restartJob':
        if (client.session.email === 'anonymous') {
          client.send({ type: 'error', data: 'not authorized' });
        } else {
          client.send({ type: 'request_received' });
          restartJob(parseInt(event.data.jobId, 10))
            .then(() => {
              client.send({ type: 'job restarted', data: event.data.jobId });
            });
        }
        break;

      case 'stopJob':
        if (client.session.email === 'anonymous') {
          client.send({ type: 'error', data: 'not authorized' });
        } else {
          client.send({ type: 'request_received' });
          stopJob(event.data.jobId)
            .then(() => {
              client.send({ type: 'job stopped', data: event.data.jobId });
            });
        }
        break;

      case 'debugJob':
        if (client.session.email === 'anonymous') {
          client.send({ type: 'error', data: 'not authorized' });
        } else {
          client.send({ type: 'request_received' });
          debugJob(event.data.jobId, event.data.debug)
            .then(() => {
              client.send({ type: 'job debug', data: event.data.jobId });
            });
        }
        break;

      case 'subscribeToJobOutput':
        let jobId = Number(event.data.jobId);
        let idx = processes.findIndex(proc => Number(proc.job_id) === jobId);
        if (idx !== -1) {
          let proc = processes[idx];
          client.send({ type: 'jobLog', data: proc.log });
          client.send({ type: 'exposed ports', data: proc.exposed_ports || null });
          client.send({ type: 'debug', data: proc.debug || null });
        }

        client.subscriptions.jobOutput = terminalEvents
          .pipe(filter(e => Number(e.job_id) === Number(event.data.jobId)))
          .subscribe(output => client.send(output));
        break;

      case 'subscribeToLogs':
        if (client.session.email === 'anonymous') {
          client.send({ type: 'error', data: 'not authorized' });
        } else {
          client.send({ type: 'request_received' });
          client.subscriptions.logs = logger.subscribe(msg => client.send(msg));
        }
        break;

      case 'subscribeToNotifications':
        if (client.session.email === 'anonymous') {
          client.send({ type: 'error', data: 'not authorized' });
        } else {
          client.send({ type: 'request_received' });
          logger.pipe(filter((msg: any) => !!msg.notify)).subscribe(msg => {
            let notify = { notification: msg, type: 'notification' };
            client.send(notify);
          });
        }
        break;

      case 'subscribeToStats':
      client.subscriptions.stats = merge(...[memory(), cpu(), getContainersStats()])
        .subscribe(e => client.send(e));
        break;

      case 'unsubscribeFromStats':
        if (client.subscriptions.stats) {
          client.subscriptions.stats.unsubscribe();
        }
        break;
    }
  }
}
