package zendesk

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

const (
	zendeskAPIPrefix  = "/api/v2"
	httpClientTimeout = 10 * time.Second
)

type Client struct {
	baseURL    *url.URL
	httpClient *http.Client
	credential Credential
}

type Error struct {
	body []byte
	resp *http.Response
}

func (e Error) Status() int {
	return e.resp.StatusCode
}

// Error the error string for this error
func (e Error) Error() string {
	msg := string(e.body)
	if msg == "" {
		msg = http.StatusText(e.Status())
	}

	return fmt.Sprintf("HTTP %d: %s", e.resp.StatusCode, msg)
}

func NewClient(zendeskDomain, email, apiToken string) (*Client, error) {
	httpClient := &http.Client{Timeout: httpClientTimeout}
	client := &Client{
		httpClient: httpClient,
	}
	cred := newAPITokenCredential(email, apiToken)
	client.setCredential(cred)
	err := client.setbaseURL(zendeskDomain)
	if err != nil {
		return client, err
	}
	return client, nil
}

func (c *Client) setCredential(cred Credential) {
	c.credential = cred
}

func (c *Client) setbaseURL(zendeskDomain string) error {
	apiPath := fmt.Sprintf("https://%s%s", zendeskDomain, zendeskAPIPrefix)
	baseURL, err := url.Parse(apiPath)
	if err != nil {
		return err
	}
	c.baseURL = baseURL
	return nil
}

func (c *Client) prepareRequest(req *http.Request) *http.Request {
	req.Header.Set("Content-Type", "application/json")
	req.SetBasicAuth(c.credential.Email(), c.credential.Secret())
	return req
}

func (c *Client) post(path string, payload interface{}) ([]byte, error) {
	bytes, err := json.Marshal(payload)
	if err != nil {
		return nil, err
	}

	req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", c.baseURL.String(), path), strings.NewReader(string(bytes)))
	if err != nil {
		return nil, err
	}

	req = c.prepareRequest(req)
	resp, err := c.httpClient.Do(req)
	if err != nil {
		return nil, err
	}

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

	err = resp.Body.Close()
	if err != nil {
		return nil, err
	}

	if resp.StatusCode == http.StatusCreated || resp.StatusCode == http.StatusOK {
		return body, nil
	}
	return nil, Error{
		body: body,
		resp: resp,
	}
}

type Credential interface {
	Email() string
	Secret() string
}

// APITokenCredential is type of credential for API token authentication
type APITokenCredential struct {
	email    string
	apiToken string
}

// newAPITokenCredential creates APITokenCredential and returns its pointer
func newAPITokenCredential(email string, apiToken string) *APITokenCredential {
	return &APITokenCredential{
		email:    email,
		apiToken: apiToken,
	}
}

// Email is accessor which returns email address
func (c APITokenCredential) Email() string {
	return fmt.Sprintf("%s%s", c.email, "/token")
}

// Secret is accessor which returns API token
func (c APITokenCredential) Secret() string {
	return c.apiToken
}
