package callgraph

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

	"code.justin.tv/release/trace/analysis/paths"
	"code.justin.tv/release/trace/analysis/routes"
	"code.justin.tv/release/trace/api"
	"code.justin.tv/release/trace/pbmsg"
)

// txnode implements the CallNode interface, and is backed by a
// api.Call and a api.Service to represent the client.
type txnode struct {
	client *api.Service
	call   *api.Call
}

func TransactionToCallgraph(tx *api.Transaction) CallNode {
	node := &txnode{client: tx.GetClient(), call: tx.GetRoot()}
	return node
}

func (t *txnode) Children() []Node {
	children := make([]Node, len(t.call.GetSubcalls()))
	svc := t.call.GetSvc()
	for i, subcall := range t.call.GetSubcalls() {
		children[i] = &txnode{client: svc, call: subcall}
	}
	return children
}

func (t *txnode) Label(dim StringDimension) string {
	switch dim {
	case Client:
		return t.clientName()
	case ClientHost:
		if svc := t.client; svc != nil {
			return svc.Host
		}
	case Server:
		return t.serverName()
	case ServerHost:
		if svc := t.call.GetSvc(); svc != nil {
			return svc.Host
		}
	case Signature:
		return signature(t)
	case RPCName:
		return t.rpcName()
	}
	return ""
}

func (t *txnode) clientName() string {
	if svc := t.client; svc != nil {
		switch {
		case svc.Name != "":
			return svc.Name
		case svc.Host != "":
			return svc.Host
		}
	}
	return "unknown"
}

func (t *txnode) serverName() string {
	if svc := t.call.GetSvc(); svc != nil {
		switch {
		case svc.Name != "":
			return svc.Name
		case svc.Host != "":
			return svc.Host
		}
	}
	if t.call != nil && t.call.RequestSentTo != "" {
		return t.call.RequestSentTo
	}
	return "unknown"
}

func (t *txnode) rpcName() string {
	params := t.call.GetParams()
	if params == nil {
		return "unknown"
	}

	if http := params.GetHttp(); http != nil {
		return t.httpRoute()
	}

	if sql := params.GetSql(); sql != nil {
		return sql.Dbuser + "@" + sql.Dbname + ":" + strings.Join(sql.Tables, "/")
	}

	if grpc := params.GetGrpc(); grpc != nil {
		return grpc.Method
	}
	return "unknown"
}

// Returns HTTP route if its available, else returns empty string
func (t *txnode) httpRoute() string {
	http := t.call.GetParams().GetHttp()
	if http != nil && http.UriPath == "" {
		return ""
	}
	svc := t.call.GetSvc()
	if svc != nil && svc.Name == "" {
		return ""
	}

	routemap, ok := routes.Routes[svc.Name]
	if !ok {
		return ""
	}

	return routemap.LookupTrimSlash(http.UriPath, http.Method.String())
}

func (t *txnode) IntValue(dim Int64Dimension) int64 {
	switch dim {
	case ClientPid:
		if t.client != nil {
			return int64(t.client.Pid)
		}
	case ClientStart:
		return safeUnixTime(api.ClientStart(t.call))
	case ClientEnd:
		return safeUnixTime(api.ClientEnd(t.call))
	case ClientNanos:
		return int64(api.ClientDuration(t.call))
	case ServerPid:
		if svc := t.call.GetSvc(); svc != nil {
			return int64(svc.Pid)
		}
	case ServerStart:
		return safeUnixTime(api.ServiceStart(t.call))
	case ServerEnd:
		return safeUnixTime(api.ServiceEnd(t.call))
	case ServerNanos:
		return int64(api.ServiceDuration(t.call))
	}
	return 0
}

// returns input time as unix nanoseconds, but has a special case for
// zero-valued input time to return zero.
func safeUnixTime(t time.Time) int64 {
	if t.IsZero() {
		return 0
	}
	return t.UnixNano()
}

func (t *txnode) MarshalJSON() ([]byte, error) {
	children := make(map[string]*txnode)
	for _, child := range t.Children() {
		var path []uint32
		if call := child.(*txnode).call; call != nil {
			path = call.Path
		}
		pathEnd := path[len(path)-1]
		children[strconv.Itoa(int(pathEnd))] = child.(*txnode)
	}
	type data struct {
		Client      string
		ClientStart int64
		ClientEnd   int64
		ClientNanos int64
		ClientHost  string
		ClientPid   int64

		Server      string
		ServerStart int64
		ServerEnd   int64
		ServerNanos int64
		ServerHost  string
		ServerPid   int64

		Signature string
		Path      string

		RPC RPCAnnotation `json:",omitempty"`

		Children map[string]*txnode
	}

	var path []uint32
	if t.call != nil {
		path = t.call.Path
	}

	d := data{
		Client:      t.Label(Client),
		ClientHost:  t.Label(ClientHost),
		ClientPid:   t.IntValue(ClientPid),
		ClientStart: t.IntValue(ClientStart),
		ClientEnd:   t.IntValue(ClientEnd),
		ClientNanos: t.IntValue(ClientNanos),

		Server:      t.Label(Server),
		ServerHost:  t.Label(ServerHost),
		ServerPid:   t.IntValue(ServerPid),
		ServerStart: t.IntValue(ServerStart),
		ServerEnd:   t.IntValue(ServerEnd),
		ServerNanos: t.IntValue(ServerNanos),

		Signature: t.Label(Signature),
		Path:      paths.PathToString(path),

		RPC: paramsToAnnotation(t.call.GetParams()),

		Children: children,
	}

	return json.Marshal(d)
}

func (t *txnode) Add(e *pbmsg.Event) {}

// Convert from proto struct to the interface intended for JSON
// serialization. This is a light shim to achieve backwards
// compatibility: we wrote the web frontend off of RPCAnnotation and
// only later added RPCParams. This code allows us to continue using
// the same old web frontend JavaScript off of the new RPCParams.
func paramsToAnnotation(p *api.RPCParams) RPCAnnotation {
	if p == nil {
		return nil
	}

	if h := p.GetHttp(); h != nil {
		return &HTTPAnnotation{
			HTTPURIPath: h.UriPath,
			HTTPStatus:  int(h.Status),
			HTTPMethod:  h.Method.String(),
			route:       h.Route,
		}
	}

	if s := p.GetSql(); s != nil {
		return &SQLAnnotation{
			DBName:        s.Dbname,
			DBUser:        s.Dbuser,
			StrippedQuery: s.StrippedQuery,
			Tables:        s.Tables,
		}
	}

	if g := p.GetGrpc(); g != nil {
		return &GRPCAnnotation{
			Method: g.Method,
		}
	}

	if m := p.GetMemcached(); m != nil {
		return &MemcachedAnnotation{
			Command:       m.Command.String(),
			KeysRequested: int(m.NKeysRequest),
			KeysReceived:  int(m.NKeysResponse),
		}
	}

	return nil
}
