package notification

import (
	"context"
	"fmt"
	"sync"
	"time"

	slackapi "github.com/nlopes/slack"
	"github.com/pkg/errors"

	"code.justin.tv/eventbus/controlplane/infrastructure/notification/state"
	"code.justin.tv/eventbus/controlplane/infrastructure/validation"
	"code.justin.tv/eventbus/controlplane/internal/clients/slack"
)

const (
	colorRed    = "#ff0000"
	colorYellow = "#ffff00"
	colorGreen  = "#00ff00"
)

type slackActions struct {
	client           slack.Slack
	errorAttachments []slackapi.Attachment
	warnAttachments  []slackapi.Attachment
	okAttachments    []slackapi.Attachment
}

func (s *slackActions) Open(report *validation.Report) error {
	s.attach(report)
	return nil
}
func (s *slackActions) ChangeStatus(oldReport *validation.Report, newReport *validation.Report) error {
	s.attach(newReport)
	return nil
}
func (s *slackActions) Renotify(report *validation.Report) error {
	s.attach(report)
	return nil
}
func (s *slackActions) Resolve(problemReport *validation.Report, resolutionReport *validation.Report) error {
	s.attach(resolutionReport)
	return nil
}

func (s *slackActions) sendAll(ctx context.Context) error {
	attachments := append(s.okAttachments, s.warnAttachments...)
	attachments = append(attachments, s.errorAttachments...)

	if len(attachments) == 0 {
		return nil
	}

	ts := time.Now()
	msgText := fmt.Sprintf("New validation report: %s", ts.Format("Jan 2 15:04:05"))
	// Slack has a limit of 20 attachments per message.
	for i := 0; i < len(attachments); i += 20 {
		if i > 0 {
			msgText = "" // purge non-first message text
		}
		end := i + 20
		if len(attachments) < end {
			end = len(attachments)
		}
		err := s.client.SendMessage(ctx, msgText, attachments[i:end]...)
		if err != nil {
			return errors.Wrap(err, "unable to send slack message")
		}
	}

	return nil
}

func (s *slackActions) attach(report *validation.Report) {
	switch report.Status {
	case validation.StatusOk:
		s.okAttachments = append(s.okAttachments, attachment(report))
	case validation.StatusWarn:
		s.warnAttachments = append(s.warnAttachments, attachment(report))
	default:
		s.errorAttachments = append(s.errorAttachments, attachment(report))
	}
}

func (s *slackActions) reset() {
	s.okAttachments = nil
	s.warnAttachments = nil
	s.errorAttachments = nil
}

type Slack struct {
	stateTracker *state.Tracker
	actions      *slackActions
	reports      []*validation.Report
	mutex        sync.Mutex
}

func NewSlack(client slack.Slack, deduplicationInterval time.Duration) *Slack {
	actions := &slackActions{
		client: client,
	}
	return &Slack{
		reports:      make([]*validation.Report, 0),
		stateTracker: state.NewTracker(actions, deduplicationInterval, 2),
		actions:      actions,
	}
}

func (s *Slack) Add(report *validation.Report) {
	s.mutex.Lock()
	s.reports = append(s.reports, report)
	s.mutex.Unlock()
}

func (s *Slack) Submit(ctx context.Context) error {
	s.mutex.Lock()
	defer s.mutex.Unlock()

	for _, report := range s.reports {
		err := s.stateTracker.Handle(report)
		if err != nil {
			return errors.Wrap(err, "could not process reports for slack")
		}
	}

	err := s.actions.sendAll(ctx)
	if err != nil {
		return errors.Wrap(err, "could not send slack messages")
	}

	s.reset()
	return nil
}

func (s *Slack) reset() {
	s.actions.reset()
	s.reports = nil
}

func attachment(report *validation.Report) slackapi.Attachment {
	attachment := slackapi.Attachment{
		Color: color(report),
		Blocks: []slackapi.Block{
			severitySection(report.Status),
			itemDetailsSection(report.Item),
		},
	}

	if report.Status != validation.StatusOk {
		attachment.Blocks = append(attachment.Blocks, messageSection(report.Message))
	}

	return attachment
}

func severitySection(status validation.Status) *slackapi.SectionBlock {
	return slackapi.NewSectionBlock(
		slackapi.NewTextBlockObject("mrkdwn", fmt.Sprintf("*%s*", status), false, false),
		nil,
		nil,
	)
}

func itemDetailsSection(item validation.Item) *slackapi.SectionBlock {
	fields := make([]*slackapi.TextBlockObject, len(item.Attributes()))
	for i, attr := range item.Attributes() {
		text := fmt.Sprintf("*%s*: %s", attr.Key, attr.Value)
		fields[i] = slackapi.NewTextBlockObject("mrkdwn", text, false, false)
	}

	return slackapi.NewSectionBlock(
		slackapi.NewTextBlockObject("mrkdwn", item.Type(), false, false),
		fields,
		nil,
	)
}

func messageSection(msg string) *slackapi.SectionBlock {
	return slackapi.NewSectionBlock(
		slackapi.NewTextBlockObject("mrkdwn", fmt.Sprintf("```%s```", msg), false, false),
		nil,
		nil,
	)
}

func color(report *validation.Report) string {
	switch report.Status {
	case validation.StatusOk:
		return colorGreen
	case validation.StatusWarn:
		return colorYellow
	case validation.StatusError, validation.StatusValidationError:
		return colorRed
	default:
		return colorRed
	}
}
