const { Telegraf } = require('telegraf')
const DOMPurify = require('dompurify')
const { JSDOM } = require('jsdom')
const { window } = new JSDOM('<!DOCTYPE html>')
const domPurify = DOMPurify(window)

const DEFAULT_IMG_URL = "https://gancio.org/assets/gancio.png"
const NBSP = "\u00a0"

const plugin = {
  configuration: {
    name: 'Telegram',
    author: 'fadelkon',
    url: 'https://framagit.org/bcn.convocala/gancio-plugin-telegram-bridge',
    description: 'Republishes content to Telegram channels or groups. The goal is to spread the info of our networks to the capitalist cyberspace, and pull otherwise isolated people to our radical and free part of the internet.',
    settings: {
      auth_token: {
        type: 'TEXT',
        description: 'Auth token',
        required: true,
        hint: 'Your Telegram\'s <strong>bot token</strong> you want to impersonate.<br/><a href="https://core.telegram.org/bots#6-botfather">Help on telegram bots</a>'
      },
      chat_id: {
        type: 'TEXT',
        description: 'Channel id',
        required: true,
        hint: 'The <strong>channel/group id</strong> where to publish the messages.<br/><a href="https://github.com/GabrielRF/telegram-id">Help on getting chat ids</a>'
      },
      disable_tags_link_to_gancio: {
        type: 'CHECK',
        description: 'Send Telegram tags instead of tags linked to this gancio',
        hint: "By default an event's tags will link to the tag on Gancio. When checked, they will be handled by Telegram."
      },
      skip_final_link_to_gancio: {
        type: 'CHECK',
        description: 'Skip the final link to Gancio',
        hint: "By default a link with Gancio's title will be added to each event. When checked, it is skipped"
      },
      send_long_descriptions: {
        type: 'CHECK',
        description: 'Reply messages of events with long descriptions cropped by telegram limits.',
        hint: 'Text replies can be longer than image captions.'
      }
    }
  },
  gancio: null, // { helpers, log, settings }
  log: null,
  bot: null,
  settings: null,

  load(gancio, settings) {
    plugin.gancio = gancio // contains all gancio settings, including all plugins settings
    plugin.log = gancio.log // just the logger
    plugin.settings = settings // this plugin settings


    plugin.bot = new Telegraf(settings.auth_token)
    plugin.log.info("Telegram plugin loaded!")
  },

  onTest() {
      plugin.log.debug("Send test message to Telegram")
      let image_url = DEFAULT_IMG_URL;
      let html_msg = '<b>GANCIO TELEGRAM PLUGIN TEST</b>\n\n' +
      'It\'s <i>working</i>! This means that the supplied <code>chat id</code> and the <code>bot key</code> are correct\n\n' +
      'However, if event messages are not sent, make sure that your gancio is serving from a public URL.\n' +
      'If it is not, then telegram won\'t be able to fetch your images and so will fail with a "bad url" error.\n\n' +
      'If it is but still messages are not sent, it may be due to a violation of Telegram\'s API rules, such as message size.\n' +
      'If that\'s the case, please, report it to the plugin development team at https://framagit.org/bcn.convocala/gancio-plugin-telegram-bridge/'
      plugin.bot.telegram.sendPhoto(
        plugin.settings.chat_id, image_url, { "parse_mode": "HTML", "caption": html_msg }
      ).then(logOk, logErr)
  },

  onEventCreate(event) {
    plugin.log.debug("Send new event to Telegram")
    if (!event.is_visible) {
      return
    }
    let image_url = `${plugin.gancio.settings.baseurl}/logo.png`;
    if (event.media && event.media.length) {
      image_url = `${plugin.gancio.settings.baseurl}/media/${event.media[0].url}`
    }
    if (image_url.includes("localhost")) {
      plugin.log.warn("Replacing media with gancio's logo, as telegram won't be able to fetch an image hosted in a private URL")
      image_url = DEFAULT_IMG_URL
    }
    // Check length. Telegram counts in UTF-16 code units, same as javascript. We can use String.lengt() directly
    // Media captions: 1024 c.u.; Text messages: 4096 c.u.
    // Telegram: https://core.telegram.org/api/entities#entity-length
    // Javascript: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/length
    let [head, body, feet] = renderEvent(event);
    let message = head + body + feet;
    let extra_msg = "";
    let read_more_text = " […]";
    let L1 = 1024;
    let L2 = 4096;
    if (message.length > L1) {
      // clone body string
      let new_body = `${body}`
      // remove html as we don't want to deal with opening/closing tags
      new_body = rmFormat(new_body);
      // if the removal of html is enough to fit in the message, we are good to go
      if(head.length + new_body.length + feet.length < L1) {
        message = "".concat(head, new_body, feet);
      } else {
        max_body_len = L1 - head.length - feet.length - read_more_text.length;
        let i = max_body_len;
        let last_codepoint_out
        // crop message by max length, and do it until we hit the end of a word (space)
        do {
          last_codepoint_out = new_body[i];
          new_body = new_body.slice(0, i);
          i--;
        } while (last_codepoint_out != " " && i>=0)
        message = "".concat(head, new_body, read_more_text, feet);
        extra_msg = body.slice(0, L2)
      }
    }
    // send message and keep the id that tg should return
    // check if extra_msg is empty or not, and if the option of sending the 2d message is enabled.
    // if so, send the second msg as a reply to the 1st
    plugin.bot.telegram.sendPhoto(
      plugin.settings.chat_id,
      image_url,
      { "parse_mode": "HTML", "caption": message }
    ).then(msg => {
        logOk(msg);
        if (extra_msg.length > 0 && plugin.settings.send_long_descriptions) {
          plugin.bot.telegram.sendMessage(
            plugin.settings.chat_id,
            extra_msg,
            { "parse_mode": "HTML", "reply_to_message_id": msg.message_id }
          ).then(logOk, logErr)
        }
      },
      logErr
    )
  },

  onEventUpdate(event) {
    plugin.log.info(`Event "${event.title}" updated`)
    plugin.log.debug(event)
  },

  onEventDelete(event) {
    plugin.log.info(`Event "${event.title}" deleted`)
    plugin.log.debug(event)
  }
}

