package main

import (
	"encoding/json"
	"net/http"
	"regexp"
	"strings"
	"time"

	log "github.com/sirupsen/logrus"

	"github.com/Luzifer/go_helpers/v2/str"
	"github.com/Luzifer/twitch-bot/v3/pkg/twitch"
	"github.com/Luzifer/twitch-bot/v3/plugins"
)

const frontendNotifyTypeReload = "configReload"

type (
	configEditorLoginResponse struct {
		ExpiresAt time.Time `json:"expiresAt"`
		Token     string    `json:"token"`
		User      string    `json:"user"`
	}
)

var frontendNotifyHooks = newHooker()

//nolint:funlen // Just contains a collection of objects
func registerEditorGlobalMethods() {
	for _, rd := range []plugins.HTTPRouteRegistrationArgs{
		{
			Description:  "Returns the documentation for available actions",
			HandlerFunc:  configEditorGlobalGetActions,
			Method:       http.MethodGet,
			Module:       moduleConfigEditor,
			Name:         "Get available actions",
			Path:         "/actions",
			ResponseType: plugins.HTTPRouteResponseTypeJSON,
		},
		{
			Description:  "Exchanges the Twitch token against an internal Bearer token",
			HandlerFunc:  configEditorGlobalLogin,
			Method:       http.MethodPost,
			Module:       moduleConfigEditor,
			Name:         "Authorize on Config-Editor",
			Path:         "/login",
			ResponseType: plugins.HTTPRouteResponseTypeJSON,
		},
		{
			Description:  "Returns all available modules for auth",
			HandlerFunc:  configEditorGlobalGetModules,
			Method:       http.MethodGet,
			Module:       moduleConfigEditor,
			Name:         "Get available modules",
			Path:         "/modules",
			ResponseType: plugins.HTTPRouteResponseTypeJSON,
		},
		{
			Description: "Returns information about a Twitch user to properly display bot editors",
			HandlerFunc: configEditorGlobalGetUser,
			Method:      http.MethodGet,
			Module:      moduleConfigEditor,
			Name:        "Get user information",
			Path:        "/user",
			QueryParams: []plugins.HTTPRouteParamDocumentation{
				{
					Description: "The user to query the information for",
					Name:        "user",
					Required:    true,
					Type:        "string",
				},
			},
			RequiresEditorsAuth: true,
			ResponseType:        plugins.HTTPRouteResponseTypeJSON,
		},
		{
			Description:  "Subscribe for configuration changes",
			HandlerFunc:  configEditorGlobalSubscribe,
			Method:       http.MethodGet,
			Module:       moduleConfigEditor,
			Name:         "Websocket: Subscribe config changes",
			Path:         "/notify-config",
			ResponseType: plugins.HTTPRouteResponseTypeTextPlain,
		},
		{
			Description:         "Takes the authorization token present in the request and returns a new one for the same user",
			HandlerFunc:         configEditorGlobalRefreshToken,
			Method:              http.MethodGet,
			Module:              moduleConfigEditor,
			Name:                "Refresh Auth-Token",
			Path:                "/refreshToken",
			RequiresEditorsAuth: true,
			ResponseType:        plugins.HTTPRouteResponseTypeJSON,
		},
		{
			Description: "Validate a cron expression and return the next executions",
			HandlerFunc: configEditorGlobalValidateCron,
			Method:      http.MethodPut,
			Module:      moduleConfigEditor,
			Name:        "Validate cron expression",
			Path:        "/validate-cron",
			QueryParams: []plugins.HTTPRouteParamDocumentation{
				{
					Description: "The cron expression to test",
					Name:        "cron",
					Required:    true,
					Type:        "string",
				},
				{
					Description: "Check cron with last execution of auto-message",
					Name:        "uuid",
					Required:    false,
					Type:        "string",
				},
			},
			ResponseType: plugins.HTTPRouteResponseTypeJSON,
		},
		{
			Description: "Validate a regular expression against the RE2 regex parser",
			HandlerFunc: configEditorGlobalValidateRegex,
			Method:      http.MethodPut,
			Module:      moduleConfigEditor,
			Name:        "Validate regular expression",
			Path:        "/validate-regex",
			QueryParams: []plugins.HTTPRouteParamDocumentation{
				{
					Description: "The regular expression to test",
					Name:        "regexp",
					Required:    true,
					Type:        "string",
				},
			},
			ResponseType: plugins.HTTPRouteResponseTypeTextPlain,
		},
		{
			Description: "Validate a template expression against the built in template function library",
			HandlerFunc: configEditorGlobalValidateTemplate,
			Method:      http.MethodPut,
			Module:      moduleConfigEditor,
			Name:        "Validate template expression",
			Path:        "/validate-template",
			QueryParams: []plugins.HTTPRouteParamDocumentation{
				{
					Description: "The template expression to test",
					Name:        "template",
					Required:    true,
					Type:        "string",
				},
			},
			ResponseType: plugins.HTTPRouteResponseTypeTextPlain,
		},
	} {
		if err := registerRoute(rd); err != nil {
			log.WithError(err).Fatal("Unable to register config editor route")
		}
	}
}

