package callgraph

import (
	"encoding/json"
	"errors"
	"fmt"
	"strings"

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

type RPCProtocol string

const (
	HTTP       RPCProtocol = "HTTP"
	SQL        RPCProtocol = "SQL"
	Memcached  RPCProtocol = "Memcached"
	GRPC       RPCProtocol = "gRPC"
	UnknownRPC string      = "?"
)

type RPCAnnotation interface {
	json.Marshaler
	// Proto should return the RPCProtocol that the RPCAnnotation is
	// representing
	Proto() RPCProtocol
	// Fill populates the RPCAnnotation with data from a single Event
	// in a call. An error should be returned if the event is for an
	// unexpected RPCProtocol.
	Fill(*pbmsg.Event, *call) error
	// Name should return a brief string representing the method name
	// of the RPC. For HTTP calls, this might be the name of the
	// controller, for example.
	Name() string
}

// newAnnotation generates the appropriate RPCAnnotation for a given
// event within the context of a call
func newAnnotation(e *pbmsg.Event, call *call) RPCAnnotation {
	extra := e.GetExtra()
	if extra == nil {
		return nil
	}
	if extra.GetHttp() != nil {
		a := HTTPAnnotation{}
		a.Fill(e, call)
		return &a
	}
	if extra.GetSql() != nil {
		a := SQLAnnotation{}
		a.Fill(e, call)
		return &a
	}
	if extra.GetMemcached() != nil {
		a := MemcachedAnnotation{}
		a.Fill(e, call)
		return &a
	}
	if extra.GetGrpc() != nil {
		a := GRPCAnnotation{}
		a.Fill(e, call)
		return &a
	}
	return nil
}

type HTTPAnnotation struct {
	HTTPURIPath string
	HTTPStatus  int
	HTTPMethod  string
	route       string
}

func (a *HTTPAnnotation) MarshalJSON() ([]byte, error) {
	return json.Marshal(struct {
		HTTPAnnotation
		Protocol string
		Name     string
	}{*a, string(a.Proto()), a.Name()})
}

func (a *HTTPAnnotation) Proto() RPCProtocol {
	return HTTP
}

func (a *HTTPAnnotation) Name() string {
	return a.route
}

func (a *HTTPAnnotation) Fill(e *pbmsg.Event, c *call) error {
	extra := e.GetExtra()
	if extra == nil {
		return errors.New("no Extra available to annotate")
	}
	http := extra.GetHttp()
	if http == nil {
		return errors.New("no HTTPExtra available to annotate")
	}
	if m := http.GetMethod(); m != pbmsg.ExtraHTTP_UNKNOWN {
		a.HTTPMethod = m.String()
	}
	if p := http.GetUriPath(); p != "" {
		a.HTTPURIPath = p
	}
	if s := http.GetStatusCode(); s != 0 {
		a.HTTPStatus = int(s)
	}

	if a.route == "" && a.HTTPURIPath != "" && c.server.svc != "" {
		routemap, ok := routes.Routes[c.server.svc]
		if !ok {
			a.route = UnknownRPC
		} else {
			a.route = routemap.LookupTrimSlash(a.HTTPURIPath, a.HTTPMethod)
		}
	}

	return nil
}

type SQLAnnotation struct {
	DBName        string
	DBUser        string
	StrippedQuery string
	Tables        []string
}

func (a *SQLAnnotation) Proto() RPCProtocol {
	return SQL
}

func (a *SQLAnnotation) Name() string {
	return a.DBUser + "@" + a.DBName + ":" + strings.Join(a.Tables, "/")
}

func (a *SQLAnnotation) Fill(e *pbmsg.Event, c *call) error {
	extra := e.GetExtra()
	if extra == nil {
		return errors.New("no Extra available to annotate")
	}
	sql := extra.GetSql()
	if sql == nil {
		return errors.New("no SQLExtra available to annotate")
	}
	if db := sql.GetDatabaseName(); db != "" {
		a.DBName = db
	}
	if u := sql.GetDatabaseUser(); u != "" {
		a.DBUser = u
	}
	if q := sql.GetStrippedQuery(); q != "" {
		a.StrippedQuery = q

		tables, err := pgtables.Cache.Tables(q)
		if err == nil {
			a.Tables = tables
		}
	}
	return nil
}

func (a *SQLAnnotation) MarshalJSON() ([]byte, error) {
	return json.Marshal(struct {
		SQLAnnotation
		Protocol string
		Name     string
	}{*a, string(a.Proto()), a.Name()})
}

type MemcachedAnnotation struct {
	Host          string
	Command       string
	KeysRequested int
	KeysReceived  int
}

func (a *MemcachedAnnotation) Proto() RPCProtocol {
	return Memcached
}

func (a *MemcachedAnnotation) Name() string {
	if cmd := strings.ToLower(a.Command); cmd == "get" || cmd == "gets" {
		if a.KeysReceived >= a.KeysRequested {
			return "cache-hit"
		} else if a.KeysReceived > 0 {
			return "cache-partial-hit"
		} else {
			return "cache-miss"
		}
	}
	return a.Command
}

func (a *MemcachedAnnotation) Fill(e *pbmsg.Event, c *call) error {
	extra := e.GetExtra()
	if extra == nil {
		return errors.New("no Extra available to annotate")
	}
	mc := extra.GetMemcached()
	if mc == nil {
		return errors.New("no MemcachedExtra available to annotate")
	}

	switch mc.GetCommand() {
	case pbmsg.ExtraMemcached_GET:
		a.Command = "get"
	case pbmsg.ExtraMemcached_GETS:
		a.Command = "gets"
	case pbmsg.ExtraMemcached_SET:
		a.Command = "set"
	case pbmsg.ExtraMemcached_ADD:
		a.Command = "add"
	case pbmsg.ExtraMemcached_REPLACE:
		a.Command = "replace"
	case pbmsg.ExtraMemcached_APPEND:
		a.Command = "append"
	case pbmsg.ExtraMemcached_PREPEND:
		a.Command = "prepend"
	case pbmsg.ExtraMemcached_CAS:
		a.Command = "cas"
	case pbmsg.ExtraMemcached_DELETE:
		a.Command = "delete"
	case pbmsg.ExtraMemcached_INCR:
		a.Command = "incr"
	case pbmsg.ExtraMemcached_DECR:
		a.Command = "decr"
	}

	if isRequestEvent(e) {
		a.KeysRequested = int(mc.GetNKeys())
	} else if isResponseEvent(e) {
		a.KeysReceived = int(mc.GetNKeys())
	}

	if a.Host == "" {
		a.Host = extra.GetPeer()
	}

	return nil
}

func (a *MemcachedAnnotation) MarshalJSON() ([]byte, error) {
	return json.Marshal(struct {
		MemcachedAnnotation
		Protocol string
		Name     string
	}{*a, string(a.Proto()), a.Name()})
}

type GRPCAnnotation struct {
	Method string
}

func (a *GRPCAnnotation) Proto() RPCProtocol { return GRPC }
func (a *GRPCAnnotation) Name() string {
	return fmt.Sprintf("grpc: %s", a.Method)
}

func (a *GRPCAnnotation) Fill(e *pbmsg.Event, c *call) error {
	extra := e.GetExtra()
	if extra == nil {
		return errors.New("no Extra available to annotate")
	}
	grpc := extra.GetGrpc()
	if grpc == nil {
		return errors.New("no GRPCExtra available to annotate")
	}

	if method := grpc.GetMethod(); method != "" {
		a.Method = method
	}
	return nil
}

func (a *GRPCAnnotation) MarshalJSON() ([]byte, error) {
	return json.Marshal(struct {
		GRPCAnnotation
		Protocol string
		Name     string
	}{*a, string(a.Proto()), a.Name()})
}
