package stfe

import (
	"context"
	"fmt"
	"time"

	"net/http"

	"github.com/golang/glog"
	"github.com/google/trillian"
)

// handler implements the http.Handler interface, and contains a reference
// to an STFE server instance as well as a function that uses it.
type handler struct {
	instance *Instance // STFE server instance
	endpoint string    // e.g., add-entry
	method   string    // e.g., GET
	handler  func(context.Context, *Instance, http.ResponseWriter, *http.Request) (int, error)
}

func (a handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	// export prometheus metrics
	var now time.Time = time.Now()
	var statusCode int
	defer func() {
		rspcnt.Inc(a.instance.LogParameters.id(), a.endpoint, fmt.Sprintf("%d", statusCode))
		latency.Observe(time.Now().Sub(now).Seconds(), a.instance.LogParameters.id(), a.endpoint, fmt.Sprintf("%d", statusCode))
	}()
	reqcnt.Inc(a.instance.LogParameters.id(), a.endpoint)

	ctx, cancel := context.WithDeadline(r.Context(), now.Add(a.instance.Deadline))
	defer cancel()

	if r.Method != a.method {
		glog.Warningf("%s: got HTTP %s, wanted HTTP %s", a.instance.LogParameters.Prefix+a.endpoint, r.Method, a.method)
		a.sendHTTPError(w, http.StatusMethodNotAllowed, fmt.Errorf("method not allowed: %s", r.Method))
		return
	}

	statusCode, err := a.handler(ctx, a.instance, w, r)
	if err != nil {
		glog.Warningf("handler error %s/%s: %v", a.instance.LogParameters.Prefix, a.endpoint, err)
		a.sendHTTPError(w, statusCode, err)
	}
}

func (a handler) sendHTTPError(w http.ResponseWriter, statusCode int, err error) {
	http.Error(w, http.StatusText(statusCode), statusCode)
}

func addEntry(ctx context.Context, i *Instance, w http.ResponseWriter, r *http.Request) (int, error) {
	glog.V(3).Info("handling add-entry request")
	leaf, appendix, err := i.LogParameters.newAddEntryRequest(r)
	if err != nil {
		return http.StatusBadRequest, err
	}

	trsp, err := i.Client.QueueLeaf(ctx, &trillian.QueueLeafRequest{
		LogId: i.LogParameters.TreeId,
		Leaf: &trillian.LogLeaf{
			LeafValue: leaf,
			ExtraData: appendix,
		},
	})
	if status, errInner := checkQueueLeaf(trsp, err); errInner != nil {
		return status, fmt.Errorf("bad QueueLeafResponse: %v", errInner)
	}

	sdi, err := i.LogParameters.genV1Sdi(trsp.QueuedLeaf.Leaf.LeafValue)
	if err != nil {
		return http.StatusInternalServerError, fmt.Errorf("failed creating signed debug info: %v", err)
	}
	rsp, err := sdi.MarshalB64()
	if err != nil {
		return http.StatusInternalServerError, err
	}
	if err := writeJsonResponse(rsp, w); err != nil {
		return http.StatusInternalServerError, err
	}
	return http.StatusOK, nil
}

// getEntries provides a list of entries from the Trillian backend
func getEntries(ctx context.Context, i *Instance, w http.ResponseWriter, r *http.Request) (int, error) {
	glog.V(3).Info("handling get-entries request")
	req, err := i.LogParameters.newGetEntriesRequest(r)
	if err != nil {
		return http.StatusBadRequest, err
	}

	trsp, err := i.Client.GetLeavesByRange(ctx, &trillian.GetLeavesByRangeRequest{
		LogId:      i.LogParameters.TreeId,
		StartIndex: req.Start,
		Count:      req.End - req.Start + 1,
	})
	if status, errInner := checkGetLeavesByRange(req, trsp, err); errInner != nil {
		return status, fmt.Errorf("bad GetLeavesByRangeResponse: %v", errInner)
	}

	rsp, err := i.LogParameters.newGetEntriesResponse(trsp.Leaves)
	if err != nil {
		return http.StatusInternalServerError, fmt.Errorf("failed creating GetEntriesResponse: %v", err)
	}
	if err := writeJsonResponse(rsp, w); err != nil {
		return http.StatusInternalServerError, err
	}
	return http.StatusOK, nil
}

