/*
  StreamGoose Owncast backend — Go reference implementation
  
  Created by Luxferre in 2024, released into public domain
*/

package main

import (
  "fmt"
  "log"
  "strings"
  "slices"
  "time"
  "encoding/json"
  "net/http"
  "io/ioutil"
  "path/filepath"
)

/* global API root URL placeholder */
var OC_API_ROOT string
/* global API token placeholder */
var OC_API_TOKEN string
/* global debug flag */
var OC_DEBUG bool

const OC_ID_CACHE_LIMIT = 1000

/* global message ID cache */
var oc_msg_id_cache []string

/* API request helper function */
func oc_api_get(endpoint string) []interface{} {
  url := OC_API_ROOT + endpoint /* get full URL */
  req, err := http.NewRequest("GET", url, nil)
  req.Header.Set("Accept", "application/json; charset=utf-8")
  req.Header.Set("Authorization", "Bearer " + OC_API_TOKEN)

  client := &http.Client{}
  resp, err := client.Do(req)
  if err != nil {
    log.Fatal(err)
  }
  body, _ := ioutil.ReadAll(resp.Body)
  resp.Body.Close()

  /* print the response if the debug flag is set */
  if OC_DEBUG {
    log.Printf("[owncast] API response: %s", string(body))
  }

  /* unmarshal the response */
  
  var respbody []interface{}
  err = json.Unmarshal(body, &respbody)
  if err != nil {
    log.Fatal(err)
  }

  return respbody
}

/* main message procesing function */
func oc_process_message(rawmsg map[string]interface{}) {
  /* if the message came from a real user, this is the one we should process */
  if rawmsg["type"] == "CHAT" {
    /* first, check if we need to include this message at all */
    msg_hidden := false
    user_banned := false
    if _, ok := rawmsg["hiddenAt"]; ok && rawmsg["hiddenAt"].(string) != "" {
      msg_hidden = true
    }
    if _, ok := rawmsg["user"]; ok {
      if _, ok := rawmsg["user"].(map[string]interface{})["disabledAt"]; ok {
        if rawmsg["user"].(map[string]interface{})["disabledAt"].(string) != "" {
          user_banned = true
        }
      }
    }
    msg_included := slices.Contains(oc_msg_id_cache, rawmsg["id"].(string))
    if msg_hidden || user_banned { /* don't include the message, check if it already is included */
      if msg_included { /* delete this message from the cache */
        msg_idx := slices.Index(oc_msg_id_cache, rawmsg["id"].(string))
        oc_msg_id_cache = slices.Delete(oc_msg_id_cache, msg_idx, msg_idx+1)
      }
    } else if !msg_included { /* we have a fresh message here */
      var author map[string]interface{}
      /* check if the "user" field is present */
      if _, ok := rawmsg["user"]; ok {
        author = rawmsg["user"].(map[string]interface{})
      } else {
        author["displayName"] = "System"
      }

      /* convert the timestamp */
      sent_dt, _ := time.Parse("2006-01-02T15:04:05.000000000Z", rawmsg["timestamp"].(string))

      /* prepare output message */
      out_message := ChatMessage {
        Type: "oc",
        NativeId: rawmsg["id"].(string),
        AuthorId: author["id"].(string),
        Username: author["displayName"].(string),
        Timestamp: sent_dt.UTC().Format("2006-01-02T15:04:05Z"),
        Text: rawmsg["body"].(string),
        Icon: "",
      }

      /* remove starting and ending <p> tag from the message */
      out_message.Text = strings.ReplaceAll(out_message.Text, "\u003cp\u003e", "")
      out_message.Text = strings.ReplaceAll(out_message.Text, "\u003c/p\u003e", "")

      /* check for authenticated role */
      if _, ok := author["authenticated"]; ok && author["authenticated"].(bool) == true {
        out_message.Type += "^authenticated"
      }

      /* check for bot role */
      if _, ok := author["isBot"]; ok && author["isBot"].(bool) == true {
        out_message.Type += "^bot"
      }

      /* add display color hint */
      if _, ok := author["displayColor"]; ok {
        dcolor := int(author["displayColor"].(float64))
        if dcolor > 0 {
          out_message.Type += fmt.Sprintf("^occolor%d", dcolor)
        }
      }

      /* finally, send it to the message broker via the message_transport channel */
      message_transport <- out_message

      /* update the ID cache */
      oc_msg_id_cache = append(oc_msg_id_cache, rawmsg["id"].(string))
      cachelen := len(oc_msg_id_cache)
      if cachelen > OC_ID_CACHE_LIMIT { /* trim the excess */
        oc_msg_id_cache = oc_msg_id_cache[cachelen-OC_ID_CACHE_LIMIT:cachelen]
      }

    }
  }
}

/* entry point */
func owncast_run(config map[string]interface{}) {
  token_path := filepath.Join(APP_ROOT_DIR, "auth", "oc-token.txt")
  token, err := ioutil.ReadFile(token_path)
  token_err_msg := "[owncast] No Owncast token file provided or it is empty. "
  token_err_msg += "Please generate a token in your Owncast admin panel "
  token_err_msg += "and paste it into the " + token_path + " file!"
  if err != nil {
    log.Fatal(token_err_msg + "\nDetailed error: ", err)
  }
  OC_API_TOKEN = strings.TrimSpace(string(token))
  if len(OC_API_TOKEN) < 1 {
    log.Fatal(token_err_msg)
  }

  API_POLL_TIMEOUT := 1 /* polling timeout in seconds */

  if _, ok := config["debug"]; ok {
    OC_DEBUG = config["debug"].(bool)
  } 

  if _, ok := config["oc_server"]; ok {
    OC_API_ROOT = config["oc_server"].(string)
  } else {
    log.Fatal("[owncast] Owncast server missing in config!")
  }

  if _, ok := config["oc_api_poll_timeout"]; ok {
    API_POLL_TIMEOUT = int(config["oc_api_poll_timeout"].(float64))
  } 
  
  log.Println("StreamGoose Owncast Go backend started")

  var raw_chat_data []interface{}
  oc_msg_id_cache = append(oc_msg_id_cache, "") /* init the cache */
  for { /* main loop */
    raw_chat_data = oc_api_get("/api/integrations/chat")
    for _, rawmsg := range raw_chat_data { /* iterate over raw message objects */
      oc_process_message(rawmsg.(map[string]interface{}))
    }
    time.Sleep(time.Duration(API_POLL_TIMEOUT) * time.Second) /* wait for specified amount before polling next */
  }
}
