// (c) 2021, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

package api

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"time"

	"github.com/ava-labs/avalanchego/ids"
	"github.com/ava-labs/ortelius/cfg"
	"github.com/ava-labs/ortelius/services/indexes/avax"
	"github.com/ava-labs/ortelius/services/indexes/params"
	"github.com/ava-labs/ortelius/servicesctrl"
	"github.com/ava-labs/ortelius/utils"
	"github.com/gocraft/web"
	"go.uber.org/zap"
)

var (
	// ErrCacheableFnFailed is returned when the execution of a CacheableFn
	// fails.
	ErrCacheableFnFailed = errors.New("failed to load resource")
)

// Context is the base context for APIs in the ortelius systems
type Context struct {
	sc *servicesctrl.Control

	networkID   uint32
	avaxAssetID ids.ID

	delayCache  *utils.DelayCache
	avaxReader  *avax.Reader
	connections *utils.Connections
}

// NetworkID returns the networkID this request is for
func (c *Context) NetworkID() uint32 {
	return c.networkID
}

func (c *Context) cacheGet(key string) ([]byte, error) {
	ctxget, cancelFnGet := context.WithTimeout(context.Background(), cfg.CacheTimeout)
	defer cancelFnGet()
	// Get from cache or, if there is a cache miss, from the cacheablefn
	return c.delayCache.Cache.Get(ctxget, key)
}

func (c *Context) cacheRun(reqTime time.Duration, cacheable utils.Cacheable) (interface{}, error) {
	ctxreq, cancelFnReq := context.WithTimeout(context.Background(), reqTime)
	defer cancelFnReq()

	return cacheable.CacheableFn(ctxreq)
}

// WriteCacheable writes to the http response the output of the given Cacheable's
// function, either from the cache or from a new execution of the function
func (c *Context) WriteCacheable(w http.ResponseWriter, cacheable utils.Cacheable) {
	key := utils.CacheKey(c.NetworkID(), cacheable.Key...)

	// Get from cache or, if there is a cache miss, from the cacheablefn
	resp, err := c.cacheGet(key)
	switch err {
	case nil:
	default:
		var obj interface{}
		obj, err = c.cacheRun(cfg.RequestTimeout, cacheable)
		if err == nil {
			resp, err = json.Marshal(obj)
			if err == nil {
				c.delayCache.Worker.TryEnque(&utils.CacheJob{Key: key, Body: &resp, TTL: cacheable.TTL})
			}
		}
	}

	// Write error or response
	if err != nil {
		c.sc.Log.Warn("server error",
			zap.Error(err),
		)
		c.WriteErr(w, 500, ErrCacheableFnFailed)
		return
	}
	WriteJSON(w, resp)
}

// WriteErr writes an error response to the http response
func (c *Context) WriteErr(w http.ResponseWriter, code int, err error) {
	errBytes, err := json.Marshal(&ErrorResponse{
		Code:    code,
		Message: err.Error(),
	})
	if err != nil {
		w.WriteHeader(500)
		c.sc.Log.Warn("marshal error",
			zap.Error(err),
		)
		return
	}

	w.WriteHeader(code)
	fmt.Fprint(w, string(errBytes))
}

func (*Context) setHeaders(w web.ResponseWriter, r *web.Request, next web.NextMiddlewareFunc) {
	h := w.Header()
	h.Add("access-control-allow-headers", "Accept, Content-Type, Content-Length, Accept-Encoding")
	h.Add("access-control-allow-methods", "GET")
	h.Add("access-control-allow-origin", "*")

	h.Add("Content-Type", "application/json")

	next(w, r)
}

func (*Context) notFoundHandler(w web.ResponseWriter, r *web.Request) {
	WriteErr(w, 404, "Not Found")
}

func (c *Context) cacheKeyForID(name string, id string) []string {
	return []string{"avax", name, params.CacheKey("id", id)}
}

func (c *Context) cacheKeyForParams(name string, p params.Param) []string {
	return append([]string{"avax", name}, p.CacheKey()...)
}

func newContextSetter(sc *servicesctrl.Control, networkID uint32, connections *utils.Connections, delayCache *utils.DelayCache) func(*Context, web.ResponseWriter, *web.Request, web.NextMiddlewareFunc) {
	return func(c *Context, w web.ResponseWriter, r *web.Request, next web.NextMiddlewareFunc) {
		c.sc = sc
		c.connections = connections
		c.delayCache = delayCache
		c.networkID = networkID

		// Execute handler
		next(w, r)
	}
}
