package udnssdk

// udnssdk - a golang sdk for the ultradns REST service.
// 2015-07-03 - jmasseo@gmail.com

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

	"golang.org/x/oauth2"

	oauthPassword "github.com/terra-farm/udnssdk/password"
)

const (
	libraryVersion = "0.1"
	// DefaultTestBaseURL returns the URL for UltraDNS's test restapi endpoint
	DefaultTestBaseURL = "https://test-restapi.ultradns.com/"
	// DefaultLiveBaseURL returns the URL for UltraDNS's production restapi endpoint
	DefaultLiveBaseURL = "https://restapi.ultradns.com/"

	userAgent = "udnssdk-go/" + libraryVersion

	apiVersion = "v1"
)

// QueryInfo wraps a query request
type QueryInfo struct {
	Q       string `json:"q"`
	Sort    string `json:"sort"`
	Reverse bool   `json:"reverse"`
	Limit   int    `json:"limit"`
}

// ResultInfo wraps the list metadata for an index response
type ResultInfo struct {
	TotalCount    int `json:"totalCount"`
	Offset        int `json:"offset"`
	ReturnedCount int `json:"returnedCount"`
}

// Client wraps our general-purpose Service Client
type Client struct {
	// This is our client structure.
	HTTPClient *http.Client
	Config     *oauthPassword.Config

	BaseURL   *url.URL
	UserAgent string

	// Accounts API
	Accounts *AccountsService
	// Probe Alerts API
	Alerts *AlertsService
	// Directional Pools API
	DirectionalPools *DirectionalPoolsService
	// Events API
	Events *EventsService
	// Notifications API
	Notifications *NotificationsService
	// Probes API
	Probes *ProbesService
	// Resource Record Sets API
	RRSets *RRSetsService
	// Tasks API
	Tasks *TasksService
}

// NewClient returns a new ultradns API client.
func NewClient(username, password, baseURL string) (*Client, error) {
	ctx := oauth2.NoContext
	conf := NewConfig(username, password, baseURL)

	u, err := url.Parse(baseURL)
	if err != nil {
		return nil, err
	}

	c := &Client{
		HTTPClient: conf.Client(ctx),
		BaseURL:    u,
		UserAgent:  userAgent,
		Config:     conf,
	}
	c.Accounts = &AccountsService{client: c}
	c.Alerts = &AlertsService{client: c}
	c.DirectionalPools = &DirectionalPoolsService{client: c}
	c.Events = &EventsService{client: c}
	c.Notifications = &NotificationsService{client: c}
	c.Probes = &ProbesService{client: c}
	c.RRSets = &RRSetsService{client: c}
	c.Tasks = &TasksService{client: c}
	return c, nil
}

// newStubClient returns a new ultradns API client.
func newStubClient(username, password, baseURL, clientID, clientSecret string) (*Client, error) {
	u, err := url.Parse(baseURL)
	if err != nil {
		return nil, err
	}

	c := &Client{
		HTTPClient: &http.Client{},
		BaseURL:    u,
		UserAgent:  userAgent,
	}
	c.Accounts = &AccountsService{client: c}
	c.Alerts = &AlertsService{client: c}
	c.DirectionalPools = &DirectionalPoolsService{client: c}
	c.Events = &EventsService{client: c}
	c.Notifications = &NotificationsService{client: c}
	c.Probes = &ProbesService{client: c}
	c.RRSets = &RRSetsService{client: c}
	c.Tasks = &TasksService{client: c}
	return c, nil
}

// NewRequest creates an API request.
// The path is expected to be a relative path and will be resolved
// according to the BaseURL of the Client. Paths should always be specified without a preceding slash.
func (c *Client) NewRequest(method, pathquery string, payload interface{}) (*http.Request, error) {
	url := *c.BaseURL

	pq := strings.SplitN(pathquery, "?", 2)
	url.Path = url.Path + fmt.Sprintf("%s/%s", apiVersion, pq[0])
	if len(pq) == 2 {
		url.RawQuery = pq[1]
	}

	body := new(bytes.Buffer)
	if payload != nil {
		err := json.NewEncoder(body).Encode(payload)
		if err != nil {
			return nil, err
		}
	}

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

	req.Header.Set("Content-Type", "application/json")
	req.Header.Add("Accept", "application/json")
	req.Header.Add("User-Agent", c.UserAgent)

	return req, nil
}

func (c *Client) get(path string, v interface{}) (*http.Response, error) {
	return c.Do("GET", path, nil, v)
}

func (c *Client) post(path string, payload, v interface{}) (*http.Response, error) {
	return c.Do("POST", path, payload, v)
}

