package pocket

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

// Client pocket client Documention at https://getpocket.com/developer/docs/overview
type Client struct {
	consumerKey string
	accessToken *string
	baseURL     string
	redirectURL string
}

//NewClient returns a new PocketClient
func NewClient(consumerKey, redirectURL string) *Client {
	return &Client{
		consumerKey: consumerKey,
		redirectURL: redirectURL,
		baseURL:     "https://getpocket.com/v3",
	}

}

//Item represents link in pocket api
type Item struct {
	ItemID                 string `json:"item_id"`
	ResolvedID             string `json:"resolved_id"`
	GivenURL               string `json:"given_url"`
	GivenTitle             string `json:"given_title"`
	Favorite               string `json:"favorite"`
	Status                 string `json:"status"`
	TimeAdded              string `json:"time_added"`
	TimeUpdated            string `json:"time_updated"`
	TimeRead               string `json:"time_read"`
	TimeFavorited          string `json:"time_favorited"`
	SortID                 int    `json:"sort_id"`
	ResolvedTitle          string `json:"resolved_title"`
	ResolvedURL            string `json:"resolved_url"`
	Excerpt                string `json:"excerpt"`
	IsArticle              string `json:"is_article"`
	IsIndex                string `json:"is_index"`
	HasVideo               string `json:"has_video"`
	HasImage               string `json:"has_image"`
	WordCount              string `json:"word_count"`
	Lang                   string `json:"lang"`
	TimeToRead             int    `json:"time_to_read"`
	TopImageURL            string `json:"top_image_url"`
	ListenDurationEstimate int    `json:"listen_duration_estimate"`
}

//ItemLists represent list of links
type ItemLists struct {
	Status   int             `json:"status"`
	Complete int             `json:"complete"`
	List     map[string]Item `json:"list"`
	Since    int             `json:"since"`
}

type request struct {
	requestBody interface{}
	method      string
	headers     map[string]string
	url         string
}

func (client *Client) request(req request, result interface{}) error {
	jsonValues, err := json.Marshal(req.requestBody)
	if err != nil {
		return err
	}
	request, err := http.NewRequest(req.method, req.url, bytes.NewBuffer(jsonValues))
	if err != nil {
		return err
	}

	for key, value := range req.headers {
		request.Header.Add(key, value)
	}

	resp, err := http.DefaultClient.Do(request)

	if err != nil {
		return err
	}
	defer func() { _ = resp.Body.Close() }()

	responseBody, err := ioutil.ReadAll(resp.Body)

	if err != nil {
		return err
	}
	if resp.StatusCode >= 400 {
		return fmt.Errorf(`server responded with [%d]:%s,url:%s`, resp.StatusCode, responseBody, req.url)
	}

	if err := json.Unmarshal(responseBody, &result); err != nil {
		return fmt.Errorf("could not unmarshal url [%s] \n\t\tresponse [%s] request[%s] error:%w",
			req.url, responseBody, jsonValues, err)
	}

	return nil

}

type obtainRequestTokenRequest struct {
	ConsumerKey string `json:"consumer_key"`
	RedirectURI string `json:"redirect_uri"`
}

//ObtainRequestToken get request token to be used in the auth workflow
func (client *Client) ObtainRequestToken() (code string, err error) {
	url := fmt.Sprintf("%s/oauth/request", client.baseURL)
	requestData := obtainRequestTokenRequest{ConsumerKey: client.consumerKey, RedirectURI: client.redirectURL}

	var responseData map[string]string
	req := request{
		headers: map[string]string{
			"X-Accept":     "application/json",
			"Content-Type": "application/json",
		},
		method:      "POST",
		requestBody: requestData,
		url:         url,
	}
	err = client.request(req, &responseData)

	if err != nil {
		return code, err
	}

	return responseData["code"], nil

}

//CreateAuthLink create authorization link to redirect the user to
func (client *Client) CreateAuthLink(requestToken string) string {
	return fmt.Sprintf("https://getpocket.com/auth/authorize?request_token=%s&redirect_uri=%s", requestToken, client.redirectURL)
}

type accessTokenRequest struct {
	ConsumerKey string `json:"consumer_key"`
	RequestCode string `json:"code"`
}

// accessTokenResponse represents
type accessTokenResponse struct {
	AccessToken string `json:"access_token"`
}

//GetAccessToken exchange request token for accesstoken
func (client *Client) GetAccessToken(requestToken string) (accessToken string, err error) {
	url := fmt.Sprintf("%s/oauth/authorize", client.baseURL)
	requestData := accessTokenRequest{
		ConsumerKey: client.consumerKey,
		RequestCode: requestToken,
	}
	req := request{
		method:      "POST",
		url:         url,
		requestBody: requestData,
	}
	req.headers = map[string]string{
		"X-Accept":     "application/json",
		"Content-Type": "application/json",
	}

	var response accessTokenResponse
	err = client.request(req, &response)
	if err != nil {
		return "", err
	}
	return response.AccessToken, nil

}

/*LinkState  represents link states to be retrieved
According to the api https://getpocket.com/developer/docs/v3/retrieve
there are 3 states:
	1-archive
	2-unread
	3-all
however archive does not really well work and returns links that are in the
unread list
buy inspecting getpocket I found out that there is an undocumanted read state
*/
type LinkState string

const (
	//Read links that has been read (undocumanted)
	Read LinkState = "read"
	//Unread links has not been read
	Unread LinkState = "unread"
)

// GetLinks retrieve links of a given states https://getpocket.com/developer/docs/v3/retrieve
func (client *Client) GetLinks(state LinkState) (response ItemLists, err error) {
	url := fmt.Sprintf("%s/get?sort=newest&state=%s&consumer_key=%s&access_token=%s", client.baseURL, state, client.consumerKey, *client.accessToken)
	req := request{
		method: "GET",
		url:    url,
	}
	req.headers = map[string]string{
		"X-Accept":     "application/json",
		"Content-Type": "application/json",
	}

	err = client.request(req, &response)
	return response, err
}

//Action represents a mutation to link
type Action string

const (
	//Archive to put the link in the archived list (read list)
	Archive Action = "archive"
	//ReAdd to put the link back in the to reed list
	ReAdd Action = "readd"
)

type actionParams struct {
	Action Action `json:"action"`
	ItemID string `json:"item_id"`
}

//ModifyLink change the state of the link
func (client *Client) ModifyLink(action Action, itemID string) (ok bool, err error) {

	actions := []actionParams{
		{
			Action: action,
			ItemID: itemID,
		},
	}

	urlActionParm, err := json.Marshal(actions)
	if err != nil {
		return false, err
	}
	url := fmt.Sprintf("%s/send?consumer_key=%s&access_token=%s&actions=%s", client.baseURL, client.consumerKey, *client.accessToken, urlActionParm)

	req := request{
		method: "GET",
		url:    url,
	}

	err = client.request(req, nil)

	if err != nil {
		return false, err
	}

	return true, nil

}
