package gqlclient

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"mime"
	"net/http"
)

// Client is a GraphQL HTTP client.
type Client struct {
	endpoint string
	http     *http.Client
}

// New creates a new GraphQL client with the specified endpoint.
//
// If hc is nil, http.DefaultClient is used.
func New(endpoint string, hc *http.Client) *Client {
	if hc == nil {
		hc = http.DefaultClient
	}
	return &Client{
		endpoint: endpoint,
		http:     hc,
	}
}

// Operation describes a GraphQL operation.
//
// An operation is a query with variables.
type Operation struct {
	query   string
	vars    map[string]interface{}
	uploads map[string]Upload
}

// NewOperation creates a new GraphQL operation.
func NewOperation(query string) *Operation {
	return &Operation{query: query}
}

// Var defines a new variable.
//
// If the variable is already defined, Var panics.
func (op *Operation) Var(k string, v interface{}) {
	if op.vars == nil {
		op.vars = make(map[string]interface{})
		op.uploads = make(map[string]Upload)
	}
	if _, ok := op.vars[k]; ok {
		panic(fmt.Sprintf("gqlclient: called Operation.Var twice on %q", k))
	}
	op.vars[k] = v

	// TODO: support more deeply nested uploads
	switch v := v.(type) {
	case Upload:
		op.uploads[k] = v
	case *Upload:
		if v != nil {
			op.uploads[k] = *v
		}
	case []Upload:
		for i, upload := range v {
			upload := upload // copy
			op.uploads[fmt.Sprintf("%v.%v", k, i)] = upload
		}
	case []*Upload:
		for i, upload := range v {
			if upload != nil {
				op.uploads[fmt.Sprintf("%v.%v", k, i)] = *upload
			}
		}
	}
}

// Execute sends the operation to the GraphQL server.
//
// The data returned by the server will be decoded into the data argument.
func (c *Client) Execute(ctx context.Context, op *Operation, data interface{}) error {
	reqData := struct {
		Query string                 `json:"query"`
		Vars  map[string]interface{} `json:"variables"`
	}{
		Query: op.query,
		Vars:  op.vars,
	}

	var reqBuf bytes.Buffer
	if err := json.NewEncoder(&reqBuf).Encode(&reqData); err != nil {
		return fmt.Errorf("failed to encode request payload: %v", err)
	}

	var reqBody io.Reader
	var contentType string
	if len(op.uploads) > 0 {
		pr, pw := io.Pipe()
		defer pr.Close()

		reqBody = pr
		contentType = writeMultipart(pw, op.uploads, &reqBuf)
	} else {
		reqBody = &reqBuf
		contentType = "application/json; charset=utf-8"
	}

	// io.TeeReader(reqBody, os.Stderr)
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.endpoint, reqBody)
	if err != nil {
		return fmt.Errorf("failed to create HTTP request: %v", err)
	}
	req.Header.Set("Content-Type", contentType)
	req.Header.Set("Accept", "application/json")

	resp, err := c.http.Do(req)
	if err != nil {
		return fmt.Errorf("HTTP request failed: %v", err)
	}
	defer resp.Body.Close()

	contentType = resp.Header.Get("Content-Type")
	if contentType == "" {
		contentType = "text/plain"
	}
	mediaType, _, err := mime.ParseMediaType(contentType)
	if err != nil {
		return fmt.Errorf("invalid Content-Type %q: %v", contentType, err)
	} else if mediaType != "application/json" {
		if resp.StatusCode/100 != 2 {
			return fmt.Errorf("HTTP server error: %v %v", resp.StatusCode, resp.Status)
		}
		return fmt.Errorf("invalid Content-Type %q: expected application/json", contentType)
	}

	respData := struct {
		Data   interface{}
		Errors []Error
	}{Data: data}
	// io.TeeReader(resp.Body, os.Stderr)
	if err := json.NewDecoder(resp.Body).Decode(&respData); err != nil {
		return fmt.Errorf("failed to decode response payload: %v", err)
	}

	if len(respData.Errors) > 0 {
		return &respData.Errors[0]
	}
	return nil
}
