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

package main

import (
  "log"
  "slices"
  "time"
  "encoding/json"
  "net/http"
  "io/ioutil"
)

/* global API root URL placeholder */
var YT_API_ROOT string

const YT_ID_CACHE_LIMIT = 1000

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

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

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

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

  return respbody
}

/* main message procesing function */
func yt_process_message(rawmsg map[string]interface{}) {
  /* if the message came from a real user, this is the one we should process */
  if rawmsg["kind"] == "youtube#liveChatMessage" && rawmsg["snippet"].(map[string]interface{})["type"] == "textMessageEvent" {
    if !slices.Contains(yt_msg_id_cache, rawmsg["id"].(string)) { /* we have a fresh message here */
      snip := rawmsg["snippet"].(map[string]interface{})
      author := rawmsg["authorDetails"].(map[string]interface{})

      /* convert the timestamp */
      sent_dt, _ := time.Parse("2006-01-02T15:04:05.000000-07:00", snip["publishedAt"].(string))

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

      /* check for profile image */
      if _, ok := author["profileImageUrl"]; ok && len(author["profileImageUrl"].(string)) > 0 {
        out_message.Icon = author["profileImageUrl"].(string)
      }

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

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

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

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

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

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

    }
  }
}

/* entry point */
func youtube_run(config map[string]interface{}) {
  API_POLL_TIMEOUT := 1 /* polling timeout in seconds */

  if _, ok := config["yt_api_root_url"]; ok {
    YT_API_ROOT = config["yt_api_root_url"].(string)
  } else {
    log.Fatal("[youtube] API Root URL missing in config!")
  }

  if _, ok := config["yt_api_poll_timeout"]; ok {
    API_POLL_TIMEOUT = int(config["yt_api_poll_timeout"].(float64))
  } 

  video_id := "" /* video broadcast ID is empty by default */
  if _, ok := config["yt_video_id"]; ok { /* override the video broadcast ID */
    video_id = config["yt_video_id"].(string)
  } else { /* no video ID supplied, begin with the usual flow */
    /* first, get channel ID by YouTube handle */
    channel_id_data := yt_api_get("/channels?part=id&forHandle=" + config["yt_channel"].(string))
    channel_id := ""
    if _, ok := channel_id_data["items"]; ok && len(channel_id_data["items"].([]interface{})) > 0 {
      channel_id = channel_id_data["items"].([]interface{})[0].(map[string]interface{})["id"].(string)
    } else {
      log.Fatal("[youtube] Error while getting channel ID!")
    }

    /* then, get live broadcast video ID by channel ID */
    video_id_data := yt_api_get("/search?part=snippet&eventType=live&type=video&channelId=" + channel_id)
    if _, ok := video_id_data["items"]; ok && len(video_id_data["items"].([]interface{})) > 0 {
      id_desc := video_id_data["items"].([]interface{})[0].(map[string]interface{})["id"]
      video_id = id_desc.(map[string]interface{})["videoId"].(string)
    } else {
      log.Fatal("[youtube] Error while getting video ID!")
    }
  }

  /* now, get active live chat ID by broadcast video ID */
  chat_id := ""
  chat_id_data := yt_api_get("/videos?part=liveStreamingDetails&id=" + video_id)
  if _, ok := chat_id_data["items"]; ok && len(chat_id_data["items"].([]interface{})) > 0 {
    details := chat_id_data["items"].([]interface{})[0].(map[string]interface{})["liveStreamingDetails"]
    chat_id = details.(map[string]interface{})["activeLiveChatId"].(string)
  } else {
    log.Fatal("[youtube] Error while getting live chat ID!")
  }

  if len(chat_id) < 1 { /* the chat ID is still empty */
    log.Fatal("[youtube] Unknown error while getting live chat ID!")
  }
  
  log.Println("StreamGoose YouTube Go backend started")

  var raw_chat_data map[string]interface{}
  yt_msg_id_cache = append(yt_msg_id_cache, "") /* init the cache */
  for { /* main loop */
    raw_chat_data = yt_api_get("/liveChat/messages?part=snippet,authorDetails&liveChatId=" + chat_id)
    if _, ok := raw_chat_data["items"]; ok && len(raw_chat_data["items"].([]interface{})) > 0 {
      for _, rawmsg := range raw_chat_data["items"].([]interface{}) { /* iterate over raw message objects */
         yt_process_message(rawmsg.(map[string]interface{}))
      }
    }
    time.Sleep(time.Duration(API_POLL_TIMEOUT) * time.Second) /* wait for specified amount before polling next */
  }
}
