package client

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"strings"
	"time"

	commonAPI "code.secondbit.org/api.hg"
	"code.secondbit.org/uuid.hg"

	"code.secondbit.org/ducky/subscriptions.hg/api"
)

var (
	// ErrNilClient is returned when a method is called on a nil Client.
	ErrNilClient = errors.New("nil client wrapper")
	// ErrNilHTTPClient is returned when a method is called on a Client
	// with its http.Client not set.
	ErrNilHTTPClient = errors.New("nil client")
)

// Client is a wrapper that bundles all the information necessary to interact
// with the subscriptions API.
type Client struct {
	client  *http.Client
	address string
	ID      uuid.ID
}

// New returns a new Client. The passed address is the base address for the
// subscriptions API, as an absolute or relative URL. The passed ID is the
// OAuth2 client ID to use for the Client.
func New(address string, id uuid.ID) *Client {
	address = strings.TrimRight(address, "/")
	return &Client{
		address: address,
		client:  &http.Client{},
		ID:      id,
	}
}

func (c *Client) do(method, url string, request interface{}, scopes []string, subject uuid.ID) (api.Response, error) {
	if c == nil {
		return api.Response{}, ErrNilClient
	}
	if c.client == nil {
		return api.Response{}, ErrNilHTTPClient
	}
	var response api.Response
	if !strings.HasPrefix(url, "http") {
		url = strings.TrimLeft(url, "/")
		url = "/" + url
		url = c.address + url
	}
	var body io.Reader
	if request != nil {
		data, err := json.Marshal(request)
		if err != nil {
			return response, err
		}
		body = bytes.NewBuffer(data)
	}
	req, err := http.NewRequest(method, url, body)
	if err != nil {
		return response, err
	}
	req.Header.Set("Ducky-Scope", strings.Join(scopes, " "))
	req.Header.Set("Ducky-Issuer", c.ID.String())
	if subject != nil {
		req.Header.Set("Ducky-Subject", subject.String())
	}
	req.Header.Set("Ducky-Expires", time.Now().Add(time.Hour).String())
	req.Header.Set("Ducky-Issued-At", time.Now().String())
	req.Header.Set("Ducky-Not-Before", time.Now().Add(-5*time.Minute).String())
	resp, err := c.client.Do(req)
	if err != nil {
		return response, err
	}
	defer resp.Body.Close()
	switch resp.Header.Get("Content-Type") {
	case "application/json":
		dec := json.NewDecoder(resp.Body)
		err = dec.Decode(&response)
		if err != nil {
			return response, err
		}
	default:
		dec := json.NewDecoder(resp.Body)
		err = dec.Decode(&response)
		if err != nil {
			return response, err
		}
	}
	if len(response.Errors) > 0 {
		return response, httpErrors(response.Errors)
	}
	return response, nil
}

type httpErrors []commonAPI.RequestError

func (h httpErrors) Error() string {
	return fmt.Sprintf("%+#v", h)
}

// Get returns the api Response object for the passed URL called
// over HTTP GET. The passed scope IDs will be applied to the request,
// and the request will be made on behalf of the subject (user)
// specified by the passed ID. If the zero value is passed for the ID,
// the request will be made without a subject. To request no scopes,
// pass an empty slice or nil.
func (c *Client) Get(url string, scopes []string, subject uuid.ID) (api.Response, error) {
	return c.do("GET", url, nil, scopes, subject)
}

// Post returns the api Response object for the passed URL called
// over HTTP POST. The passed scope IDs will be applied to the request,
// and the request will be made on behalf of the subject (user)
// specified by the passed ID. If the zero value is passed for the ID,
// the request will be made without a subject. To request no scopes,
// pass an empty slice or nil.
func (c *Client) Post(url string, request interface{}, scopes []string, subject uuid.ID) (api.Response, error) {
	return c.do("POST", url, request, scopes, subject)
}

// Patch returns the api Response object for the passed URL called
// over HTTP PATCH. The passed scope IDs will be applied to the request,
// and the request will be made on behalf of the subject (user)
// specified by the passed ID. If the zero value is passed for the ID,
// the request will be made without a subject. To request no scopes,
// pass an empty slice or nil.
func (c *Client) Patch(url string, request interface{}, scopes []string, subject uuid.ID) (api.Response, error) {
	return c.do("PATCH", url, request, scopes, subject)
}
