package parser

import (
	"errors"
	"strconv"
	"strings"
)

func parseJsonPayload(payload jsonObject) (*messageProperties, error) {
	msg := parseJsonMessage(payload)
	if msg == "" {
		return nil, errors.New("msg was empty")
	}

	requestID := parseRequestID(payload)
	stackTrace := parseJsonStackTrace(payload)
	operation := payload.getString("twirp.method")

	// StreamSchedule and Nexus calculate fingerprints client side.
	fingerprint := payload.getString("fingerprint")
	if fingerprint == "" {
		fingerprint = calcJsonFingerprint(msg, operation, stackTrace)
	}

	return &messageProperties{
		message:     msg,
		requestID:   requestID,
		operation:   operation,
		fingerprint: fingerprint,
		stackTrace:  stackTrace,
		level:       "error",
	}, nil
}

func parseJsonMessage(payload jsonObject) string {
	tokens := make([]string, 0, 2)

	// Logs that come from the TwitchLogging Brazil package's Logger interface always have a "msg" key.
	// StreamSchedule and Nexus store errors in the msg.
	msg := payload.getString("msg")
	if msg != "" {
		tokens = append(tokens, msg)
	}

	// Shelfie stores error messages under an "error" key.
	error := payload.getString("error")
	if error != "" {
		tokens = append(tokens, error)
	}

	return strings.Join(tokens, ": ")
}

func parseRequestID(payload jsonObject) string {
	// StreamSchedule and Nexus store the request ID under a "rid" key.
	requestID := payload.getString("rid")
	if requestID != "" {
		return requestID
	}

	// Shelfie stores the request ID under a "request_id" key.
	return payload.getString("request_id")
}

func calcJsonFingerprint(message string, operation string, stackTrace []StackTrace) string {
	builder := strings.Builder{}
	builder.WriteString(sanitize(message))
	builder.WriteString(":")
	builder.WriteString(operation)
	builder.WriteString(":")
	for _, frame := range stackTrace {
		builder.WriteString(frame.Method)
		builder.WriteString(frame.FullFilePath)
		builder.WriteString(strconv.Itoa(int(frame.Line)))
		builder.WriteString(":")
	}

	return hash(builder.String())
}

// Parses a stack trace from the json payload, where the frames follow the following format:
//
//   {
//     "traces.0.filename": "/local/p4clients/pkgbuild-8JP8M/workspace/src/StreamSchedule/storage/segments.go",
//     "traces.0.line": "22",
//     "traces.0.method": "init",
//     "traces.1.filename": "/opt/brazil-pkg-cache/packages/GoLang/GoLang-1.x.6797.0/AL2_x86_64/DEV.STD.PTHREAD/build/lib/src/runtime/proc.go",
//     "traces.1.line": "5420",
//     "traces.1.method": "doInit",
//   }
//
// StreamSchedule and Nexus currently log stack traces in this way.
func parseJsonStackTrace(payload jsonObject) []StackTrace {
	positionToStack := make(map[int32]*StackTrace)
	var maxLine int32 = -1

	for key := range payload {
		if !strings.HasPrefix(key, "traces.") {
			continue
		}

		tokens := strings.Split(key, ".")
		if len(tokens) != 3 {
			continue
		}

		position64, err := strconv.ParseInt(tokens[1], 10, 32)
		if err != nil {
			continue
		}
		position := int32(position64)

		if positionToStack[position] == nil {
			positionToStack[position] = &StackTrace{}
		}

		switch tokens[2] {
		case "filename":
			filename := payload.getString(key)
			if filename == "" {
				continue
			}
			positionToStack[position].FullFilePath = filename

		case "line":
			lineStr := payload.getString(key)
			if lineStr == "" {
				continue
			}

			lineInt64, err := strconv.ParseInt(lineStr, 10, 32)
			if err != nil {
				continue
			}

			line := int32(lineInt64)
			positionToStack[position].Line = line

			if line > maxLine {
				maxLine = line
			}

		case "method":
			method := payload.getString(key)
			if method == "" {
				continue
			}
			positionToStack[position].Method = method
		}
	}

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

	stackTraces := make([]StackTrace, 0, len(positionToStack))
	for i := int32(0); i <= maxLine; i++ {
		stackTrace := positionToStack[i]
		if stackTrace == nil {
			continue
		}
		stackTraces = append(stackTraces, *stackTrace)
	}

	// Add Brazil package properties
	for i, stackFrame := range stackTraces {
		brazilPath := ParseBrazilPath(stackFrame.FullFilePath)
		if brazilPath == nil {
			continue
		}

		brazilPackageName := brazilPath.Package
		stackFrame.BrazilPackageName = brazilPackageName

		brazilFilePath := brazilPath.FilePath
		stackFrame.ShortFilePath = brazilFilePath

		stackFrame.Relevant = brazilPath.IsServicePackage
		if brazilPackageName != "" {
			stackFrame.PackageURL = createPackageURL(brazilPath.Package)
		}
		if brazilPath.HasMainlineBranch {
			if brazilPackageName != "" && brazilFilePath != "" {
				stackFrame.LineURL = createFileURL(brazilPackageName, brazilFilePath, stackFrame.Line)
			}
		}

		stackTraces[i] = stackFrame
	}

	return stackTraces
}
