package chat

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"sort"
	"strings"
	"time"

	"code.nonshy.com/nonshy/website/pkg/config"
	"code.nonshy.com/nonshy/website/pkg/geoip"
	"code.nonshy.com/nonshy/website/pkg/log"
	"code.nonshy.com/nonshy/website/pkg/middleware"
	"code.nonshy.com/nonshy/website/pkg/models"
	"code.nonshy.com/nonshy/website/pkg/photo"
	"code.nonshy.com/nonshy/website/pkg/session"
	"code.nonshy.com/nonshy/website/pkg/templates"
	"code.nonshy.com/nonshy/website/pkg/worker"
	"github.com/golang-jwt/jwt/v4"
)

// JWT claims.
type Claims struct {
	// Custom claims.
	IsAdmin    bool   `json:"op,omitempty"`
	VIP        bool   `json:"vip,omitempty"`
	Avatar     string `json:"img,omitempty"`
	ProfileURL string `json:"url,omitempty"`
	Nickname   string `json:"nick,omitempty"`
	Emoji      string `json:"emoji,omitempty"`
	Gender     string `json:"gender,omitempty"`

	// Standard claims. Notes:
	// subject = username
	jwt.RegisteredClaims
}

// Gender returns the BareRTC gender string for the user's gender selection.
func Gender(u *models.User) string {
	switch u.GetProfileField("gender") {
	case "Man", "Trans (FTM)":
		return "m"
	case "Woman", "Trans (MTF)":
		return "f"
	case "Non-binary", "Trans", "Other":
		return "o"
	default:
		return ""
	}
}

// Landing page for chat rooms.
func Landing() http.HandlerFunc {
	tmpl := templates.Must("chat.html")
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// Get the current user.
		currentUser, err := session.CurrentUser(r)
		if err != nil {
			session.FlashError(w, r, "Couldn't get current user: %s", err)
			templates.Redirect(w, "/")
			return
		}

		// Are they logging into the chat room?
		var (
			intent   = r.FormValue("intent")
			isShy    = currentUser.IsShy()
			birthday = r.FormValue("birthday") == "true" && currentUser.IsBirthday()
		)
		if intent == "join" {
			// Maintenance mode?
			if middleware.ChatMaintenance(currentUser, w, r) {
				return
			}

			// If we are shy, block chat for now.
			if isShy {
				session.FlashError(w, r,
					"You have a Shy Account and are not allowed in the chat room at this time where our non-shy members may "+
						"be on camera.",
				)
				templates.Redirect(w, "/chat")
				return
			}

			// Get our Chat JWT secret.
			var (
				secret  = []byte(config.Current.BareRTC.JWTSecret)
				chatURL = config.Current.BareRTC.URL
			)
			if len(secret) == 0 || chatURL == "" {
				session.FlashError(w, r, "Couldn't sign you into the chat: JWT secret key or chat URL not configured!")
				templates.Redirect(w, r.URL.Path)
				return
			}

			// Avatar URL - masked if non-public.
			avatar := photo.URLPath(currentUser.ProfilePhoto.CroppedFilename)
			switch currentUser.ProfilePhoto.Visibility {
			case models.PhotoPrivate:
				avatar = "/static/img/shy-private.png"
			case models.PhotoFriends:
				avatar = "/static/img/shy-friends.png"
			}

			// Country flag emoji.
			emoji, err := geoip.GetChatFlagEmoji(r)
			if err != nil {
				emoji, err = geoip.CountryFlagEmojiWithCode("US")
				if err != nil {
					emoji = "🏴‍☠️"
				}
			}

			// Birthday cake emoji?
			if birthday {
				emoji = "🍰 It's my birthday!"
			}

			// Create the JWT claims.
			claims := Claims{
				IsAdmin:    currentUser.HasAdminScope(config.ScopeChatModerator),
				VIP:        currentUser.IsInnerCircle(),
				Avatar:     avatar,
				ProfileURL: "/u/" + currentUser.Username,
				Nickname:   currentUser.NameOrUsername(),
				Emoji:      emoji,
				Gender:     Gender(currentUser),
				RegisteredClaims: jwt.RegisteredClaims{
					ExpiresAt: jwt.NewNumericDate(time.Now().Add(5 * time.Minute)),
					IssuedAt:  jwt.NewNumericDate(time.Now()),
					NotBefore: jwt.NewNumericDate(time.Now()),
					Issuer:    config.Title,
					Subject:   currentUser.Username,
					ID:        fmt.Sprintf("%d", currentUser.ID),
				},
			}
			token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
			ss, err := token.SignedString(secret)
			if err != nil {
				session.FlashError(w, r, "Couldn't sign you into the chat: %s", err)
				templates.Redirect(w, r.URL.Path)
				return
			}

			// Send over their blocklist to the chat server.
			if err := SendBlocklist(currentUser); err != nil {
				log.Error("SendBlocklist: %s", err)
			}

			// Redirect them to the chat room.
			templates.Redirect(w, strings.TrimSuffix(chatURL, "/")+"/?jwt="+ss)
			return
		}

		// Get the ChatStatistics and select our friend names from it.
		var (
			stats         = FilteredChatStatistics(currentUser)
			friendsOnline = models.FilterFriendUsernames(currentUser, stats.Usernames)
		)

		sort.Strings(friendsOnline)

		var vars = map[string]interface{}{
			"ChatAPI":   strings.TrimSuffix(config.Current.BareRTC.URL, "/") + "/api/statistics",
			"IsShyUser": isShy,

			// Pre-populate the "who's online" widget from backend cache data
			"ChatStatistics": stats,
			"FriendsOnline":  friendsOnline,
		}
		if err := tmpl.Execute(w, r, vars); err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
	})
}

