package grafana

import (
	"bytes"
	"fmt"
	"io/ioutil"
	"net/http"
	"strings"
)

// Client implements a Grafana API client, and contains the instance's base URL
// and API key, along with an HTTP client used to request the API.
type Client struct {
	BaseURL    string
	APIKey     string
	httpClient *http.Client
}

// NewClient returns a new Grafana API client from a given base URL and API key.
func NewClient(baseURL string, apiKey string) (c *Client) {
	// Grafana doesn't support double slashes in the API routes, so we strip the
	// last slash if there's one, because request() will append one anyway.
	if strings.HasSuffix(baseURL, "/") {
		baseURL = baseURL[:len(baseURL)-1]
	}

	return &Client{
		BaseURL:    baseURL,
		APIKey:     apiKey,
		httpClient: new(http.Client),
	}
}

// request preforms an HTTP request on a given endpoint, with a given method and
// body. The endpoint is the Grafana API route to request, without the "/api/"
// part. If the request doesn't require a body, the function has to be called
// with "nil" as the "body" parameter.
// Returns the response body (as a []byte containing JSON data).
// Returns an error if there was an issue initialising the request, performing
// it or reading the response body. Also returns an error on non-200 response
// status codes. If the status code is 404, a standard error is returned, if the
// status code is neither 200 nor 404 an error of type httpUnkownError is
// returned.
func (c *Client) request(method string, endpoint string, body []byte) ([]byte, error) {
	url := c.BaseURL + "/api/" + endpoint

	// Create the request
	req, err := http.NewRequest(method, url, bytes.NewBuffer(body))
	if err != nil {
		return nil, err
	}

	// Add the API key to the request as an Authorization HTTP header
	authHeader := fmt.Sprintf("Bearer %s", c.APIKey)
	req.Header.Add("Authorization", authHeader)

	// If the request isn't a GET, the body will be sent as JSON, so we need to
	// append the appropriate header
	if method != "GET" {
		req.Header.Add("Content-Type", "application/json")
	}

	// Perform the request
	resp, err := c.httpClient.Do(req)
	if err != nil {
		return nil, err
	}

	// Read the response body
	respBody, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}

	// Return an error if the Grafana API responded with a non-200 status code.
	// We perform this here because http.Client.Do() doesn't return with an
	// error on non-200 status codes.
	if resp.StatusCode != http.StatusOK {
		if resp.StatusCode == http.StatusNotFound {
			err = fmt.Errorf("%s not found (404)", url)
		} else {
			// Return an httpUnkownError error if the status code is neither 200
			// nor 404
			err = newHttpUnknownError(resp.StatusCode)
		}
	}

	// Return the response body along with the error. This allows callers to
	// process httpUnkownError errors by displaying an error message located in
	// the response body along with the data contained in the error.
	return respBody, err
}

// httpUnkownError represents an HTTP error, created from an HTTP response where
// the status code is neither 200 nor 404.
type httpUnkownError struct {
	StatusCode int
}

// newHttpUnknownError creates and returns a new httpUnkownError error using
// the provided status code.
func newHttpUnknownError(statusCode int) *httpUnkownError {
	return &httpUnkownError{
		StatusCode: statusCode,
	}
}

// Error implements error.Error().
func (e *httpUnkownError) Error() string {
	return fmt.Sprintf("Unknown HTTP error: %d", e.StatusCode)
}
