package slack

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"strconv"
	"strings"
	"time"

	"github.com/nlopes/slack"

	"github.com/keel-hq/keel/bot"
	"github.com/keel-hq/keel/constants"
	"github.com/keel-hq/keel/version"

	log "github.com/sirupsen/logrus"
)

// SlackImplementer - implementes slack HTTP functionality, used to
// send messages with attachments
type SlackImplementer interface {
	PostMessage(channelID string, options ...slack.MsgOption) (string, string, error)
}

// Bot - main slack bot container
type Bot struct {
	id   string // bot id
	name string // bot name

	users map[string]string

	msgPrefix string

	slackClient *slack.Client
	slackRTM    *slack.RTM

	slackHTTPClient SlackImplementer

	approvalsChannel string // slack approvals channel name

	ctx                context.Context
	botMessagesChannel chan *bot.BotMessage
	approvalsRespCh    chan *bot.ApprovalResponse
}

func init() {
	bot.RegisterBot("slack", &Bot{})
}

func (b *Bot) Configure(approvalsRespCh chan *bot.ApprovalResponse, botMessagesChannel chan *bot.BotMessage) bool {
	if os.Getenv(constants.EnvSlackToken) != "" {

		b.name = "keel"
		if bootName := os.Getenv(constants.EnvSlackBotName); bootName != "" {
			b.name = bootName
		}

		token := os.Getenv(constants.EnvSlackToken)
		client := slack.New(token)

		b.approvalsChannel = "general"
		if channel := os.Getenv(constants.EnvSlackApprovalsChannel); channel != "" {
			b.approvalsChannel = strings.TrimPrefix(channel, "#")
		}

		b.slackClient = client
		b.slackHTTPClient = client
		b.approvalsRespCh = approvalsRespCh
		b.botMessagesChannel = botMessagesChannel

		return true
	}
	log.Info("bot.slack.Configure(): Slack approval bot is not configured")
	return false
}

// Start - start bot
func (b *Bot) Start(ctx context.Context) error {
	// setting root context
	b.ctx = ctx

	users, err := b.slackClient.GetUsers()
	if err != nil {
		return err
	}

	b.users = map[string]string{}

	for _, user := range users {
		switch user.Name {
		case b.name:
			if user.IsBot {
				b.id = user.ID
			}
		default:
			continue
		}
	}
	if b.id == "" {
		return errors.New("could not find bot in the list of names, check if the bot is called \"" + b.name + "\" ")
	}

	b.msgPrefix = strings.ToLower("<@" + b.id + ">")

	go b.startInternal()

	return nil
}

func (b *Bot) startInternal() error {
	b.slackRTM = b.slackClient.NewRTM()

	go b.slackRTM.ManageConnection()
	for {
		select {
		case <-b.ctx.Done():
			return nil

		case msg := <-b.slackRTM.IncomingEvents:
			switch ev := msg.Data.(type) {
			case *slack.HelloEvent:
				// Ignore hello
			case *slack.ConnectedEvent:
				// nothing to do
			case *slack.MessageEvent:
				b.handleMessage(ev)
			case *slack.PresenceChangeEvent:
				// nothing to do
			case *slack.RTMError:
				log.Errorf("Error: %s", ev.Error())
			case *slack.InvalidAuthEvent:
				log.Error("Invalid credentials")
				return fmt.Errorf("invalid credentials")

			default:

				// Ignore other events..
				// fmt.Printf("Unexpected: %v\n", msg.Data)
			}
		}
	}
}

func (b *Bot) postMessage(title, message, color string, fields []slack.AttachmentField) error {
	params := slack.NewPostMessageParameters()
	params.Username = b.name
	params.IconURL = b.getBotUserIconURL()

	attachements := []slack.Attachment{
		slack.Attachment{
			Fallback: message,
			Color:    color,
			Fields:   fields,
			Footer:   fmt.Sprintf("https://keel.sh %s", version.GetKeelVersion().Version),
			Ts:       json.Number(strconv.Itoa(int(time.Now().Unix()))),
		},
	}

	var mgsOpts []slack.MsgOption

	mgsOpts = append(mgsOpts, slack.MsgOptionPostMessageParameters(params))
	mgsOpts = append(mgsOpts, slack.MsgOptionAttachments(attachements...))

	_, _, err := b.slackHTTPClient.PostMessage(b.approvalsChannel, mgsOpts...)
	if err != nil {
		log.WithFields(log.Fields{
			"error":             err,
			"approvals_channel": b.approvalsChannel,
		}).Error("bot.postMessage: failed to send message")
	}
	return err
}

