package http

import (
	"encoding/json"
	"errors"
	"fmt"
	"io/ioutil"
	"net/http"
	"unicode/utf8"

	"github.com/influxdata/platform"
	"github.com/influxdata/platform/query"
	"github.com/influxdata/platform/query/csv"
	"github.com/julienschmidt/httprouter"
	"github.com/prometheus/client_golang/prometheus"
	"go.uber.org/zap"
)

// ExternalQueryHandler implements the /query API endpoint defined in the swagger doc.
// This only implements the POST method and only supports Spec or Flux queries.
type ExternalQueryHandler struct {
	*httprouter.Router

	Logger *zap.Logger

	ProxyQueryService   query.ProxyQueryService
	OrganizationService platform.OrganizationService
}

// NewExternalQueryHandler returns a new instance of QueryHandler.
func NewExternalQueryHandler() *ExternalQueryHandler {
	h := &ExternalQueryHandler{
		Router: httprouter.New(),
	}

	h.HandlerFunc("POST", "/query", h.handlePostQuery)
	return h
}

func decodeQueryRequest(r *http.Request, req *query.ProxyRequest, orgSvc platform.OrganizationService) error {
	orgName := r.FormValue("organization")
	if orgName == "" {
		return errors.New(`missing the "organization" parameter`)
	}
	o, err := orgSvc.FindOrganization(r.Context(), platform.OrganizationFilter{Name: &orgName})
	if err != nil {
		return err
	}
	req.Request.OrganizationID = o.ID
	request := struct {
		Spec    *query.Spec `json:"spec"`
		Query   string      `json:"query"`
		Type    string      `json:"type"`
		Dialect struct {
			Header         *bool    `json:"header"`
			Delimiter      string   `json:"delimiter"`
			CommentPrefix  string   `json:"commentPrefix"`
			DateTimeFormat string   `json:"dateTimeFormat"`
			Annotations    []string `json:"annotations"`
		}
	}{}

	switch r.Header.Get("Content-Type") {
	case "application/json":
		err := json.NewDecoder(r.Body).Decode(&request)
		if err != nil {
			return err
		}

		// Set defaults
		if request.Type == "" {
			request.Type = "flux"
		}

		if request.Dialect.Header == nil {
			header := true
			request.Dialect.Header = &header
		}
		if request.Dialect.Delimiter == "" {
			request.Dialect.Delimiter = ","
		}
		if request.Dialect.DateTimeFormat == "" {
			request.Dialect.DateTimeFormat = "RFC3339"
		}

		if request.Type != "flux" {
			return fmt.Errorf(`unknown query type: %s`, request.Type)
		}
		if len(request.Dialect.CommentPrefix) > 1 {
			return fmt.Errorf("invalid dialect comment prefix: must be length 0 or 1")
		}
		if len(request.Dialect.Delimiter) != 1 {
			return fmt.Errorf("invalid dialect delimeter: must be length  1")
		}
		for _, a := range request.Dialect.Annotations {
			switch a {
			case "group", "datatype", "default":
			default:
				return fmt.Errorf(`unknown dialect annotation type: %s`, a)
			}
		}

		switch request.Dialect.DateTimeFormat {
		case "RFC3339", "RFC3339Nano":
		default:
			return fmt.Errorf(`unknown dialect date time format: %s`, request.Dialect.DateTimeFormat)
		}

		if request.Query != "" {
			req.Request.Compiler = query.FluxCompiler{Query: request.Query}
		} else if request.Spec != nil {
			req.Request.Compiler = query.SpecCompiler{
				Spec: request.Spec,
			}
		} else {
			return errors.New(`request body requires either spec or query`)
		}
	default:
		q := r.FormValue("query")
		if q == "" {
			data, err := ioutil.ReadAll(r.Body)
			if err != nil {
				return err
			}
			q = string(data)
		}
		req.Request.Compiler = query.FluxCompiler{
			Query: q,
		}
	}

	switch r.Header.Get("Accept") {
	case "text/csv":
		fallthrough
	default:
		var delimiter rune
		dialect := request.Dialect
		if dialect.Delimiter != "" {
			delimiter, _ = utf8.DecodeRuneInString(dialect.Delimiter)
		}
		noHeader := false
		if dialect.Header != nil {
			noHeader = !*dialect.Header
		}
		// TODO(nathanielc): Use commentPrefix and dateTimeFormat
		// once they are supported.
		config := csv.ResultEncoderConfig{
			NoHeader:    noHeader,
			Delimiter:   delimiter,
			Annotations: dialect.Annotations,
		}
		req.Dialect = csv.Dialect{
			ResultEncoderConfig: config,
		}
	}
	return nil
}

func (h *ExternalQueryHandler) handlePostQuery(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()

	var req query.ProxyRequest
	if err := decodeQueryRequest(r, &req, h.OrganizationService); err != nil {
		EncodeError(ctx, err, w)
		return
	}

	hd, ok := req.Dialect.(HTTPDialect)
	if !ok {
		EncodeError(ctx, fmt.Errorf("unsupported dialect over HTTP %T", req.Dialect), w)
		return
	}
	hd.SetHeaders(w)

	n, err := h.ProxyQueryService.Query(ctx, w, &req)
	if err != nil {
		if n == 0 {
			// Only record the error headers IFF nothing has been written to w.
			EncodeError(ctx, err, w)
			return
		}
		h.Logger.Info("Error writing response to client",
			zap.String("handler", "transpilerde"),
			zap.Error(err),
		)
	}
}

// PrometheusCollectors satisifies the prom.PrometheusCollector interface.
func (h *ExternalQueryHandler) PrometheusCollectors() []prometheus.Collector {
	// TODO: gather and return relevant metrics.
	return nil
}