package ctxlog

import (
	"context"
	"net/http"
	"strconv"

	"sync/atomic"

	"io"

	goctx "golang.org/x/net/context"
)

// Logger logs a set of keyvals. An implementation should consider logging
// values appended to the context.
type Logger interface {
	LogCtx(ctx goctx.Context, keyvals ...interface{})
	DebugCtx(ctx goctx.Context, keyvals ...interface{})
}

// HandlerC allows us to handle old style http request contexts
type HandlerC interface {
	ServeHTTPC(ctx goctx.Context, w http.ResponseWriter, r *http.Request)
}

// CtxAppender appends values to a context and reads those values from the context later.
type CtxAppender interface {
	// Append appends vals to a context
	Append(ctx goctx.Context, vals ...interface{}) goctx.Context
	// From retrieves all vals from a context
	From(ctx goctx.Context) []interface{}
}

// CtxHandler listens for HTTP requests and injects a debug ID if one is needed
type CtxHandler struct {
	// Either NextC or Next should be provided based on the handler that your version of Goji provides.
	NextC HandlerC
	Next  http.Handler

	Ctxlog *Ctxlog
	Logger Logger

	// IDFetcher is optional and fetches a request ID from the context.
	IDFetcher func(ctx context.Context) string
}

type Ctxlog struct {
	// Required parameters
	CtxDims    CtxAppender
	ElevateKey interface{}

	// These are optional parameters
	ElevateLogHeader string
	LogIDHeader      string

	// This is optional but honestly you probably want to set it to something large and random
	StartingIndex int64
}

var _ http.Handler = &CtxHandler{}

func (c *Ctxlog) GetElevateLogHeader() string {
	if c.ElevateLogHeader == "" {
		return "X-Ctxlog-Elevate"
	}
	return c.ElevateLogHeader
}

func (c *Ctxlog) GetLogIDHeader() string {
	if c.LogIDHeader == "" {
		return "X-Ctxlog-LogID"
	}
	return c.LogIDHeader
}

func (c *Ctxlog) NewDebugID() string {
	return strconv.FormatInt(atomic.AddInt64(&c.StartingIndex, 1), 10)
}

func (c *CtxHandler) getIDFromRequest(req *http.Request) string {
	if c.IDFetcher != nil {
		requestID := c.IDFetcher(req.Context())
		if requestID != "" {
			return requestID
		}
	}

	ctxLogHeader := req.Header.Get(c.Ctxlog.GetLogIDHeader())
	if ctxLogHeader != "" {
		return ctxLogHeader
	}
	return ""
}

// ServeHTTPC calls ServeHTTP but sets req's context to ctx, ignoring what's already there
func (c *CtxHandler) ServeHTTPC(ctx goctx.Context, rw http.ResponseWriter, req *http.Request) {
	c.ServeHTTP(rw, req.WithContext(ctx))
}

// RequestDoer is anything that can issue HTTP requests
func (c *CtxHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
	currentDebugID := c.getIDFromRequest(req)
	if currentDebugID == "" {
		currentDebugID = c.Ctxlog.NewDebugID()
	}
	rw.Header().Set(c.Ctxlog.GetLogIDHeader(), currentDebugID)
	req = req.WithContext(c.Ctxlog.CtxDims.Append(req.Context(), c.Ctxlog.GetLogIDHeader(), currentDebugID, "in_http_path", req.URL.Path, "in_http_remote_addr", req.RemoteAddr))

	if c.Ctxlog.ElevateKey != nil {
		isElevateSet := req.Header.Get(c.Ctxlog.GetElevateLogHeader())
		if isElevateSet != "" {
			req = req.WithContext(goctx.WithValue(req.Context(), c.Ctxlog.ElevateKey, true))
		}
	}

	Log(req.Context(), c.Logger, func() {
		if c.NextC != nil {
			c.NextC.ServeHTTPC(req.Context(), rw, req)
		} else {
			c.Next.ServeHTTP(rw, req)
		}
	})
}

func (c *Ctxlog) ExtractDebugInfo(ctx goctx.Context) (string, bool) {
	var currentDebugID string
	currentIsElevated := ctx.Value(c.ElevateKey) != nil
	currentDims := c.CtxDims.From(ctx)
	// Need to extract out log ID and if it's elevated
	for i := 0; i < len(currentDims); i += 2 {
		if i == len(currentDims)-1 {
			continue
		}
		key, isString := currentDims[i].(string)
		if !isString {
			continue
		}
		if key == c.GetLogIDHeader() {
			next, isString := currentDims[i+1].(string)
			if !isString {
				continue
			}
			currentDebugID = next
		}
	}
	return currentDebugID, currentIsElevated
}

// WrapHTTPRequestWithCtxlog returns a function that can wrap a NewRequest method with one that will also add debuglog headers
func WrapHTTPRequestWithCtxlog(c *Ctxlog, originalNewRequest func(string, string, io.Reader) (*http.Request, error)) func(goctx.Context, string, string, io.Reader) (*http.Request, error) {
	return func(ctx goctx.Context, method string, url string, body io.Reader) (*http.Request, error) {
		req, err := originalNewRequest(method, url, body)
		if err != nil {
			return nil, err
		}
		req = req.WithContext(ctx)
		return NewHTTPRequest(c, req), nil
	}
}

func NewHTTPRequest(c *Ctxlog, req *http.Request) *http.Request {
	currentDebugID, currentIsElevated := c.ExtractDebugInfo(req.Context())

	if currentDebugID == "" {
		currentDebugID = c.NewDebugID()
		req = req.WithContext(c.CtxDims.Append(req.Context(), c.GetLogIDHeader(), currentDebugID))
	}

	req.Header.Set(c.GetLogIDHeader(), currentDebugID)

	if currentIsElevated {
		req = req.WithContext(goctx.WithValue(req.Context(), c.ElevateKey, currentIsElevated))
		req.Header.Set(c.GetElevateLogHeader(), "true")
	}

	return req
}

// RequestDoer can process a HTTP request and return a response
type RequestDoer interface {
	Do(req *http.Request) (*http.Response, error)
}

var _ RequestDoer = &http.Client{}

func Log(ctx goctx.Context, logger Logger, f func(), params ...interface{}) {
	if len(params)%2 != 0 {
		panic("expect even number of params")
	}

	p1 := make([]interface{}, 0, len(params)+3)
	p1 = append(p1, params...)
	p1 = append(p1, "phase", "start", "starting a request")
	logger.DebugCtx(ctx, p1...)

	p2 := make([]interface{}, 0, len(params)+3)
	p2 = append(p2, params...)
	p2 = append(p2, "phase", "end", "ending a request")
	defer logger.DebugCtx(ctx, p2...)

	f()
}

type LoggedDoer struct {
	Logger Logger
	C      *Ctxlog

	// An example would be http.Client{}
	Client RequestDoer
}

func (l *LoggedDoer) Do(req *http.Request) (res *http.Response, err error) {
	Log(req.Context(), l.Logger, func() {
		req = NewHTTPRequest(l.C, req)
		res, err = l.Client.Do(req)
	})
	return
}
