package common

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

	"github.com/DataDog/datadog-go/statsd"
	"github.com/getsentry/raven-go"
	"github.com/jmoiron/sqlx"
	"github.com/valyala/fasthttp"
	"gopkg.in/redis.v5"
)

// RavenClient is the raven client to which report errors happening.
// If nil, errors will just be fmt.Println'd
var RavenClient *raven.Client

// MethodData is a struct containing the data passed over to an API method.
type MethodData struct {
	User  Token
	DB    *sqlx.DB
	Doggo *statsd.Client
	R     *redis.Client
	Ctx   *fasthttp.RequestCtx
}

// ClientIP implements a best effort algorithm to return the real client IP, it parses
// X-Real-IP and X-Forwarded-For in order to work properly with reverse-proxies such us: nginx or haproxy.
func (md MethodData) ClientIP() string {
	clientIP := strings.TrimSpace(string(md.Ctx.Request.Header.Peek("X-Real-Ip")))
	if len(clientIP) > 0 {
		return clientIP
	}
	clientIP = string(md.Ctx.Request.Header.Peek("X-Forwarded-For"))
	if index := strings.IndexByte(clientIP, ','); index >= 0 {
		clientIP = clientIP[0:index]
	}
	clientIP = strings.TrimSpace(clientIP)
	if len(clientIP) > 0 {
		return clientIP
	}
	return md.Ctx.RemoteIP().String()
}

// Err logs an error. If RavenClient is set, it will use the client to report
// the error to sentry, otherwise it will just write the error to stdout.
func (md MethodData) Err(err error) {
	user := &raven.User{
		ID:       strconv.Itoa(md.User.UserID),
		Username: md.User.Value,
		IP:       md.Ctx.RemoteAddr().String(),
	}
	// Generate tags for error
	tags := map[string]string{
		"endpoint": string(md.Ctx.RequestURI()),
		"token":    md.User.Value,
	}
	_err(err, tags, user, md.Ctx)
}

// Err for peppy API calls
func Err(c *fasthttp.RequestCtx, err error) {
	// Generate tags for error
	tags := map[string]string{
		"endpoint": string(c.RequestURI()),
	}

	_err(err, tags, nil, c)
}

func _err(err error, tags map[string]string, user *raven.User, c *fasthttp.RequestCtx) {
	if RavenClient == nil {
		fmt.Println("ERROR!!!!")
		fmt.Println(err)
		return
	}

	// Create stacktrace
	st := raven.NewStacktrace(0, 3, []string{"zxq.co/ripple", "git.zxq.co/ripple"})

	ifaces := []raven.Interface{st, generateRavenHTTP(c)}
	if user != nil {
		ifaces = append(ifaces, user)
	}

	RavenClient.CaptureError(
		err,
		tags,
		ifaces...,
	)
}

func generateRavenHTTP(ctx *fasthttp.RequestCtx) *raven.Http {
	// build uri
	uri := ctx.URI()
	// safe to use b2s because a new string gets allocated eventually for
	// concatenation
	sURI := b2s(uri.Scheme()) + "://" + b2s(uri.Host()) + b2s(uri.Path())

	// build header map
	// using ctx.Request.Header.Len would mean calling .VisitAll two times
	// which can be quite expensive since it means iterating over all the
	// headers, so we give a rough estimate of the number of headers we expect
	// to have
	m := make(map[string]string, 16)
	ctx.Request.Header.VisitAll(func(k, v []byte) {
		// not using b2s because we mustn't keep references to the underlying
		// k and v
		m[string(k)] = string(v)
	})

	return &raven.Http{
		URL: sURI,
		// Not using b2s because raven sending is concurrent and may happen
		// AFTER the request, meaning that values could potentially be replaced
		// by new ones.
		Method:  string(ctx.Method()),
		Query:   string(uri.QueryString()),
		Cookies: string(ctx.Request.Header.Peek("Cookie")),
		Headers: m,
	}
}

// ID retrieves the Token's owner user ID.
func (md MethodData) ID() int {
	return md.User.UserID
}

// Query is shorthand for md.C.Query.
func (md MethodData) Query(q string) string {
	return b2s(md.Ctx.QueryArgs().Peek(q))
}

// HasQuery returns true if the parameter is encountered in the querystring.
// It returns true even if the parameter is "" (the case of ?param&etc=etc)
func (md MethodData) HasQuery(q string) bool {
	return md.Ctx.QueryArgs().Has(q)
}

// Unmarshal unmarshals a request's JSON body into an interface.
func (md MethodData) Unmarshal(into interface{}) error {
	return json.Unmarshal(md.Ctx.PostBody(), into)
}
