/*
*
* Gosora Alerts System
* Copyright Azareal 2017 - 2020
*
 */
package common

import (
	"database/sql"
	"errors"
	"strconv"
	"strings"
	"time"

	//"fmt"

	"github.com/Azareal/Gosora/common/phrases"
	qgen "github.com/Azareal/Gosora/query_gen"
)

type Alert struct {
	ASID         int
	ActorID      int
	TargetUserID int
	Event        string
	ElementType  string
	ElementID    int
	CreatedAt    time.Time
	Extra        string

	Actor *User
}

type AlertStmts struct {
	notifyWatchers *sql.Stmt
	getWatchers    *sql.Stmt
}

var alertStmts AlertStmts

// TODO: Move these statements into some sort of activity abstraction
// TODO: Rewrite the alerts logic
func init() {
	DbInits.Add(func(acc *qgen.Accumulator) error {
		alertStmts = AlertStmts{
			notifyWatchers: acc.SimpleInsertInnerJoin(
				qgen.DBInsert{"activity_stream_matches", "watcher,asid", ""},
				qgen.DBJoin{"activity_stream", "activity_subscriptions", "activity_subscriptions.user, activity_stream.asid", "activity_subscriptions.targetType = activity_stream.elementType AND activity_subscriptions.targetID = activity_stream.elementID AND activity_subscriptions.user != activity_stream.actor", "asid=?", "", ""},
			),
			getWatchers: acc.SimpleInnerJoin("activity_stream", "activity_subscriptions", "activity_subscriptions.user", "activity_subscriptions.targetType = activity_stream.elementType AND activity_subscriptions.targetID = activity_stream.elementID AND activity_subscriptions.user != activity_stream.actor", "asid=?", "", ""),
		}
		return acc.FirstError()
	})
}

const AlertsGrowHint = len(`{"msgs":[],"count":,"tc":}`) + 1 + 10

// TODO: See if we can json.Marshal instead?
func escapeTextInJson(in string) string {
	in = strings.Replace(in, "\"", "\\\"", -1)
	return strings.Replace(in, "/", "\\/", -1)
}

func BuildAlert(a Alert, user User /* The current user */) (out string, err error) {
	var targetUser *User
	if a.Actor == nil {
		a.Actor, err = Users.Get(a.ActorID)
		if err != nil {
			return "", errors.New(phrases.GetErrorPhrase("alerts_no_actor"))
		}
	}

	/*if a.ElementType != "forum" {
		targetUser, err = users.Get(a.TargetUserID)
		if err != nil {
			LocalErrorJS("Unable to find the target user",w,r)
			return
		}
	}*/
	if a.Event == "friend_invite" {
		return buildAlertString(".new_friend_invite", []string{a.Actor.Name}, a.Actor.Link, a.Actor.Avatar, a.ASID), nil
	}

	// Not that many events for us to handle in a forum
	if a.ElementType == "forum" {
		if a.Event == "reply" {
			topic, err := Topics.Get(a.ElementID)
			if err != nil {
				DebugLogf("Unable to find linked topic %d", a.ElementID)
				return "", errors.New(phrases.GetErrorPhrase("alerts_no_linked_topic"))
			}
			// Store the forum ID in the targetUser column instead of making a new one? o.O
			// Add an additional column for extra information later on when we add the ability to link directly to posts. We don't need the forum data for now...
			return buildAlertString(".forum_new_topic", []string{a.Actor.Name, topic.Title}, topic.Link, a.Actor.Avatar, a.ASID), nil
		}
		return buildAlertString(".forum_unknown_action", []string{a.Actor.Name}, "", a.Actor.Avatar, a.ASID), nil
	}

	var url, area, phraseName string
	own := false
	// TODO: Avoid loading a bit of data twice
	switch a.ElementType {
	case "convo":
		convo, err := Convos.Get(a.ElementID)
		if err != nil {
			DebugLogf("Unable to find linked convo %d", a.ElementID)
			return "", errors.New(phrases.GetErrorPhrase("alerts_no_linked_convo"))
		}
		url = convo.Link
	case "topic":
		topic, err := Topics.Get(a.ElementID)
		if err != nil {
			DebugLogf("Unable to find linked topic %d", a.ElementID)
			return "", errors.New(phrases.GetErrorPhrase("alerts_no_linked_topic"))
		}
		url = topic.Link
		area = topic.Title
		own = a.TargetUserID == user.ID
	case "user":
		targetUser, err = Users.Get(a.ElementID)
		if err != nil {
			DebugLogf("Unable to find target user %d", a.ElementID)
			return "", errors.New(phrases.GetErrorPhrase("alerts_no_target_user"))
		}
		area = targetUser.Name
		url = targetUser.Link
		own = a.TargetUserID == user.ID
	case "post":
		topic, err := TopicByReplyID(a.ElementID)
		if err != nil {
			DebugLogf("Unable to find linked topic by reply ID %d", a.ElementID)
			return "", errors.New(phrases.GetErrorPhrase("alerts_no_linked_topic_by_reply"))
		}
		url = topic.Link
		area = topic.Title
		own = a.TargetUserID == user.ID
	default:
		return "", errors.New(phrases.GetErrorPhrase("alerts_invalid_elementtype"))
	}

	badEv := false
	switch a.Event {
	case "create", "like", "mention", "reply":
		// skip
	default:
		badEv = true
	}

	if own && !badEv {
		phraseName = "." + a.ElementType + "_own_" + a.Event
	} else if !badEv {
		phraseName = "." + a.ElementType + "_" + a.Event
	} else if own {
		phraseName = "." + a.ElementType + "_own"
	} else {
		phraseName = "." + a.ElementType
	}

	return buildAlertString(phraseName, []string{a.Actor.Name, area}, url, a.Actor.Avatar, a.ASID), nil
}

