package chat

import (
	"context"
	"encoding/json"
	"errors"
	"log/slog"
	"strings"
	textTemplate "text/template"
	"time"

	"git.sr.ht/~ashkeel/strimertul/log"

	"github.com/nicklaw5/helix/v2"

	"git.sr.ht/~ashkeel/containers/sync"
	"git.sr.ht/~ashkeel/strimertul/database"
	"git.sr.ht/~ashkeel/strimertul/twitch"
	"git.sr.ht/~ashkeel/strimertul/twitch/eventsub"
	"git.sr.ht/~ashkeel/strimertul/twitch/template"
	"git.sr.ht/~ashkeel/strimertul/utils"
)

type Module struct {
	Config Config

	streamerAPI *helix.Client
	ctx         context.Context
	db          database.Database
	api         *helix.Client
	streamer    helix.User
	user        helix.User
	logger      *slog.Logger
	templater   template.Engine
	lastMessage *sync.RWSync[time.Time]

	commands        *sync.Map[string, Command]
	customCommands  *sync.Map[string, CustomCommand]
	customTemplates *sync.Map[string, *textTemplate.Template]
	customFunctions textTemplate.FuncMap
}

func Setup(ctx context.Context, db database.Database, api *helix.Client, user helix.User, logger *slog.Logger, templater template.Engine) *Module {
	mod := &Module{
		ctx:         ctx,
		db:          db,
		streamerAPI: api,
		api:         api,
		streamer:    user,
		user:        user,
		logger:      logger,
		templater:   templater,
		lastMessage: sync.NewRWSync(time.Now()),

		commands:        sync.NewMap[string, Command](),
		customCommands:  sync.NewMap[string, CustomCommand](),
		customTemplates: sync.NewMap[string, *textTemplate.Template](),
		customFunctions: make(textTemplate.FuncMap),
	}

	// Get config
	if err := db.GetJSON(ConfigKey, &mod.Config); err != nil {
		if errors.Is(err, database.ErrEmptyKey) {
			mod.Config = Config{
				CommandCooldown: 2,
			}
		} else {
			logger.Error("Failed to load chat module config", log.Error(err))
		}
	}

	// Set custom user (and set hook to reload when it changes)
	mod.setCustomUser()
	if err := db.SubscribeKeyContext(ctx, CustomAccountKey, func(value string) {
		mod.setCustomUser()
	}); err != nil {
		logger.Error("Could not subscribe to custom account changes", log.Error(err))
	}

	if err := db.SubscribeKeyContext(ctx, eventsub.EventKeyPrefix+helix.EventSubTypeChannelChatMessage, mod.onChatMessage); err != nil {
		logger.Error("Could not subscribe to chat messages", log.Error(err))
	}

	// Load custom commands
	var customCommands map[string]CustomCommand
	if err := db.GetJSON(CustomCommandsKey, &customCommands); err != nil {
		if errors.Is(err, database.ErrEmptyKey) {
			customCommands = make(map[string]CustomCommand)
		} else {
			logger.Error("Failed to load custom commands", log.Error(err))
		}
	}
	mod.customCommands.Set(customCommands)

	if err := mod.updateTemplates(); err != nil {
		logger.Error("Failed to parse custom commands", log.Error(err))
	}

	if err := db.SubscribeKeyContext(ctx, CustomCommandsKey, mod.updateCommands); err != nil {
		logger.Error("Could not set-up chat command reload subscription", log.Error(err))
	}

	if err := db.SubscribeKeyContext(ctx, WriteMessageRPC, mod.handleWriteMessageRPC); err != nil {
		logger.Error("Could not set-up chat command reload subscription", log.Error(err))
	}

	return mod
}

func (mod *Module) setCustomUser() {
	customUserClient, customUserInfo, err := GetCustomUser(mod.db)
	if err != nil {
		if !errors.Is(err, database.ErrEmptyKey) {
			mod.logger.Error("Failed to get custom user, falling back to streamer account", log.Error(err))
		}
		customUserClient = mod.streamerAPI
		customUserInfo = mod.streamer
	}
	mod.api = customUserClient
	mod.user = customUserInfo
}

func (mod *Module) onChatMessage(newValue string) {
	var chatMessage struct {
		Event helix.EventSubChannelChatMessageEvent `json:"event"`
	}
	if err := json.Unmarshal([]byte(newValue), &chatMessage); err != nil {
		mod.logger.Error("Failed to decode incoming chat message", log.Error(err))
		return
	}

	// TODO Command cooldown logic here!

	lowercaseMessage := strings.TrimSpace(strings.ToLower(chatMessage.Event.Message.Text))

	// Check if it's a command
	if strings.HasPrefix(lowercaseMessage, "!") {
		// Run through supported commands
		for cmd, data := range mod.commands.Copy() {
			if !data.Enabled {
				continue
			}
			if !strings.HasPrefix(lowercaseMessage, cmd) {
				continue
			}
			parts := strings.SplitN(lowercaseMessage, " ", 2)
			if parts[0] != cmd {
				continue
			}
			go data.Handler(chatMessage.Event)
			mod.lastMessage.Set(time.Now())
		}
	}

	// Run through custom commands
	for cmd, data := range mod.customCommands.Copy() {
		if !data.Enabled {
			continue
		}
		lc := strings.ToLower(cmd)
		if !strings.HasPrefix(lowercaseMessage, lc) {
			continue
		}
		parts := strings.SplitN(lowercaseMessage, " ", 2)
		if parts[0] != lc {
			continue
		}
		go cmdCustom(mod, cmd, data, chatMessage.Event)
		mod.lastMessage.Set(time.Now())
	}
}

