package parser

import (
	"encoding/json"
	"fmt"
	"log"
	"strconv"
	"strings"
	"time"

	eventbus "code.justin.tv/eventbus/client"
	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/service/sqs"
)

type ParseResult struct {
	SQS *sqs.Message

	// Message Attributes hoisted out as possible
	SQSAttrs map[string]interface{}

	ReceiveCount int
	FirstReceive time.Time

	HasSNSMessage bool // if true, this has an embedded SNS message
	IsEventBus    bool // If true, this contains an event bus message

	SNS *SNSMessage

	EventBusType string
	EventBus     *eventbus.RawMessage

	// the most specific payload possible.
	Payload interface{}
}

func (pr *ParseResult) String() string {
	var b strings.Builder
	b.WriteString("SQS:\n")

	b.WriteString("\tMessageID: ")
	b.WriteString(aws.StringValue(pr.SQS.MessageId))
	b.WriteString("\n")

	b.WriteString("\tFirstReceiveTime: ")
	b.WriteString(pr.FirstReceive.Format(time.RFC3339))
	b.WriteString("\n")

	b.WriteString("\tReceiveCount: ")
	b.WriteString(strconv.Itoa(pr.ReceiveCount))
	b.WriteString("\n")

	b.WriteString("\tAttributes: ")
	if len(pr.SQSAttrs) != 0 {
		for key, value := range pr.SQSAttrs {
			b.WriteString("\n\t\t")
			b.WriteString(key)
			b.WriteString(": ")
			b.WriteString(fmt.Sprintf("%v", value))
		}
	} else {
		b.WriteString("(None)")
	}
	b.WriteString("\n")

	b.WriteString("SNS:\n")
	b.WriteString("\tMessageID: ")
	b.WriteString(pr.SNS.MessageID)
	b.WriteString("\n")

	b.WriteString("\tAttributes: ")
	if len(pr.SNS.MessageAttributes) != 0 {
		for key, value := range pr.SNS.MessageAttributes {
			b.WriteString("\n\t\t")
			b.WriteString(key)
			b.WriteString(": ")
			blob, err := json.Marshal(value)
			if err != nil {
				log.Fatal("Error marshaling SNS message attribute")
			}
			b.WriteString(string(blob))
		}
	} else {
		b.WriteString("(None)")
	}
	b.WriteString("\n")

	b.WriteString("\tEventBus:\n")
	b.WriteString("\t\tType: ")
	b.WriteString(pr.EventBusType)
	b.WriteString("\n")

	b.WriteString("\t\tHeader:\n")

	b.WriteString("\t\t\tMessageID: ")
	b.WriteString(pr.EventBus.Header.MessageID.String())
	b.WriteString("\n")

	b.WriteString("\t\t\tCreatedAt: ")
	b.WriteString(pr.EventBus.Header.CreatedAt.Format(time.RFC3339))
	b.WriteString("\n")

	b.WriteString("\t\t\tEnvironment: ")
	b.WriteString(pr.EventBus.Header.Environment)
	b.WriteString("\n")

	b.WriteString("\t\tPayload:\n\t\t\t")
	blob, err := json.MarshalIndent(pr.Payload, "\t\t\t", "\t")
	if err != nil {
		log.Fatal("Error marshaling EventBus message payload")
	}
	b.Write(blob)
	b.WriteString("\n")

	return b.String()
}

func Parse(message *sqs.Message) (*ParseResult, error) {
	result := &ParseResult{
		SQS: message,
	}
	var err error

	// Receive count
	if rc := message.Attributes["ApproximateReceiveCount"]; rc != nil {
		result.ReceiveCount, err = strconv.Atoi(*rc)
		if err != nil {
			return nil, err
		}
	}

	if epochRaw := message.Attributes["ApproximateFirstReceiveTimestamp"]; epochRaw != nil {
		epoch, err := strconv.ParseInt(aws.StringValue(epochRaw), 10, 64)
		if err != nil {
			return nil, err
		} else if epoch > 0 {
			result.FirstReceive = time.Unix(epoch/1000, int64(epoch%1000)*int64(time.Millisecond/time.Nanosecond))
		}
	}

	if len(message.MessageAttributes) > 0 {
		result.SQSAttrs, err = parseSQSAttrs(message.MessageAttributes)
	}

	body := aws.StringValue(message.Body)
	if strings.HasPrefix(body, "{") {
		// check for embedded SNS message
		var snsMessage SNSMessage
		buf := []byte(body)
		err := json.Unmarshal(buf, &snsMessage)
		if err != nil {
			var dest map[string]interface{}
			if err = json.Unmarshal(buf, &dest); err == nil {
				result.Payload = dest
			}
		} else if snsMessage.TopicArn != "" || !snsMessage.Timestamp.IsZero() {
			result.HasSNSMessage = true
			result.SNS = &snsMessage
			result.Payload = snsMessage.Message
		}
	}
	GuessPayload(result)
	return result, nil
}

func parseSQSAttrs(input map[string]*sqs.MessageAttributeValue) (map[string]interface{}, error) {
	attrs := make(map[string]interface{})
	for k, rawVal := range input {
		var v interface{}
		switch aws.StringValue(rawVal.DataType) {
		case "String":
			v = aws.StringValue(rawVal.StringValue)
		case "Binary":
			v = rawVal.BinaryValue
		case "Number":
			s := aws.StringValue(rawVal.StringValue)
			if strings.Contains(s, ".") {
				f, err := strconv.ParseFloat(s, 64)
				if err != nil {
					return nil, err
				}
				v = f
			} else {
				n, err := strconv.Atoi(s)
				if err != nil {
					return nil, err
				}
				v = n
			}
		}
		attrs[k] = v
	}
	return attrs, nil
}
