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

package main

import (
  "fmt"
  "log"
  "slices"
  "strings"
  "regexp"
  "time"
  "encoding/json"
  "net/http"
  "io/ioutil"
  "github.com/DaRealFreak/cloudflare-bp-go"
)

/* global API root URL */
const KK_API_ROOT = "https://kick.com/api"

/* global debug flag */
var KK_DEBUG bool

/* global useragent config */
var KK_USERAGENT string

/* recent time cache */
var KK_RECENT_TIME string

const KK_ID_CACHE_LIMIT = 1000

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

/* API request helper function */
func kk_api_get(endpoint string) map[string]interface{} {
  url := KK_API_ROOT + endpoint /* get full URL */
  req, err := http.NewRequest("GET", url, nil)
  req.Header.Set("User-Agent", KK_USERAGENT)
  req.Header.Set("Alt-Used", "kick.com")
  req.Header.Set("Accept", "application/json; charset=utf-8")
  req.Header.Set("Accept-Language", "en-US")
  req.Header.Set("Connection", "keep-alive")

  client := &http.Client{}
  client.Transport = cloudflarebp.AddCloudFlareByPass(client.Transport)
  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 KK_DEBUG {
    log.Printf("[kick] API response: %s", string(body))
  }

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

  return respbody
}

/* emote handling function */
/* the format is [emote:ID:NAME] */
/* then the client will substitute this id with https://files.kick.com/emotes/ID/fullsize image */
func kk_handle_emotes(text string) string {
  mbracket := regexp.QuoteMeta("]")
  re := regexp.MustCompile(regexp.QuoteMeta("[emote:") + "[^:]+:[^" + mbracket + "]+" + mbracket)
  text = re.ReplaceAllStringFunc(text, func(emotestr string) string {
    parts := strings.Split(emotestr, ":")
		return " #KKEMOTE-" + parts[1] + "# "
	})
  return text  
}

/* main message procesing function */
func kk_process_message(rawmsg map[string]interface{}) {
  /* if the message came from a real user, this is the one we should process */
  if rawmsg["type"] == "message" {
    if !slices.Contains(kk_msg_id_cache, rawmsg["id"].(string)) { /* we have a fresh message here */
      var author map[string]interface{}
      /* check if the "sender" field is present */
      if _, ok := rawmsg["sender"]; ok {
        author = rawmsg["sender"].(map[string]interface{})
      } else {
        author["username"] = "System"
      }

      KK_RECENT_TIME = rawmsg["created_at"].(string)

      /* convert the timestamp */
      sent_dt, _ := time.Parse("2006-01-02T15:04:05Z", KK_RECENT_TIME)

      /* update the recent time variable */
      KK_RECENT_TIME = fmt.Sprintf("%d", sent_dt.Unix())

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

      /* get user metadata */
      metadata := author["identity"].(map[string]interface{})

      /* process username color preference */
      if _, ok := metadata["color"]; ok && len(metadata["color"].(string)) > 0 {
        out_message.Type += "^color" + metadata["color"].(string)
      }

      /* process username badges */
      if _, ok := metadata["badges"]; ok && len(metadata["badges"].([]interface{})) > 0 {
        for _, badge := range metadata["badges"].([]interface{}) {
          if badge.(map[string]interface{})["active"].(bool) {
            out_message.Type += "^" + strings.ToLower(badge.(map[string]interface{})["text"].(string))
          }
        }
      }

      /* process emotes */
      out_message.Text = kk_handle_emotes(out_message.Text)

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

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

    }
  }
}

/* entry point */
func kick_run(config map[string]interface{}) {
  if _, ok := config["debug"]; ok {
    KK_DEBUG = config["debug"].(bool)
  } 

  if _, ok := config["useragent"]; ok {
    KK_USERAGENT = config["useragent"].(string)
  } 
  
  API_POLL_TIMEOUT := 2

  KK_RECENT_TIME = "" /* init the recent time */

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

  /* get chat ID by channel name */
  channel_id_endpoint := fmt.Sprintf("/v2/channels/%s", config["kk_channel"].(string))
  if KK_DEBUG {
    log.Printf("[kick] querying %s", channel_id_endpoint)
  }
  channel_id_data := kk_api_get(channel_id_endpoint)

  channel_id := 0
  if _, ok := channel_id_data["id"]; ok && channel_id_data["id"].(float64) > 0 {
    channel_id = int(channel_id_data["id"].(float64))
  }

  if channel_id < 1 { /* the channel ID is still empty */
    log.Fatal("[kick] Unknown error while getting channel ID!")
  }

  log.Println("StreamGoose Kick Go backend started")

  var raw_chat_data map[string]interface{}
  kk_msg_id_cache = append(kk_msg_id_cache, "") /* init the cache */
  for { /* main loop */
    chat_api_endpoint := fmt.Sprintf("/v2/channels/%d/messages?start_time=%s", channel_id, KK_RECENT_TIME)
    raw_chat_data = kk_api_get(chat_api_endpoint)
    if raw_chat_data["status"].(map[string]interface{})["error"].(bool) == false {
      msg_arr := raw_chat_data["data"].(map[string]interface{})["messages"].([]interface{})
      for _, rawmsg := range msg_arr { /* iterate over raw message objects */
        kk_process_message(rawmsg.(map[string]interface{}))
      }
    }
    time.Sleep(time.Duration(API_POLL_TIMEOUT) * time.Second) /* wait for specified amount before polling next */
  }
}
