package statuscake

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

const apiBaseURL = "https://app.statuscake.com/API"

type responseBody struct {
	io.Reader
}

func (r *responseBody) Close() error {
	return nil
}

// Auth wraps the authorisation headers required for each request
type Auth struct {
	Username string
	Apikey   string
}

func (a *Auth) validate() error {
	e := make(ValidationError)

	if a.Username == "" {
		e["Username"] = "is required"
	}

	if a.Apikey == "" {
		e["Apikey"] = "is required"
	}

	if len(e) > 0 {
		return e
	}

	return nil
}

type httpClient interface {
	Do(*http.Request) (*http.Response, error)
}

type apiClient interface {
	get(string, url.Values) (*http.Response, error)
	delete(string, url.Values) (*http.Response, error)
	put(string, url.Values) (*http.Response, error)
}

// Client is the http client that wraps the remote API.
type Client struct {
	c           httpClient
	username    string
	apiKey      string
	testsClient Tests
}

// New returns a new Client
func New(auth Auth) (*Client, error) {
	if err := auth.validate(); err != nil {
		return nil, err
	}

	return &Client{
		c:        &http.Client{},
		username: auth.Username,
		apiKey:   auth.Apikey,
	}, nil
}

func (c *Client) newRequest(method string, path string, v url.Values, body io.Reader) (*http.Request, error) {
	url := fmt.Sprintf("%s%s", apiBaseURL, path)
	if v != nil {
		url = fmt.Sprintf("%s?%s", url, v.Encode())
	}

	r, err := http.NewRequest(method, url, body)
	if err != nil {
		return nil, err
	}

	r.Header.Set("Username", c.username)
	r.Header.Set("API", c.apiKey)

	return r, nil
}

func (c *Client) doRequest(r *http.Request) (*http.Response, error) {
	resp, err := c.c.Do(r)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	if resp.StatusCode < 200 || resp.StatusCode > 299 {
		return nil, &httpError{
			status:     resp.Status,
			statusCode: resp.StatusCode,
		}
	}

	var aer autheticationErrorResponse

	// We read and save the response body so that if we don't have error messages
	// we can set it again for future usage
	b, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}

	err = json.Unmarshal(b, &aer)
	if err == nil && aer.ErrNo == 0 && aer.Error != "" {
		return nil, &AuthenticationError{
			errNo:   aer.ErrNo,
			message: aer.Error,
		}
	}

	resp.Body = &responseBody{
		Reader: bytes.NewReader(b),
	}

	return resp, nil
}

func (c *Client) get(path string, v url.Values) (*http.Response, error) {
	r, err := c.newRequest("GET", path, v, nil)
	if err != nil {
		return nil, err
	}

	return c.doRequest(r)
}

func (c *Client) put(path string, v url.Values) (*http.Response, error) {
	r, err := c.newRequest("PUT", path, nil, strings.NewReader(v.Encode()))
	r.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	if err != nil {
		return nil, err
	}

	return c.doRequest(r)
}

func (c *Client) delete(path string, v url.Values) (*http.Response, error) {
	r, err := c.newRequest("DELETE", path, v, nil)
	if err != nil {
		return nil, err
	}

	return c.doRequest(r)
}

// Tests returns a client that implements the `Tests` API.
func (c *Client) Tests() Tests {
	if c.testsClient == nil {
		c.testsClient = newTests(c)
	}

	return c.testsClient
}