func (c *Client) put(path string, payload, v interface{}) (*http.Response, error) {
	return c.Do("PUT", path, payload, v)
}

func (c *Client) delete(path string, payload interface{}) (*http.Response, error) {
	return c.Do("DELETE", path, payload, nil)
}

// Do sends an API request and returns the API response.
// The API response is JSON decoded and stored in the value pointed by v,
// or returned as an error if an API error has occurred.
// If v implements the io.Writer interface, the raw response body will be written to v,
// without attempting to decode it.
func (c *Client) Do(method, path string, payload, v interface{}) (*http.Response, error) {
	hc := c.HTTPClient
	req, err := c.NewRequest(method, path, payload)
	if err != nil {
		return nil, err
	}
	log.Printf("[DEBUG] HTTP Request: %+v\n", req)
	r, err := hc.Do(req)
	log.Printf("[DEBUG] HTTP Response: %+v\n", r)
	if err != nil {
		return nil, err
	}
	defer r.Body.Close()

	if r.StatusCode == 202 {
		// This is a deferred task.
		tid := TaskID(r.Header.Get("X-Task-Id"))
		log.Printf("[DEBUG] Received Async Task %+v..  will retry...\n", tid)
		// TODO: Sane Configuration for timeouts / retries
		timeout := 5
		waittime := 5 * time.Second
		i := 0
		breakmeout := false
		for i < timeout || breakmeout {
			t, _, err := c.Tasks.Find(tid)
			if err != nil {
				return nil, err
			}
			log.Printf("[DEBUG] Task ID: %+v Retry: %d Status Code: %s\n", tid, i, t.TaskStatusCode)
			switch t.TaskStatusCode {
			case "COMPLETE":
				// Yay
				resp, err := c.Tasks.FindResultByTask(t)
				if err != nil {
					return nil, err
				}
				r = resp
				breakmeout = true
			case "PENDING", "IN_PROCESS":
				i = i + 1
				time.Sleep(waittime)
				continue
			case "ERROR":
				return nil, err
			}
		}
	}

	err = CheckResponse(r)
	if err != nil {
		return r, err
	}

	if v != nil {
		if w, ok := v.(io.Writer); ok {
			io.Copy(w, r.Body)
		} else {
			err = json.NewDecoder(r.Body).Decode(v)
			// err = json.Unmarshal(r.Body, v)
		}
	}

	return r, err
}

// ErrorResponse represents an error caused by an API request.
// Example:
// {"errorCode":60001,"errorMessage":"invalid_grant:Invalid username & password combination.","error":"invalid_grant","error_description":"60001: invalid_grant:Invalid username & password combination."}
type ErrorResponse struct {
	Response         *http.Response // HTTP response that caused this error
	ErrorCode        int            `json:"errorCode"`    //  error code
	ErrorMessage     string         `json:"errorMessage"` // human-readable message
	ErrorStr         string         `json:"error"`
	ErrorDescription string         `json:"error_description"`
}

// ErrorResponseList wraps an HTTP response that has a list of errors
type ErrorResponseList struct {
	Response  *http.Response // HTTP response that caused this error
	Responses []ErrorResponse
}

// Error implements the error interface.
func (r ErrorResponse) Error() string {
	return fmt.Sprintf("%v %v: %d %d %v",
		r.Response.Request.Method, r.Response.Request.URL,
		r.Response.StatusCode, r.ErrorCode, r.ErrorMessage)
}

func (r ErrorResponseList) Error() string {
	return fmt.Sprintf("%v %v: %d %d %v",
		r.Response.Request.Method, r.Response.Request.URL,
		r.Response.StatusCode, r.Responses[0].ErrorCode, r.Responses[0].ErrorMessage)
}

// CheckResponse checks the API response for errors, and returns them if present.
// A response is considered an error if the status code is different than 2xx. Specific requests
// may have additional requirements, but this is sufficient in most of the cases.
func CheckResponse(r *http.Response) error {
	if code := r.StatusCode; 200 <= code && code <= 299 {
		return nil
	}

	body, err := ioutil.ReadAll(r.Body)
	if err != nil {
		return err
	}

	// Attempt marshaling to ErrorResponse
	var er ErrorResponse
	err = json.Unmarshal(body, &er)
	if err == nil {
		er.Response = r
		return er
	}

	// Attempt marshaling to ErrorResponseList
	var ers []ErrorResponse
	err = json.Unmarshal(body, &ers)
	if err == nil {
		return &ErrorResponseList{Response: r, Responses: ers}
	}

	return fmt.Errorf("Response had non-successful Status: %#v, but could not extract any errors from Body: %#v", r.Status, string(body))
}
