package query

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

	"github.com/influxdata/flux"
	platform "github.com/influxdata/influxdb/v2"
)

const (
	PreferHeaderKey                = "Prefer"
	PreferNoContentHeaderValue     = "return-no-content"
	PreferNoContentWErrHeaderValue = "return-no-content-with-error"
)

// Request represents the query to run.
// Options to mutate the header associated to this Request can be specified
// via `WithOption` or associated methods.
// One should always `Request.ApplyOptions()` before encoding and sending the request.
type Request struct {
	// Scope
	Authorization  *platform.Authorization `json:"authorization,omitempty"`
	OrganizationID platform.ID             `json:"organization_id"`

	// Command

	// Compiler converts the query to a specification to run against the data.
	Compiler flux.Compiler `json:"compiler"`

	// Source represents the ultimate source of the request.
	Source string `json:"source"`

	// compilerMappings maps compiler types to creation methods
	compilerMappings flux.CompilerMappings

	options []RequestHeaderOption
}

// SetReturnNoContent sets the header for a Request to return no content.
func SetReturnNoContent(header http.Header, withError bool) {
	if withError {
		header.Set(PreferHeaderKey, PreferNoContentWErrHeaderValue)
	} else {
		header.Set(PreferHeaderKey, PreferNoContentHeaderValue)
	}
}

// RequestHeaderOption is a function that mutates the header associated to a Request.
type RequestHeaderOption = func(header http.Header) error

// WithOption adds a RequestHeaderOption to this Request.
func (r *Request) WithOption(option RequestHeaderOption) {
	r.options = append(r.options, option)
}

// WithReturnNoContent makes this Request return no content.
func (r *Request) WithReturnNoContent(withError bool) {
	r.WithOption(func(header http.Header) error {
		SetReturnNoContent(header, withError)
		return nil
	})
}

// ApplyOptions applies every option added to this Request to the given header.
func (r *Request) ApplyOptions(header http.Header) error {
	for _, visitor := range r.options {
		if err := visitor(header); err != nil {
			return err
		}
	}
	return nil
}

// WithCompilerMappings sets the query type mappings on the request.
func (r *Request) WithCompilerMappings(mappings flux.CompilerMappings) {
	r.compilerMappings = mappings
}

// UnmarshalJSON populates the request from the JSON data.
// WithCompilerMappings must have been called or an error will occur.
func (r *Request) UnmarshalJSON(data []byte) error {
	type Alias Request
	raw := struct {
		*Alias
		CompilerType flux.CompilerType `json:"compiler_type"`
		Compiler     json.RawMessage   `json:"compiler"`
	}{
		Alias: (*Alias)(r),
	}
	if err := json.Unmarshal(data, &raw); err != nil {
		return err
	}

	createCompiler, ok := r.compilerMappings[raw.CompilerType]
	if !ok {
		return fmt.Errorf("unsupported compiler type %q", raw.CompilerType)
	}

	c := createCompiler()
	if err := json.Unmarshal(raw.Compiler, c); err != nil {
		return err
	}
	r.Compiler = c

	return nil
}

func (r Request) MarshalJSON() ([]byte, error) {
	type Alias Request
	raw := struct {
		Alias
		CompilerType flux.CompilerType `json:"compiler_type"`
	}{
		Alias:        (Alias)(r),
		CompilerType: r.Compiler.CompilerType(),
	}
	return json.Marshal(raw)
}

type contextKey struct{}

var activeContextKey = contextKey{}

// ContextWithRequest returns a new context with a reference to the request.
func ContextWithRequest(ctx context.Context, req *Request) context.Context {
	return context.WithValue(ctx, activeContextKey, req)
}

// RequestFromContext retrieves a *Request from a context.
// If not request exists on the context nil is returned.
func RequestFromContext(ctx context.Context) *Request {
	v := ctx.Value(activeContextKey)
	if v == nil {
		return nil
	}
	return v.(*Request)
}

// ProxyRequest specifies a query request and the dialect for the results.
type ProxyRequest struct {
	// Request is the basic query request
	Request Request `json:"request"`

	// Dialect is the result encoder
	Dialect flux.Dialect `json:"dialect"`

	// dialectMappings maps dialect types to creation methods
	dialectMappings flux.DialectMappings
}

// WithCompilerMappings sets the compiler type mappings on the request.
func (r *ProxyRequest) WithCompilerMappings(mappings flux.CompilerMappings) {
	r.Request.WithCompilerMappings(mappings)
}

// WithDialectMappings sets the dialect type mappings on the request.
func (r *ProxyRequest) WithDialectMappings(mappings flux.DialectMappings) {
	r.dialectMappings = mappings
}

// UnmarshalJSON populates the request from the JSON data.
// WithCompilerMappings and WithDialectMappings must have been called or an error will occur.
func (r *ProxyRequest) UnmarshalJSON(data []byte) error {
	type Alias ProxyRequest
	raw := struct {
		*Alias
		DialectType flux.DialectType `json:"dialect_type"`
		Dialect     json.RawMessage  `json:"dialect"`
	}{
		Alias: (*Alias)(r),
	}
	if err := json.Unmarshal(data, &raw); err != nil {
		return err
	}

	createDialect, ok := r.dialectMappings[raw.DialectType]
	if !ok {
		return fmt.Errorf("unsupported dialect type %q", raw.DialectType)
	}

	d := createDialect()
	if err := json.Unmarshal(raw.Dialect, d); err != nil {
		return err
	}
	r.Dialect = d

	return nil
}

func (r ProxyRequest) MarshalJSON() ([]byte, error) {
	type Alias ProxyRequest
	raw := struct {
		Alias
		DialectType flux.DialectType `json:"dialect_type"`
	}{
		Alias:       (Alias)(r),
		DialectType: r.Dialect.DialectType(),
	}
	return json.Marshal(raw)
}