func configEditorGlobalGetActions(w http.ResponseWriter, _ *http.Request) {
	availableActorDocsLock.Lock()
	defer availableActorDocsLock.Unlock()

	if err := json.NewEncoder(w).Encode(availableActorDocs); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
}

func configEditorGlobalGetModules(w http.ResponseWriter, _ *http.Request) {
	if err := json.NewEncoder(w).Encode(knownModules); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
}

func configEditorGlobalGetUser(w http.ResponseWriter, r *http.Request) {
	usr, err := twitchClient.GetUserInformation(r.Context(), r.FormValue("user"))
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	if err := json.NewEncoder(w).Encode(usr); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
}

func configEditorGlobalLogin(w http.ResponseWriter, r *http.Request) {
	var payload struct {
		Token string `json:"token"`
	}

	if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	tc := twitch.New(cfg.TwitchClient, cfg.TwitchClientSecret, payload.Token, "")
	id, user, err := tc.GetAuthorizedUser(r.Context())
	if err != nil {
		http.Error(w, "access denied", http.StatusUnauthorized)
		return
	}

	if !str.StringInSlice(user, config.BotEditors) && !str.StringInSlice(id, config.BotEditors) {
		// That user is none of our editors: Deny access
		http.Error(w, "access denied", http.StatusForbidden)
		return
	}

	// Bot-Editors do have unlimited access to all modules: Pass in module `*`
	tok, expiresAt, err := editorTokenService.CreateUserToken(id, user, []string{"*"})
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	if err := json.NewEncoder(w).Encode(configEditorLoginResponse{
		ExpiresAt: expiresAt,
		Token:     tok,
		User:      user,
	}); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
}

func configEditorGlobalRefreshToken(w http.ResponseWriter, r *http.Request) {
	tokenType, token, found := strings.Cut(r.Header.Get("Authorization"), " ")
	if !found || !strings.EqualFold(tokenType, "bearer") {
		http.Error(w, "invalid renew request", http.StatusBadRequest)
	}

	id, user, _, modules, err := editorTokenService.ValidateLoginToken(token)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}

	tok, expiresAt, err := editorTokenService.CreateUserToken(id, user, modules)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}

	if err := json.NewEncoder(w).Encode(configEditorLoginResponse{
		ExpiresAt: expiresAt,
		Token:     tok,
		User:      user,
	}); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
}

func configEditorGlobalSubscribe(w http.ResponseWriter, r *http.Request) {
	conn, err := upgrader.Upgrade(w, r, nil)
	if err != nil {
		log.WithError(err).Error("Unable to initialize websocket")
		return
	}
	defer conn.Close() //nolint:errcheck

	var (
		frontendNotify = make(chan string, 1)
		pingTimer      = time.NewTicker(websocketPingInterval)
		unsubscribe    = frontendNotifyHooks.Register(func(payload any) { frontendNotify <- payload.(string) })
	)
	defer unsubscribe()

	type socketMsg struct {
		MsgType string `json:"msg_type"`
	}

	for {
		select {
		case msgType := <-frontendNotify:
			if err := conn.WriteJSON(socketMsg{
				MsgType: msgType,
			}); err != nil {
				log.WithError(err).Debug("Unable to send websocket notification")
				return
			}

		case <-pingTimer.C:
			if err := conn.WriteJSON(socketMsg{
				MsgType: "ping",
			}); err != nil {
				log.WithError(err).Debug("Unable to send websocket ping")
				return
			}
		}
	}
}

func configEditorGlobalValidateCron(w http.ResponseWriter, r *http.Request) {
	sched, err := cronParser.Parse(r.FormValue("cron"))
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	var (
		lt  = time.Now()
		out []time.Time
	)

	if id := r.FormValue("uuid"); id != "" {
		for _, a := range config.AutoMessages {
			if a.ID() != id {
				continue
			}
			lt = a.lastMessageSent
			break
		}
	}

	for i := 0; i < 3; i++ {
		lt = sched.Next(lt)
		out = append(out, lt)
	}

	if err := json.NewEncoder(w).Encode(out); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
}

func configEditorGlobalValidateRegex(w http.ResponseWriter, r *http.Request) {
	if _, err := regexp.Compile(r.FormValue("regexp")); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	w.WriteHeader(http.StatusNoContent)
}

func configEditorGlobalValidateTemplate(w http.ResponseWriter, r *http.Request) {
	if err := validateTemplate(r.FormValue("template")); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	w.WriteHeader(http.StatusNoContent)
}