func buildAlertString(msg string, sub []string, path, avatar string, asid int) string {
	var sb strings.Builder
	buildAlertSb(&sb, msg, sub, path, avatar, asid)
	return sb.String()
}

const AlertsGrowHint2 = len(`{"msg":"","sub":[],"path":"","img":"","id":}`) + 5 + 3 + 1 + 1 + 1

// TODO: Use a string builder?
func buildAlertSb(sb *strings.Builder, msg string, sub []string, path, avatar string, asid int) {
	sb.WriteString(`{"msg":"`)
	sb.WriteString(escapeTextInJson(msg))
	sb.WriteString(`","sub":[`)
	for i, it := range sub {
		if i != 0 {
			sb.WriteString(",\"")
		} else {
			sb.WriteString("\"")
		}
		sb.WriteString(escapeTextInJson(it))
		sb.WriteString("\"")
	}
	sb.WriteString(`],"path":"`)
	sb.WriteString(escapeTextInJson(path))
	sb.WriteString(`","img":"`)
	sb.WriteString(escapeTextInJson(avatar))
	sb.WriteString(`","id":`)
	sb.WriteString(strconv.Itoa(asid))
	sb.WriteRune('}')
}

func BuildAlertSb(sb *strings.Builder, a *Alert, u *User /* The current user */) (err error) {
	var targetUser *User
	if a.Actor == nil {
		a.Actor, err = Users.Get(a.ActorID)
		if err != nil {
			return errors.New(phrases.GetErrorPhrase("alerts_no_actor"))
		}
	}

	/*if a.ElementType != "forum" {
		targetUser, err = users.Get(a.TargetUserID)
		if err != nil {
			LocalErrorJS("Unable to find the target user",w,r)
			return
		}
	}*/
	if a.Event == "friend_invite" {
		buildAlertSb(sb, ".new_friend_invite", []string{a.Actor.Name}, a.Actor.Link, a.Actor.Avatar, a.ASID)
		return nil
	}

	// Not that many events for us to handle in a forum
	if a.ElementType == "forum" {
		if a.Event == "reply" {
			topic, err := Topics.Get(a.ElementID)
			if err != nil {
				DebugLogf("Unable to find linked topic %d", a.ElementID)
				return errors.New(phrases.GetErrorPhrase("alerts_no_linked_topic"))
			}
			// Store the forum ID in the targetUser column instead of making a new one? o.O
			// Add an additional column for extra information later on when we add the ability to link directly to posts. We don't need the forum data for now...
			buildAlertSb(sb, ".forum_new_topic", []string{a.Actor.Name, topic.Title}, topic.Link, a.Actor.Avatar, a.ASID)
			return nil
		}
		buildAlertSb(sb, ".forum_unknown_action", []string{a.Actor.Name}, "", a.Actor.Avatar, a.ASID)
		return nil
	}

	var url, area string
	own := false
	// TODO: Avoid loading a bit of data twice
	switch a.ElementType {
	case "convo":
		convo, err := Convos.Get(a.ElementID)
		if err != nil {
			DebugLogf("Unable to find linked convo %d", a.ElementID)
			return errors.New(phrases.GetErrorPhrase("alerts_no_linked_convo"))
		}
		url = convo.Link
	case "topic":
		topic, err := Topics.Get(a.ElementID)
		if err != nil {
			DebugLogf("Unable to find linked topic %d", a.ElementID)
			return errors.New(phrases.GetErrorPhrase("alerts_no_linked_topic"))
		}
		url = topic.Link
		area = topic.Title
		own = a.TargetUserID == u.ID
	case "user":
		targetUser, err = Users.Get(a.ElementID)
		if err != nil {
			DebugLogf("Unable to find target user %d", a.ElementID)
			return errors.New(phrases.GetErrorPhrase("alerts_no_target_user"))
		}
		area = targetUser.Name
		url = targetUser.Link
		own = a.TargetUserID == u.ID
	case "post":
		t, err := TopicByReplyID(a.ElementID)
		if err != nil {
			DebugLogf("Unable to find linked topic by reply ID %d", a.ElementID)
			return errors.New(phrases.GetErrorPhrase("alerts_no_linked_topic_by_reply"))
		}
		url = t.Link
		area = t.Title
		own = a.TargetUserID == u.ID
	default:
		return errors.New(phrases.GetErrorPhrase("alerts_invalid_elementtype"))
	}

	sb.WriteString(`{"msg":".`)
	sb.WriteString(a.ElementType)
	if own {
		sb.WriteString("_own_")
	} else {
		sb.WriteRune('_')
	}
	switch a.Event {
	case "create", "like", "mention", "reply":
		sb.WriteString(a.Event)
	}

	sb.WriteString(`","sub":["`)
	sb.WriteString(escapeTextInJson(a.Actor.Name))
	sb.WriteString("\",\"")
	sb.WriteString(escapeTextInJson(area))
	sb.WriteString(`"],"path":"`)
	sb.WriteString(escapeTextInJson(url))
	sb.WriteString(`","img":"`)
	sb.WriteString(escapeTextInJson(a.Actor.Avatar))
	sb.WriteString(`","id":`)
	sb.WriteString(strconv.Itoa(a.ASID))
	sb.WriteRune('}')

	return nil
}