// FilteredChatStatistics will return a copy of the cached ChatStatistics but where the Usernames list is
// filtered down (and the online user counts, accordingly) by blocklists.
func FilteredChatStatistics(currentUser *models.User) worker.ChatStatistics {
	var stats = worker.GetChatStatistics()
	var result = worker.ChatStatistics{
		UserCount: stats.UserCount,
		Usernames: []string{},
		Cameras:   stats.Cameras,
	}

	// Who are we blocking?
	var blockedUsernames = map[string]interface{}{}
	for _, username := range models.BlockedUsernames(currentUser) {
		blockedUsernames[username] = nil
	}

	// Filter the online users listing.
	for _, username := range stats.Usernames {
		if _, ok := blockedUsernames[username]; !ok {
			result.Usernames = append(result.Usernames, username)
		}
	}

	// Sort the names.
	sort.Strings(result.Usernames)

	return result
}

// SendBlocklist syncs the user blocklist to the chat server prior to sending them over.
func SendBlocklist(user *models.User) error {
	// Get the user's blocklist.
	blockedUsernames := models.BlockedUsernames(user)
	log.Info("SendBlocklist(%s) to BareRTC: %d blocked usernames", user.Username, len(blockedUsernames))

	// API request struct for BareRTC /api/blocklist endpoint.
	var request = struct {
		APIKey    string
		Username  string
		Blocklist []string
	}{
		config.Current.CronAPIKey,
		user.Username,
		blockedUsernames,
	}

	// JSON request body.
	jsonStr, err := json.Marshal(request)
	if err != nil {
		return err
	}

	// Make the API request to BareRTC.
	var url = strings.TrimSuffix(config.Current.BareRTC.URL, "/") + "/api/blocklist"
	req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonStr))
	if err != nil {
		return err
	}
	req.Header.Set("Content-Type", "application/json")

	client := &http.Client{
		Timeout: 10 * time.Second,
	}
	resp, err := client.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(resp.Body)
		log.Error("SendBlocklist: error syncing blocklist to BareRTC: status %d body %s", resp.StatusCode, body)
	}

	return nil
}

// BlockUserNow syncs the new block action to the chat server now, in case the user is already online.
func BlockUserNow(currentUser, user *models.User) error {
	// API request struct for BareRTC /api/block/now endpoint.
	var request = struct {
		APIKey    string
		Usernames []string
	}{
		config.Current.CronAPIKey,
		[]string{
			currentUser.Username,
			user.Username,
		},
	}

	// JSON request body.
	jsonStr, err := json.Marshal(request)
	if err != nil {
		return err
	}

	// Make the API request to BareRTC.
	var url = strings.TrimSuffix(config.Current.BareRTC.URL, "/") + "/api/block/now"
	req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonStr))
	if err != nil {
		return err
	}
	req.Header.Set("Content-Type", "application/json")

	client := &http.Client{
		Timeout: 10 * time.Second,
	}
	resp, err := client.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(resp.Body)
		log.Error("BlockUserNow: error syncing block to BareRTC: status %d body %s", resp.StatusCode, body)
	}

	return nil
}
