// venue.js
// Copyright (C) 2022 Digital Telepresence, LLC
// License: Apache-2.0

'use strict';

const mongoose = require('mongoose');

const VenueChannel = mongoose.model('VenueChannel');
const VenueChannelStatus = mongoose.model('VenueChannelStatus');

const https = require('https');
const fetch = require('node-fetch'); // jshint ignore:line

const striptags = require('striptags');
const slug = require('slug');

const CACHE_DURATION = 60 * 5;

const { SiteService, SiteError } = require('../../lib/site-lib');

class VenueService extends SiteService {

  constructor (dtp) {
    super(dtp, module.exports);
    this.soapboxDomain = process.env.DTP_SOAPBOX_HOST || 'shing.tv';
  }

  async start ( ) {
    const { user: userService } = this.dtp.services;

    process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
    this.httpsAgent = new https.Agent({
      rejectUnauthorized: false,
    });

    this.populateVenueChannel = [
      {
        path: 'owner',
        select: userService.USER_SELECT,
      },
    ];
  }

  channelMiddleware ( ) {
    return async (req, res, next) => {
      try {
        res.locals.venue = res.locals.venue || { };
        res.locals.venue.channels = [ ];
        await VenueChannel
          .find()
          .populate(this.populateVenueChannel)
          .lean()
          .cursor()
          .eachAsync(async (channel) => {
            channel.currentStatus = await this.updateChannelStatus(channel);
            res.locals.venue.channels.push(channel);
          });
        return next();
      } catch (error) {
        this.log.error('failed to populate Soapbox channel feed', { error });
        return next();
      }
    };
  }

  async createChannel (owner, channelDefinition) {
    const channel = new VenueChannel();

    channel.ownerType = owner.type;
    channel.owner = owner._id;

    channel.slug = this.getChannelSlug(channelDefinition.url);
    channel.sortOrder = parseInt(channelDefinition.sortOrder || '0', 10);

    if (!channelDefinition['credentials.streamKey'] || (channelDefinition['credentials.streamKey'] === '')) {
      throw new SiteError(400, 'Must provide a stream key');
    }

    if (!channelDefinition['credentials.widgetKey'] || (channelDefinition['credentials.widgetKey'] === '')) {
      throw new SiteError(400, 'Must provide a widget key');
    }

    channel.credentials = {
      streamKey: channelDefinition['credentials.streamKey'].trim(),
      widgetKey: channelDefinition['credentials.widgetKey'].trim(),
    };

    const status = await this.updateChannelStatus(channel);
    channel.name = status.name;
    channel.description = status.description;

    await channel.save();
    await this.updateChannelStatus(channel);

    return channel.toObject();
  }

  async updateChannel (channel, owner, channelDefinition) {
    const updateOp = { $set: { } };

    updateOp.$set.ownerType = owner.type;
    updateOp.$set.owner = owner._id;

    updateOp.$set.slug = this.getChannelSlug(channelDefinition.url);
    updateOp.$set.sortOrder = parseInt(channelDefinition.sortOrder || '0', 10);

    const status = await this.updateChannelStatus(channel);
    updateOp.$set.name = status.name;
    updateOp.$set.description = status.description;

    if (!channelDefinition['credentials.streamKey'] || (channelDefinition['credentials.streamKey'] === '')) {
      throw new SiteError(400, 'Must provide a stream key');
    }
    updateOp.$set['credentials.streamKey'] = channelDefinition['credentials.streamKey'].trim();

    if (!channelDefinition['credentials.widgetKey'] || (channelDefinition['credentials.widgetKey'] === '')) {
      throw new SiteError(400, 'Must provide a widget key');
    }
    updateOp.$set['credentials.widgetKey'] = channelDefinition['credentials.widgetKey'].trim();

    channel = await VenueChannel.findOneAndUpdate({ _id: channel._id }, updateOp, { new: true });
    await this.updateChannelStatus(channel);
  }

  async getChannels (pagination, options) {
    options = Object.assign({ featuredOnly: false, withCredentials: false }, options);

    const search = { };

    let q = VenueChannel
      .find(search)
      .sort({ sortOrder: 1, name: 1, slug: 1 });

    if (pagination) {
      q = q.skip(pagination.skip).limit(pagination.cpp);
    }
    if (options.withCredentials) {
      q = q.select('+credentials');
    }

    const channels = await q.populate(this.populateVenueChannel).lean();
    for await (const channel of channels) {
      channel.currentStatus = await this.updateChannelStatus(channel);
    }
    return channels;
  }

  async getChannelById (channelId, options) {
    options = Object.assign({ withCredentials: false }, options);
    let q = VenueChannel.findOne({ _id: channelId });
    if (options.withCredentials) {
      q = q.select('+credentials');
    }
    const channel = await q.populate(this.populateVenueChannel).lean();
    channel.currentStatus = await this.updateChannelStatus(channel);
    return channel;
  }

  async getChannelBySlug (channelSlug, options) {
    options = Object.assign({ withCredentials: false }, options);
    let q = VenueChannel.findOne({ slug: channelSlug.toLowerCase().trim() });
    if (options.withCredentials) {
      q = q.select('+credentials');
    }

    const channel = await q.populate(this.populateVenueChannel).lean();
    channel.currentStatus = await this.updateChannelStatus(channel);
    return channel;
  }

  async getChannelFeed (channel, options) {
    const { cache: cacheService } = this.dtp.services;
    const cacheKey = `venue:ch:${channel.slug}`;
    options = Object.assign({ allowCache: true }, options);

    let json;
    if (options.allowCache) {
      json = await cacheService.getObject(cacheKey);
      if (json) { return json; }
    }

    const requestUrl = `https://${this.soapboxDomain}/channel/${channel.slug}/feed/json`;
    this.log.info('fetching Shing channel feed', { slug: channel.slug, requestUrl });
    const response = await fetch(requestUrl, {
      agent: this.httpsAgent,
    });
    if (!response.ok) {
      throw new SiteError(500, `Failed to fetch Shing channel feed: ${response.statusText}`);
    }

    json = await response.json();
    await cacheService.setObjectEx(cacheKey, CACHE_DURATION, json);

    return json;
  }

  async updateChannelStatus (channel) {
    const requestUrl = `https://${this.soapboxDomain}/channel/${channel.slug}/status`;
    this.log.info('fetching Shing channel status', { slug: channel.slug, requestUrl });

    const response = await fetch(requestUrl, { agent: this.httpsAgent });
    if (!response.ok) {
      throw new SiteError(500, `Failed to fetch channel status: ${response.statusText}`);
    }

    const json = await response.json();
    if (!json.success) {
      throw new Error(`failed to fetch channel status: ${json.message}`);
    }

    return json.channel;
  }

  getChannelSlug (channelUrl) {
    const { URL } = require('url');
    const url = new URL(channelUrl);
    if (url.host !== this.soapboxDomain) {
      throw new SiteError(400, 'This is not a valid DTP stream channel URL: Domain mismatch.');
    }

    const channelUrlParts = url.pathname.split('/').filter((part) => part.length > 0);
    if (channelUrlParts[0] !== 'channel') {
      throw new SiteError(400, 'This is not a valid DTP stream channel URL: Not on channel path.');
    }

    return slug(striptags(channelUrlParts[1].trim()));
  }

  async removeChannel (channel) {
    await VenueChannelStatus.deleteMany({ channel: channel._id });
    await VenueChannel.deleteOne({ _id: channel._id });
  }
}

module.exports = {
  slug: 'venue',
  name: 'venue',
  create: (dtp) => { return new VenueService(dtp); },
};