// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.

package slashcommands

import (
	"encoding/base64"
	"errors"
	"fmt"
	"strings"

	"github.com/mattermost/mattermost-server/v6/app"
	"github.com/mattermost/mattermost-server/v6/app/request"
	"github.com/mattermost/mattermost-server/v6/model"
	"github.com/mattermost/mattermost-server/v6/shared/i18n"
)

const (
	AvailableRemoteActions = "create, accept, remove, status"
)

type RemoteProvider struct {
}

const (
	CommandTriggerRemote = "secure-connection"
)

func init() {
	app.RegisterCommandProvider(&RemoteProvider{})
}

func (rp *RemoteProvider) GetTrigger() string {
	return CommandTriggerRemote
}

func (rp *RemoteProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model.Command {

	remote := model.NewAutocompleteData(rp.GetTrigger(), "[action]", T("api.command_remote.remote_add_remove.help", map[string]any{"Actions": AvailableRemoteActions}))

	create := model.NewAutocompleteData("create", "", T("api.command_remote.invite.help"))
	create.AddNamedTextArgument("name", T("api.command_remote.name.help"), T("api.command_remote.name.hint"), "", true)
	create.AddNamedTextArgument("displayname", T("api.command_remote.displayname.help"), T("api.command_remote.displayname.hint"), "", false)
	create.AddNamedTextArgument("password", T("api.command_remote.invite_password.help"), T("api.command_remote.invite_password.hint"), "", true)

	accept := model.NewAutocompleteData("accept", "", T("api.command_remote.accept.help"))
	accept.AddNamedTextArgument("name", T("api.command_remote.name.help"), T("api.command_remote.name.hint"), "", true)
	accept.AddNamedTextArgument("displayname", T("api.command_remote.displayname.help"), T("api.command_remote.displayname.hint"), "", false)
	accept.AddNamedTextArgument("password", T("api.command_remote.invite_password.help"), T("api.command_remote.invite_password.hint"), "", true)
	accept.AddNamedTextArgument("invite", T("api.command_remote.invitation.help"), T("api.command_remote.invitation.hint"), "", true)

	remove := model.NewAutocompleteData("remove", "", T("api.command_remote.remove.help"))
	remove.AddNamedDynamicListArgument("connectionID", T("api.command_remote.remove_remote_id.help"), "builtin:"+CommandTriggerRemote, true)

	status := model.NewAutocompleteData("status", "", T("api.command_remote.status.help"))

	remote.AddCommand(create)
	remote.AddCommand(accept)
	remote.AddCommand(remove)
	remote.AddCommand(status)

	return &model.Command{
		Trigger:          rp.GetTrigger(),
		AutoComplete:     true,
		AutoCompleteDesc: T("api.command_remote.desc"),
		AutoCompleteHint: T("api.command_remote.hint"),
		DisplayName:      T("api.command_remote.name"),
		AutocompleteData: remote,
	}
}

func (rp *RemoteProvider) DoCommand(a *app.App, c request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
	if !a.HasPermissionTo(args.UserId, model.PermissionManageSecureConnections) {
		return responsef(args.T("api.command_remote.permission_required", map[string]any{"Permission": "manage_secure_connections"}))
	}

	margs := parseNamedArgs(args.Command)
	action, ok := margs[ActionKey]
	if !ok {
		return responsef(args.T("api.command_remote.missing_command", map[string]any{"Actions": AvailableRemoteActions}))
	}

	switch action {
	case "create":
		return rp.doCreate(a, args, margs)
	case "accept":
		return rp.doAccept(a, args, margs)
	case "remove":
		return rp.doRemove(a, args, margs)
	case "status":
		return rp.doStatus(a, args, margs)
	}

	return responsef(args.T("api.command_remote.unknown_action", map[string]any{"Action": action}))
}

func (rp *RemoteProvider) GetAutoCompleteListItems(a *app.App, commandArgs *model.CommandArgs, arg *model.AutocompleteArg, parsed, toBeParsed string) ([]model.AutocompleteListItem, error) {
	if !a.HasPermissionTo(commandArgs.UserId, model.PermissionManageSecureConnections) {
		return nil, errors.New("You require `manage_secure_connections` permission to manage secure connections.")
	}

	if arg.Name == "connectionID" && strings.Contains(parsed, " remove ") {
		return getRemoteClusterAutocompleteListItems(a, true)
	}

	return nil, fmt.Errorf("`%s` is not a dynamic argument", arg.Name)
}

// doCreate creates and displays an encrypted invite that can be used by a remote site to establish a simple trust.
func (rp *RemoteProvider) doCreate(a *app.App, args *model.CommandArgs, margs map[string]string) *model.CommandResponse {
	password := margs["password"]
	if password == "" {
		return responsef(args.T("api.command_remote.missing_empty", map[string]any{"Arg": "password"}))
	}

	name := margs["name"]
	if name == "" {
		return responsef(args.T("api.command_remote.missing_empty", map[string]any{"Arg": "name"}))
	}

	displayname := margs["displayname"]
	if displayname == "" {
		displayname = name
	}

	url := a.GetSiteURL()
	if url == "" {
		return responsef(args.T("api.command_remote.site_url_not_set"))
	}

	rc := &model.RemoteCluster{
		Name:        name,
		DisplayName: displayname,
		Token:       model.NewId(),
		CreatorId:   args.UserId,
	}

	rcSaved, appErr := a.AddRemoteCluster(rc)
	if appErr != nil {
		return responsef(args.T("api.command_remote.add_remote.error", map[string]any{"Error": appErr.Error()}))
	}

	// Display the encrypted invitation
	invite := &model.RemoteClusterInvite{
		RemoteId:     rcSaved.RemoteId,
		RemoteTeamId: args.TeamId,
		SiteURL:      url,
		Token:        rcSaved.Token,
	}
	encrypted, err := invite.Encrypt(password)
	if err != nil {
		return responsef(args.T("api.command_remote.encrypt_invitation.error", map[string]any{"Error": err.Error()}))
	}
	encoded := base64.URLEncoding.EncodeToString(encrypted)

	return responsef("##### " + args.T("api.command_remote.invitation_created") + "\n" +
		args.T("api.command_remote.invite_summary", map[string]any{"Command": "/secure-connection accept", "Invitation": encoded, "SiteURL": invite.SiteURL}))
}

// doAccept accepts an invitation generated by a remote site.
func (rp *RemoteProvider) doAccept(a *app.App, args *model.CommandArgs, margs map[string]string) *model.CommandResponse {
	password := margs["password"]
	if password == "" {
		return responsef(args.T("api.command_remote.missing_empty", map[string]any{"Arg": "password"}))
	}

	name := margs["name"]
	if name == "" {
		return responsef(args.T("api.command_remote.missing_empty", map[string]any{"Arg": "name"}))
	}

	displayname := margs["displayname"]
	if displayname == "" {
		displayname = name
	}

	blob := margs["invite"]
	if blob == "" {
		return responsef(args.T("api.command_remote.missing_empty", map[string]any{"Arg": "invite"}))
	}

	// invite is encoded as base64 and encrypted
	decoded, err := base64.URLEncoding.DecodeString(blob)
	if err != nil {
		return responsef(args.T("api.command_remote.decode_invitation.error", map[string]any{"Error": err.Error()}))
	}
	invite := &model.RemoteClusterInvite{}
	err = invite.Decrypt(decoded, password)
	if err != nil {
		return responsef(args.T("api.command_remote.incorrect_password.error", map[string]any{"Error": err.Error()}))
	}

	rcs, _ := a.GetRemoteClusterService()
	if rcs == nil {
		return responsef(args.T("api.command_remote.service_not_enabled"))
	}

	url := a.GetSiteURL()
	if url == "" {
		return responsef(args.T("api.command_remote.site_url_not_set"))
	}

	rc, err := rcs.AcceptInvitation(invite, name, displayname, args.UserId, args.TeamId, url)
	if err != nil {
		return responsef(args.T("api.command_remote.accept_invitation.error", map[string]any{"Error": err.Error()}))
	}

	return responsef("##### " + args.T("api.command_remote.accept_invitation", map[string]any{"SiteURL": rc.SiteURL}))
}

// doRemove removes a remote cluster from the database, effectively revoking the trust relationship.
func (rp *RemoteProvider) doRemove(a *app.App, args *model.CommandArgs, margs map[string]string) *model.CommandResponse {
	id, ok := margs["connectionID"]
	if !ok {
		return responsef(args.T("api.command_remote.missing_empty", map[string]any{"Arg": "remoteId"}))
	}

	deleted, err := a.DeleteRemoteCluster(id)
	if err != nil {
		responsef(args.T("api.command_remote.remove_remote.error", map[string]any{"Error": err.Error()}))
	}

	result := "removed"
	if !deleted {
		result = "**NOT FOUND**"
	}
	return responsef("##### " + args.T("api.command_remote.cluster_removed", map[string]any{"RemoteId": id, "Result": result}))
}

// doStatus displays connection status for all remote clusters.
func (rp *RemoteProvider) doStatus(a *app.App, args *model.CommandArgs, _ map[string]string) *model.CommandResponse {
	list, err := a.GetAllRemoteClusters(model.RemoteClusterQueryFilter{})
	if err != nil {
		responsef(args.T("api.command_remote.fetch_status.error", map[string]any{"Error": err.Error()}))
	}

	if len(list) == 0 {
		return responsef("** " + args.T("api.command_remote.remotes_not_found") + " **")
	}

	var sb strings.Builder
	fmt.Fprintf(&sb, args.T("api.command_remote.remote_table_header")+" \n")
	// | Secure Connection | Display name | ConnectionID | Site URL | Invite accepted | Online | Last ping  |
	fmt.Fprintf(&sb, "| :---- | :---- | :---- | :---- | :---- | :---- | :---- | \n")

	for _, rc := range list {
		accepted := formatBool(args.T, rc.SiteURL != "")
		online := formatBool(args.T, isOnline(rc.LastPingAt))
		lastPing := formatTimestamp(rc.LastPingAt)

		fmt.Fprintf(&sb, "| %s | %s | %s | %s | %s | %s | %s |\n", rc.Name, rc.DisplayName, rc.RemoteId, rc.SiteURL, accepted, online, lastPing)
	}
	return responsef(sb.String())
}

func isOnline(lastPing int64) bool {
	return lastPing > model.GetMillis()-model.RemoteOfflineAfterMillis
}

func getRemoteClusterAutocompleteListItems(a *app.App, includeOffline bool) ([]model.AutocompleteListItem, error) {
	filter := model.RemoteClusterQueryFilter{
		ExcludeOffline: !includeOffline,
	}
	clusters, err := a.GetAllRemoteClusters(filter)
	if err != nil || len(clusters) == 0 {
		return []model.AutocompleteListItem{}, nil
	}

	list := make([]model.AutocompleteListItem, 0, len(clusters))

	for _, rc := range clusters {
		item := model.AutocompleteListItem{
			Item:     rc.RemoteId,
			HelpText: fmt.Sprintf("%s  (%s)", rc.DisplayName, rc.SiteURL)}
		list = append(list, item)
	}
	return list, nil
}

func getRemoteClusterAutocompleteListItemsNotInChannel(a *app.App, channelId string, includeOffline bool) ([]model.AutocompleteListItem, error) {
	filter := model.RemoteClusterQueryFilter{
		ExcludeOffline: !includeOffline,
		NotInChannel:   channelId,
	}
	all, err := a.GetAllRemoteClusters(filter)
	if err != nil || len(all) == 0 {
		return []model.AutocompleteListItem{}, nil
	}

	list := make([]model.AutocompleteListItem, 0, len(all))

	for _, rc := range all {
		item := model.AutocompleteListItem{
			Item:     rc.RemoteId,
			HelpText: fmt.Sprintf("%s  (%s)", rc.DisplayName, rc.SiteURL)}
		list = append(list, item)
	}
	return list, nil
}