// getAnchors provides a list of configured trust anchors
func getAnchors(_ context.Context, i *Instance, w http.ResponseWriter, _ *http.Request) (int, error) {
	glog.V(3).Info("handling get-anchors request")
	data := i.LogParameters.newGetAnchorsResponse()
	if err := writeJsonResponse(data, w); err != nil {
		return http.StatusInternalServerError, err
	}
	return http.StatusOK, nil
}

// getProofByHash provides an inclusion proof based on a given leaf hash
func getProofByHash(ctx context.Context, i *Instance, w http.ResponseWriter, r *http.Request) (int, error) {
	glog.V(3).Info("handling get-proof-by-hash request")
	req, err := i.LogParameters.newGetProofByHashRequest(r)
	if err != nil {
		return http.StatusBadRequest, err
	}

	trsp, err := i.Client.GetInclusionProofByHash(ctx, &trillian.GetInclusionProofByHashRequest{
		LogId:           i.LogParameters.TreeId,
		LeafHash:        req.Hash,
		TreeSize:        req.TreeSize,
		OrderBySequence: true,
	})
	if status, errInner := checkGetInclusionProofByHash(i.LogParameters, trsp, err); errInner != nil {
		return status, fmt.Errorf("bad GetInclusionProofByHashResponse: %v", errInner)
	}

	rsp, err := NewInclusionProofV1(i.LogParameters.LogId, uint64(req.TreeSize), trsp.Proof[0]).MarshalB64()
	if err != nil {
		return http.StatusInternalServerError, err
	}
	if err := writeJsonResponse(rsp, w); err != nil {
		return http.StatusInternalServerError, err
	}
	return http.StatusOK, nil
}

// getConsistencyProof provides a consistency proof between two STHs
func getConsistencyProof(ctx context.Context, i *Instance, w http.ResponseWriter, r *http.Request) (int, error) {
	glog.V(3).Info("handling get-consistency-proof request")
	req, err := i.LogParameters.newGetConsistencyProofRequest(r)
	if err != nil {
		return http.StatusBadRequest, err
	}

	trsp, err := i.Client.GetConsistencyProof(ctx, &trillian.GetConsistencyProofRequest{
		LogId:          i.LogParameters.TreeId,
		FirstTreeSize:  int64(req.First),
		SecondTreeSize: int64(req.Second),
	})
	if status, errInner := checkGetConsistencyProof(i.LogParameters, trsp, err); errInner != nil {
		return status, fmt.Errorf("bad GetConsistencyProofResponse: %v", errInner)
	}

	rsp, err := NewConsistencyProofV1(i.LogParameters.LogId, req.First, req.Second, trsp.Proof).MarshalB64()
	if err != nil {
		return http.StatusInternalServerError, err
	}
	if err := writeJsonResponse(rsp, w); err != nil {
		return http.StatusInternalServerError, err
	}
	return http.StatusOK, nil
}

// getSth provides the most recent STH
func getSth(ctx context.Context, i *Instance, w http.ResponseWriter, _ *http.Request) (int, error) {
	glog.V(3).Info("handling get-sth request")
	trsp, err := i.Client.GetLatestSignedLogRoot(ctx, &trillian.GetLatestSignedLogRootRequest{
		LogId: i.LogParameters.TreeId,
	})
	if status, errInner := checkGetLatestSignedLogRoot(i.LogParameters, trsp, err); errInner != nil {
		return status, fmt.Errorf("bad GetLatestSignedLogRootResponse: %v", errInner)
	}

	th, err := NewTreeHeadV1(i.LogParameters, trsp.SignedLogRoot)
	if err != nil {
		return http.StatusInternalServerError, fmt.Errorf("failed creating tree head: %v", err)
	}
	sth, err := i.LogParameters.genV1Sth(th)
	if err != nil {
		return http.StatusInternalServerError, fmt.Errorf("failed creating signed tree head: %v", err)
	}
	rsp, err := sth.MarshalB64()
	if err != nil {
		return http.StatusInternalServerError, err
	}
	if err := writeJsonResponse(rsp, w); err != nil {
		return http.StatusInternalServerError, err
	}
	return http.StatusOK, nil
}