// checking if message was received in approvals channel
func (b *Bot) isApprovalsChannel(event *slack.MessageEvent) bool {

	channel, err := b.slackClient.GetChannelInfo(event.Channel)
	if err != nil {
		// looking for private channel
		conv, err := b.slackRTM.GetConversationInfo(event.Channel, true)
		if err != nil {
			log.Errorf("couldn't find amongst private conversations: %s", err)
		} else if conv.Name == b.approvalsChannel {
			return true
		}

		log.WithError(err).Errorf("channel with ID %s could not be retrieved", event.Channel)
		return false
	}

	log.Debugf("checking if approvals channel: %s==%s", channel.Name, b.approvalsChannel)
	if channel.Name == b.approvalsChannel {
		return true
	}

	log.Debugf("message was received not on approvals channel (%s)", channel.Name)

	return false
}

func (b *Bot) handleMessage(event *slack.MessageEvent) {
	if event.BotID != "" || event.User == "" || event.SubType == "bot_message" {
		log.WithFields(log.Fields{
			"event_bot_ID":  event.BotID,
			"event_user":    event.User,
			"msg":           event.Text,
			"event_subtype": event.SubType,
		}).Debug("handleMessage: ignoring message")
		return
	}

	eventText := strings.Trim(strings.ToLower(event.Text), " \n\r")

	if !b.isBotMessage(event, eventText) {
		return
	}

	eventText = b.trimBot(eventText)

	approval, ok := bot.IsApproval(event.User, eventText)
	// only accepting approvals from approvals channel
	if ok && b.isApprovalsChannel(event) {
		b.approvalsRespCh <- approval
		return
	} else if ok {
		log.WithFields(log.Fields{
			"received_on":    event.Channel,
			"approvals_chan": b.approvalsChannel,
		}).Warnf("message was received not in approvals channel: %s", event.Channel)
		b.Respond(fmt.Sprintf("please use approvals channel '%s'", b.approvalsChannel), event.Channel)
		return
	}

	b.botMessagesChannel <- &bot.BotMessage{
		Message: eventText,
		User:    event.User,
		Channel: event.Channel,
		Name:    "slack",
	}
}

func (b *Bot) Respond(text string, channel string) {

	// if message is short, replying directly via slack RTM
	if len(text) < 3000 {
		b.slackRTM.SendMessage(b.slackRTM.NewOutgoingMessage(formatAsSnippet(text), channel))
		return
	}

	// longer messages are getting uploaded as files

	f := slack.FileUploadParameters{
		Filename: "keel response",
		Content:  text,
		Filetype: "text",
		Channels: []string{channel},
	}

	_, err := b.slackClient.UploadFile(f)
	if err != nil {
		log.WithFields(log.Fields{
			"error": err,
		}).Error("Respond: failed to send message")
	}
}

func (b *Bot) isBotMessage(event *slack.MessageEvent, eventText string) bool {
	prefixes := []string{
		b.msgPrefix,
		b.name,
		// "kel",
	}

	for _, p := range prefixes {
		if strings.HasPrefix(eventText, p) {
			return true
		}
	}

	// Direct message channels always starts with 'D'
	return strings.HasPrefix(event.Channel, "D")
}

func (b *Bot) trimBot(msg string) string {
	msg = strings.Replace(msg, strings.ToLower(b.msgPrefix), "", 1)
	msg = strings.TrimPrefix(msg, b.name)
	msg = strings.Trim(msg, " :\n")

	return msg
}

func formatAsSnippet(response string) string {
	return "```" + response + "```"
}

func (b *Bot) getBotUserIconURL() string {
	res, err := b.slackClient.GetUserInfo(b.id)
	if err != nil {
		log.WithFields(log.Fields{
			"error":  err,
			"bot_id": b.id,
		}).Error("bot.postMessage: failed to retrieve bot user icon url")
	}

	return res.Profile.ImageOriginal
}