function logOk(data) {
    plugin.log.debug("Telegram message sent successfully: " + JSON.stringify(data))
}

function logErr(data){
    plugin.log.error("Telegram message not sent because some error happened: " + JSON.stringify(data))
}

function preprocessHtml(html) {
  return html
        .replace(/\n/g, "") // ignore html non-visual line breaks
        .replace(/<br ?\/?>/g, "\n") // tg format hack for br
        .replace(/<li><p>/g, "<li>") // remove p inside li
        .replace(/<li>/g, "\n  • ") // tg format hack for lists
        .replace(/<\/p><\/li>/g, "</li>") // remove p inside li
        .replace(/<\/[uo]l>/g, "\n\n") // leave double space after a list end
        .replace(/<p>/g, "") // ignore initial p
        .replace(/<\/p>/g, "\n\n") // double line after a p
        .replace(/\r?\n<[uo]l>/g, "") // remove 1 line break before list <li>
}

// Telegram only allows a few html html tags, just for very basic formatting and code
function cleanHtml(html) {
    // save some formatting (line breaks, lists)
    html = preprocessHtml(html)
    // call gancio's sanitize to get a standard clean, with its hooks
    html = plugin.gancio.helpers.sanitizeHTML(html);
    // call sanitize again, but with telegram's reduced allowed tags
    // source: https://core.telegram.org/api/entities#allowed-entities
    return domPurify.sanitize(html, {
      ALLOWED_TAGS: ['a', 'i', 'em', 'b', 'strong', 'code', 'pre', 'u', 's', 'strike', 'del'],
      ALLOWED_ATTR: ['href', 'language']
    })
}

function rmFormat(html) {
  return domPurify.sanitize(
    preprocessHtml(html),
    { ALLOWED_TAGS: [], ALLOWED_ATTR: [] }
  )
}

function renderPlace(place) {
    let prefix = `📌${NBSP}`
    let infix = `<a href="${plugin.gancio.settings.baseurl}/place/${place.name}">${place.name}</a>`
    let sufix = ""

    if (place.longitude && place.latitude) {
        sufix = ` 🗺${NBSP}<a href="https://www.openstreetmap.org/?mlat=${place.latitude}&mlon=${place.longitude}#map=19/${place.latitude}/${place.longitude}">map</a>`
    }


    return `${prefix}${infix}${sufix}\n`
}

function renderOnlinePlaces(urls) {
    if (urls.length) {
        return `🔗${NBSP}` +
            urls
            .map(url => `<a href="${url}">${url.split("/")[2]}</a>`)
            .join(` 🔗${NBSP}`) +
            '\n'
    }
    return ""
}

function renderEventTitle(event) {
    // I think this makes sense, but linking could be optional
    let eventLink = `${plugin.gancio.settings.baseurl}/event/${event.slug}`
    if (eventLink.includes("localhost")) {
        plugin.log.warn("Replacing title link with gancio's website, as telegram removes links pointing to private URL")
        eventLink = "https://gancio.org"
    }
    return `<b><a href="${eventLink}">${event.title}</a></b>\n\n`
}

function renderEventDescription(event) {
    // In certain cases, imported events have description 'null' (as string)
    if (event.description && event.description !== "null") {
        return `${cleanHtml(event.description)}\n`
    }
    return ""
}

function renderEventDateTime(event) {
    // I think this should be correct(!) please test with different server timezones
    const eventTime = new Date(event.start_datetime * 1000)
    const localeGancio = plugin.gancio.settings.instance_locale
    const tzGancio = plugin.gancio.settings.instance_timezone
    const dateFormat = { timeZone: tzGancio }
    const timeFormat = { hour: "2-digit", minute: "2-digit", ...dateFormat }
    return `📅${NBSP}${eventTime.toLocaleDateString([localeGancio], dateFormat)} ⏰${NBSP}${eventTime.toLocaleTimeString([localeGancio], timeFormat)}\n`
}

function renderEventTag(tag) {
    const tagText = tag.tag.replaceAll(" ", "_")
    if (! plugin.settings.disable_tags_link_to_gancio)
        return `<a href="${plugin.gancio.settings.baseurl}/tag/${tag.tag}">#${tagText}</a>`
    return `#${tagText}`
}

function renderEventTags(event) {
    let tags = event.tags.reduce((x, y) => { return `${x} ${renderEventTag(y)}` }, "").trim();
    if (tags == "") return ""
    return tags + "\n"
}

function renderGancioSite() {
    if (plugin.settings.skip_final_link_to_gancio)
        return "";
    return `\n👉${NBSP}<a href="${plugin.gancio.settings.baseurl}">${plugin.gancio.settings.title}</a>`
}

function renderEvent(event) {
  return [
      renderEventTitle(event).trim() + "\n\n", // HEAD
      renderEventDescription(event).trim(), // BODY
      "\n\n".concat(
          renderEventDateTime(event),
          renderPlace(event.place),
          renderOnlinePlaces(event.online_locations),
          renderEventTags(event),
          renderGancioSite()
      ).trimEnd() // FEET
  ];
}

module.exports = plugin
