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: 'Send a second message with the event description if it doesn\'t fit in just one message',
        hint: 'In Telegram, text messages can be four times longer than captions of image messages, so if the whole rendered message is too long, we can take its description out to a second message'
      }
    }
  },
  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, please check the error logs.\n' +
      'Message size limit should be under control already, but you may be hitting another Telegram API restriction.\n\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 = " […]\n\n";
    let ellipsis = "…";
    let title_max_len = 70;
    let L1 = 1024;
    let L2 = 4096;
    if (message.length > L1) {
      plugin.log.debug("Message length is longer than "+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) {
        plugin.log.debug("After removing format of description, message length is less than "+L1)
        message = "".concat(head, new_body, feet);
      } else {
        plugin.log.debug("After removing format of description, message length is still more than "+L1)
        max_body_len = L1 - head.length - feet.length - read_more_text.length;
        if(max_body_len < 0) {
            // remove tag links of type <a href="https:.../tag/some-tag-name">#someTagName</a>
            // can't use here rm_format because there's also the gancio instance, place and map links
            pattern = /<a href="[^"]+\/tag\/[^"]+">(#[^<]+)<\/a>/g
            feet = feet.replace(pattern, "$1");
        }
        max_body_len = L1 - head.length - feet.length - read_more_text.length;

        // What if title and footer are too long? Taking body away won't be enough!
        if(max_body_len < 0) {
            // crop title text to a bare minimum
            let head_sufix = '</a></b>\n\n'
            let max_head_len = L1 - feet.length - head.replace(event.title, "").length - head_sufix.length
            if (max_head_len > 0) {
                head = head.slice(0, max_head_len - ellipsis.length) + ellipsis + head_sufix;
            } else {
                // cry. does this event really deserve to be published? :_)
                // crop title to a reasonble length
                // remove title link?
                // crop tags list without deleting the server link
                // TODO: implement this in the renderer functions instead of reimplementing those outside with nested ifs
            }
        }
        if (plugin.settings.send_long_descriptions) {
            // if we send a second message, just skip the cropped description in the first one
            new_body = "";
            read_more_text = "";
        } else {
            // crop message by max length, and do it until we hit the end of a word (space)
            last_valid_space_idx = body.lastIndexOf(" ", max_body_len);
            new_body = new_body.slice(0, last_valid_space_idx);

            // check if it worked or fall-back
            if(new_body.length >= max_body_len) {
                console.warn("Cropping by spaces didn't work. Are they special blanks? Cropping regardless of breaking words...");
                new_body = new_body.slice(0, max_body_len);
            }
        }
        plugin.log.debug("Message cropped should be now shorter than "+L1+"; body is "+new_body)
        message = "".concat(head, new_body, read_more_text, feet);
        // TODO: manage possible html-tag breaks from cropping.
        // TODO: append mark at the end if cropped [...]
        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(/<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) {
    plugin.log.debug(`Rendering place ${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) {

    plugin.log.debug(`Rendering online places ${urls}`);
    if (urls.length) {
        return `🔗${NBSP}` +
            urls
            .map(url => `<a href="${url}">${url.split("/")[2]}</a>`) // present links without protocol prefix
            .join(` 🔗${NBSP}`) +
            '\n'
    }
    return ""
}

//function renderEventTitle(event, maxLength) {
function renderEventTitle(event) {

    plugin.log.debug(`Rendering event title ${event.slug} = ${event.title}`);
    // 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, maxLength) {
function renderEventDescription(event) {
    plugin.log.debug(`Rendering event description ${event.description}`);
    // In certain cases, imported events have description 'null' (as string)
    if (event.description && event.description !== "null") {
        return `${cleanHtml(event.description)}\n`
    }
    return ""
}

function formatTime(dt) {
    const tzGancio = plugin.gancio.settings.instance_timezone
    const formatOpts = { timeZone: tzGancio }
    const timeFormat = { hour: "2-digit", minute: "2-digit", ...formatOpts }
    const localeGancio = plugin.gancio.settings.instance_locale
    return dt.toLocaleTimeString([localeGancio], timeFormat);
}

function formatDate(dt) {
    const tzGancio = plugin.gancio.settings.instance_timezone
    const formatOpts = { timeZone: tzGancio }
    const localeGancio = plugin.gancio.settings.instance_locale
    return dt.toLocaleDateString([localeGancio], formatOpts);
}

function renderEventDateTime(event) {
    plugin.log.debug(`Rendering event date ${event.start_datetime} - ${event.end_datetime}`);
    // I think this should be correct(!) please test with different server timezones
    const dtStart = new Date(event.start_datetime * 1000)
    const dtEnd = new Date(event.end_datetime * 1000)
    if (event.multidate) {
        return `📅${NBSP}${formatDate(dtStart)} - ${formatDate(dtEnd)}\n`
    }
    if (event.end_datetime) {
        return `📅${NBSP}${formatDate(dtStart)} ⏰${NBSP}${formatTime(dtStart)}${NBSP}-${NBSP}${formatTime(dtEnd)}\n`
    }
    return `📅${NBSP}${formatDate(dtStart)} ⏰${NBSP}${formatTime(dtStart)}\n`
}

function renderEventTag(tag, withLink) {
    plugin.log.debug(`Rendering event tag ${tag}`);
    const tagText = tag.tag.replaceAll(" ", "_");
    if(withLink)
        return `<a href="${plugin.gancio.settings.baseurl}/tag/${tag.tag}">#${tagText}</a>`
    else
        return `#${tagText}`
}

// function renderEventTags(event, withTagLinks: boolean, maxLength: int) {
function renderEventTags(event, withTagLinks) {

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

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

function renderEvent(event) {
    // config
    let withTagLinks = ! plugin.settings.disable_tags_link_to_gancio;
    let appendGancioSite = ! plugin.settings.skip_final_link_to_gancio;

    let description = renderEventDescription(event).trim();
    let feet_margin_top = "\n\n";
    if(description.length > 0 )
    feet_margin_top = "";

    return [
      renderEventTitle(event).trim() + "\n\n", // HEAD
      description, // BODY
      feet_margin_top.concat(
          renderEventDateTime(event),
          renderPlace(event.place),
          renderOnlinePlaces(event.online_locations),
          renderEventTags(event, withTagLinks),
          renderGancioSite(appendGancioSite)
      ).trimEnd() // FEET
    ];
}

module.exports = plugin
