package bindings

import (
	"context"
	"fmt"
	"io"
	"net"
	"net/http"
	"net/url"
	"path/filepath"
	"strings"

	"github.com/containers/libpod/pkg/api/handlers"
	jsoniter "github.com/json-iterator/go"
	"github.com/pkg/errors"
)

var (
	defaultConnectionPath string = filepath.Join(fmt.Sprintf("v%s", handlers.MinimalApiVersion), "libpod")
)

type APIResponse struct {
	*http.Response
	Request *http.Request
}

type Connection struct {
	scheme  string
	address string
	client  *http.Client
}

// NewConnection takes a URI as a string and returns a context with the
// Connection embedded as a value.  This context needs to be passed to each
// endpoint to work correctly.
//
// A valid URI connection should be scheme://
// For example tcp://localhost:<port>
// or unix://run/podman/podman.sock
func NewConnection(uri string) (context.Context, error) {
	u, err := url.Parse(uri)
	if err != nil {
		return nil, err
	}
	// TODO once ssh is implemented, remove this block and
	// add it to the conditional beneath it
	if u.Scheme == "ssh" {
		return nil, ErrNotImplemented
	}
	if u.Scheme != "tcp" && u.Scheme != "unix" {
		return nil, errors.Errorf("%s is not a support schema", u.Scheme)
	}

	if u.Scheme == "tcp" && !strings.HasPrefix(uri, "tcp://") {
		return nil, errors.New("tcp URIs should begin with tcp://")
	}

	address := u.Path
	if u.Scheme == "tcp" {
		address = u.Host
	}
	newConn := newConnection(u.Scheme, address)
	ctx := context.WithValue(context.Background(), "conn", &newConn)
	if err := pingNewConnection(ctx); err != nil {
		return nil, err
	}
	return ctx, nil
}

// pingNewConnection pings to make sure the RESTFUL service is up
// and running. it should only be used where initializing a connection
func pingNewConnection(ctx context.Context) error {
	conn, err := GetConnectionFromContext(ctx)
	if err != nil {
		return err
	}
	// the ping endpoint sits at / in this case
	response, err := conn.DoRequest(nil, http.MethodGet, "../../../_ping", nil)
	if err != nil {
		return err
	}
	if response.StatusCode == http.StatusOK {
		return nil
	}
	return errors.Errorf("ping response was %q", response.StatusCode)
}

// newConnection takes a scheme and address and creates a connection from it
func newConnection(scheme, address string) Connection {
	client := http.Client{
		Transport: &http.Transport{
			DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
				return net.Dial(scheme, address)
			},
		},
	}
	newConn := Connection{
		client:  &client,
		address: address,
		scheme:  scheme,
	}
	return newConn
}

func (c *Connection) makeEndpoint(u string) string {
	// The d character in the url is discarded and is meaningless
	return fmt.Sprintf("http://d/%s%s", defaultConnectionPath, u)
}

// DoRequest assembles the http request and returns the response
func (c *Connection) DoRequest(httpBody io.Reader, httpMethod, endpoint string, queryParams map[string]string, pathValues ...string) (*APIResponse, error) {
	var (
		err      error
		response *http.Response
	)
	safePathValues := make([]interface{}, len(pathValues))
	// Make sure path values are http url safe
	for i, pv := range pathValues {
		safePathValues[i] = url.PathEscape(pv)
	}
	// Lets eventually use URL for this which might lead to safer
	// usage
	safeEndpoint := fmt.Sprintf(endpoint, safePathValues...)
	e := c.makeEndpoint(safeEndpoint)
	req, err := http.NewRequest(httpMethod, e, httpBody)
	if err != nil {
		return nil, err
	}
	if len(queryParams) > 0 {
		// if more desirable we could use url to form the encoded endpoint with params
		r := req.URL.Query()
		for k, v := range queryParams {
			r.Add(k, url.QueryEscape(v))
		}
		req.URL.RawQuery = r.Encode()
	}
	// Give the Do three chances in the case of a comm/service hiccup
	for i := 0; i < 3; i++ {
		response, err = c.client.Do(req) // nolint
		if err == nil {
			break
		}
	}
	return &APIResponse{response, req}, err
}

// GetConnectionFromContext returns a bindings connection from the context
// being passed into each method.
func GetConnectionFromContext(ctx context.Context) (*Connection, error) {
	c := ctx.Value("conn")
	if c == nil {
		return nil, errors.New("unable to get connection from context")
	}
	conn := c.(*Connection)
	return conn, nil
}

// FiltersToHTML converts our typical filter format of a
// map[string][]string to a query/html safe string.
func FiltersToHTML(filters map[string][]string) (string, error) {
	lowerCaseKeys := make(map[string][]string)
	for k, v := range filters {
		lowerCaseKeys[strings.ToLower(k)] = v
	}
	unsafeString, err := jsoniter.MarshalToString(lowerCaseKeys)
	if err != nil {
		return "", err
	}
	return url.QueryEscape(unsafeString), nil
}

// IsInformation returns true if the response code is 1xx
func (h *APIResponse) IsInformational() bool {
	return h.Response.StatusCode/100 == 1
}

// IsSuccess returns true if the response code is 2xx
func (h *APIResponse) IsSuccess() bool {
	return h.Response.StatusCode/100 == 2
}

// IsRedirection returns true if the response code is 3xx
func (h *APIResponse) IsRedirection() bool {
	return h.Response.StatusCode/100 == 3
}

// IsClientError returns true if the response code is 4xx
func (h *APIResponse) IsClientError() bool {
	return h.Response.StatusCode/100 == 4
}

// IsServerError returns true if the response code is 5xx
func (h *APIResponse) IsServerError() bool {
	return h.Response.StatusCode/100 == 5
}