//const AlertsGrowHint3 = len(`{"msg":"._","sub":["",""],"path":"","img":"","id":}`) + 3 + 2 + 2 + 2 + 2 + 1

// TODO: Create a notifier structure?
func AddActivityAndNotifyAll(a Alert) error {
	id, err := Activity.Add(a)
	if err != nil {
		return err
	}
	return NotifyWatchers(id)
}

// TODO: Create a notifier structure?
func AddActivityAndNotifyTarget(a Alert) error {
	id, err := Activity.Add(a)
	if err != nil {
		return err
	}

	err = ActivityMatches.Add(a.TargetUserID, id)
	if err != nil {
		return err
	}
	a.ASID = id

	// Live alerts, if the target is online and WebSockets is enabled
	if EnableWebsockets {
		go func() {
			defer EatPanics()
			_ = WsHub.pushAlert(a.TargetUserID, a)
			//fmt.Println("err:",err)
		}()
	}
	return nil
}

// TODO: Create a notifier structure?
func NotifyWatchers(asid int) error {
	_, err := alertStmts.notifyWatchers.Exec(asid)
	if err != nil {
		return err
	}

	// Alert the subscribers about this without blocking us from doing something else
	if EnableWebsockets {
		go func() {
			defer EatPanics()
			notifyWatchers(asid)
		}()
	}
	return nil
}

func notifyWatchers(asid int) {
	rows, e := alertStmts.getWatchers.Query(asid)
	if e != nil && e != ErrNoRows {
		LogError(e)
		return
	}
	defer rows.Close()

	var uid int
	var uids []int
	for rows.Next() {
		if e := rows.Scan(&uid); e != nil {
			LogError(e)
			return
		}
		uids = append(uids, uid)
	}
	if e = rows.Err(); e != nil {
		LogError(e)
		return
	}

	alert, e := Activity.Get(asid)
	if e != nil && e != ErrNoRows {
		LogError(e)
		return
	}
	_ = WsHub.pushAlerts(uids, alert)
}

func DismissAlert(uid, aid int) {
	_ = WsHub.PushMessage(uid, `{"event":"dismiss-alert","id":`+strconv.Itoa(aid)+`}`)
}