func (mod *Module) handleWriteMessageRPC(value string) {
	var request WriteMessageRequest
	if err := json.Unmarshal([]byte(value), &request); err != nil {
		mod.logger.Warn("Failed to decode write message request", log.Error(err))
		return
	}

	if request.Announce {
		resp, err := mod.api.SendChatAnnouncement(&helix.SendChatAnnouncementParams{
			BroadcasterID: mod.streamer.ID,
			ModeratorID:   mod.user.ID,
			Message:       request.Message,
		})
		if err != nil {
			mod.logger.Error("Failed to send announcement", log.Error(err))
		}
		if resp.Error != "" {
			mod.logger.Error("Failed to send announcement", slog.String("code", resp.Error), slog.String("message", resp.ErrorMessage))
		}
		return
	}

	if request.WhisperTo != "" {
		resp, err := mod.api.SendUserWhisper(&helix.SendUserWhisperParams{
			FromUserID: mod.user.ID,
			ToUserID:   request.WhisperTo,
			Message:    request.Message,
		})
		if err != nil {
			mod.logger.Error("Failed to send whisper", log.Error(err))
		}
		if resp.Error != "" {
			mod.logger.Error("Failed to send whisper", slog.String("code", resp.Error), slog.String("message", resp.ErrorMessage))
		}
		return
	}

	resp, err := mod.api.SendChatMessage(&helix.SendChatMessageParams{
		BroadcasterID:        mod.streamer.ID,
		SenderID:             mod.user.ID,
		Message:              request.Message,
		ReplyParentMessageID: request.ReplyTo,
	})
	if err != nil {
		mod.logger.Error("Failed to send chat message", log.Error(err))
	}
	if resp.Error != "" {
		mod.logger.Error("Failed to send chat message", slog.String("code", resp.Error), slog.String("message", resp.ErrorMessage))
	}
}

func (mod *Module) updateCommands(value string) {
	err := utils.LoadJSONToWrapped[map[string]CustomCommand](value, mod.customCommands)
	if err != nil {
		mod.logger.Error("Failed to decode new custom commands", log.Error(err))
		return
	}
	// Recreate templates
	if err := mod.updateTemplates(); err != nil {
		mod.logger.Error("Failed to update custom commands templates", log.Error(err))
		return
	}
}

func (mod *Module) updateTemplates() error {
	mod.customTemplates.Set(make(map[string]*textTemplate.Template))
	for cmd, tmpl := range mod.customCommands.Copy() {
		tpl, err := mod.templater.MakeTemplate(tmpl.Response)
		if err != nil {
			return err
		}
		mod.customTemplates.SetKey(cmd, tpl)
	}
	return nil
}

func (mod *Module) WriteMessage(request WriteMessageRequest) {
	WriteMessage(mod.db, mod.logger, request)
}

func (mod *Module) RegisterCommand(name string, command Command) {
	mod.commands.SetKey(name, command)
}

func (mod *Module) UnregisterCommand(name string) {
	mod.commands.DeleteKey(name)
}

func (mod *Module) GetChatters() (users []string) {
	cursor := ""
	for {
		userClient, err := twitch.GetUserClient(mod.db, twitch.AuthKey, false)
		if err != nil {
			slog.Error("Could not get user api client for list of chatters", log.Error(err))
			return
		}
		res, err := userClient.GetChannelChatChatters(&helix.GetChatChattersParams{
			BroadcasterID: mod.user.ID,
			ModeratorID:   mod.user.ID,
			First:         "1000",
			After:         cursor,
		})
		if err != nil {
			mod.logger.Error("Could not retrieve list of chatters", log.Error(err))
			return
		}
		for _, user := range res.Data.Chatters {
			users = append(users, user.UserLogin)
		}
		cursor = res.Data.Pagination.Cursor
		if cursor == "" {
			return
		}
	}
}

func GetCustomUser(db database.Database) (*helix.Client, helix.User, error) {
	userClient, err := twitch.GetUserClient(db, CustomAccountKey, true)
	if err != nil {
		return nil, helix.User{}, err
	}
	users, err := userClient.GetUsers(&helix.UsersParams{})
	if err != nil {
		return nil, helix.User{}, err
	}
	if len(users.Data.Users) < 1 {
		return nil, helix.User{}, errors.New("no users found")
	}

	return userClient, users.Data.Users[0], nil
}
