package client

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

	"github.com/kolo/xmlrpc"
)

const (
	// Production is the SystemType to provide to New to use the production XML API
	Production SystemType = iota
	// Testing is the SystemType to provide to New to use the test XML API
	Testing
	// LiveDNS is the SystemType to provide to New to use the Live DNS REST API
	// Full documentation of the API is available here: http://doc.livedns.gandi.net/
	LiveDNS
)

// Enable/disable debug output:
const debug = false

// SystemType is the type used to resolve gandi API address
type SystemType int

// Url returns the actual gandi API base URL
func (s SystemType) Url() string {
	if s == Production {
		return "https://rpc.gandi.net/xmlrpc/"
	}
	if s == LiveDNS {
		return "https://dns.api.gandi.net/api/v5/"
	}
	return "https://rpc.ote.gandi.net/xmlrpc/"
}

// Client holds the configuration of a gandi client
type Client struct {
	// Key is the API key to provide to gandi
	Key string
	// Url is the base URL of the gandi API
	Url string
}

// New creates a new gandi client for the given system
func New(apiKey string, system SystemType) *Client {
	return &Client{
		Key: apiKey,
		Url: system.Url(),
	}
}

// Call performs an acual XML RPC call to the gandi API
func (c *Client) Call(serviceMethod string, args []interface{}, reply interface{}) error {
	rpc, err := xmlrpc.NewClient(c.Url, nil)
	if err != nil {
		return err
	}
	return rpc.Call(serviceMethod, args, reply)
}

// DoRest performs a request to gandi LiveDNS api and optionnally decodes the reply
func (c *Client) DoRest(req *http.Request, decoded interface{}) (*http.Response, error) {
	if decoded != nil {
		req.Header.Set("Accept", "application/json")
	}
	client := http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return nil, err
	}
	if resp.StatusCode == http.StatusUnauthorized {
		return nil, fmt.Errorf("the server returned unauthorized code. Your API key might be invalid or have expired")
	}
	//
	defer func() { err = resp.Body.Close() }()
	if decoded != nil {
		b, e := ioutil.ReadAll(resp.Body)
		if e != nil {
			return nil, e
		}
		if len(b) > 0 {
			e = json.Unmarshal(b, decoded)
			if e != nil {
				return nil, e
			}
			if resp.StatusCode == http.StatusBadRequest {
				return nil, fmt.Errorf("the server returned 400 bad request (%v)", string(b))
			}
		}
		resp.Body = ioutil.NopCloser(bytes.NewReader(b))
	}
	return resp, err
}

// NewJSONRequest creates a new authenticated to gandi live DNS REST API.
// If data is not null, it will be encoded as json and prodived in the request body
func (c *Client) NewJSONRequest(method string, url string, data interface{}) (*http.Request, error) {
	var reader io.Reader
	if data != nil {
		b, err := json.Marshal(data)
		if err != nil {
			return nil, err
		}
		reader = bytes.NewReader(b)
	}
	req, err := http.NewRequest(method, fmt.Sprintf("%s/%s", strings.TrimRight(c.Url, "/"), strings.TrimLeft(url, "/")), reader)
	if err != nil {
		return nil, err
	}
	if data != nil {
		req.Header.Set("Content-Type", "application/json")
	}
	req.Header.Set("X-Api-Key", c.Key)
	return req, nil
}

// Get performs a Get request to gandi Live DNS api and decodes the returned data if a not null decoded pointer is provided
func (c *Client) Get(URI string, decoded interface{}) (*http.Response, error) {
	req, err := c.NewJSONRequest("GET", URI, nil)
	if err != nil {
		return nil, err
	}
	resp, err := c.DoRest(req, decoded)
	if err != nil {
		return nil, err
	}
	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("Unexpected http code %d on URL %v. expecting %d", resp.StatusCode, resp.Request.URL, http.StatusOK)
	}
	return resp, err
}

// Delete performs a Delete request to gandi Live DNS api and decodes the returned data if a not null decoded pointer is provided
func (c *Client) Delete(URI string, decoded interface{}) (*http.Response, error) {
	req, err := c.NewJSONRequest("DELETE", URI, nil)
	if err != nil {
		return nil, err
	}
	resp, err := c.DoRest(req, decoded)
	if err != nil {
		return nil, err
	}
	if resp.StatusCode != http.StatusNoContent {
		return nil, fmt.Errorf("Unexpected http code %d on URL %v. expecting %d", resp.StatusCode, resp.Request.URL, http.StatusNoContent)
	}
	return resp, err
}

// Post performs a Post request request to gandi Live DNS api
// - with data encoded as JSON if a not null data pointer is provided
// - decodes the returned data if a not null decoded pointer is provided
// - ensures the status code is an HTTP accepted
func (c *Client) Post(URI string, data interface{}, decoded interface{}) (*http.Response, error) {
	if debug {
		fmt.Printf("DEBUG: POST URI=%s\n", URI)
	}
	req, err := c.NewJSONRequest("POST", URI, data)
	if err != nil {
		return nil, err
	}
	if debug {
		fmt.Printf("DEBUG: POST req=%v decoded=%v\n", req, decoded)
	}
	resp, err := c.DoRest(req, decoded)
	if err != nil {
		return nil, err
	}
	if resp.StatusCode != http.StatusCreated {
		return nil, fmt.Errorf("Unexpected http code %d on URL %v. expecting %d", resp.StatusCode, resp.Request.URL, http.StatusCreated)
	}
	return resp, err
}

// Put performs a Put request to gandi Live DNS api
// - with data encoded as JSON if a not null data pointer is provided
// - decodes the returned data if a not null decoded pointer is provided
func (c *Client) Put(URI string, data interface{}, decoded interface{}) (*http.Response, error) {
	req, err := c.NewJSONRequest("PUT", URI, data)
	if err != nil {
		return nil, err
	}
	return c.DoRest(req, decoded)
}

// Patch performs a Patch request to gandi Live DNS api
// - with data encoded as JSON if a not null data pointer is provided
// - decodes the returned data if a not null decoded pointer is provided
// - ensures the status code is an HTTP accepted
func (c *Client) Patch(URI string, data interface{}, decoded interface{}) (*http.Response, error) {
	req, err := c.NewJSONRequest("PATCH", URI, data)
	if err != nil {
		return nil, err
	}
	resp, err := c.DoRest(req, decoded)
	if err != nil {
		return nil, err
	}
	if resp.StatusCode != http.StatusAccepted {
		return nil, fmt.Errorf("Unexpected http code %d on URL %v. expecting %d", resp.StatusCode, resp.Request.URL, http.StatusAccepted)
	}
	return resp, err
}
