package storage

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

const (
	headerAccept          = "Accept"
	headerEtag            = "Etag"
	headerPrefer          = "Prefer"
	headerXmsContinuation = "x-ms-Continuation-NextTableName"
)

// TableServiceClient contains operations for Microsoft Azure Table Storage
// Service.
type TableServiceClient struct {
	client Client
	auth   authentication
}

// TableOptions includes options for some table operations
type TableOptions struct {
	RequestID string
}

func (options *TableOptions) addToHeaders(h map[string]string) map[string]string {
	if options != nil {
		h = addToHeaders(h, "x-ms-client-request-id", options.RequestID)
	}
	return h
}

// QueryNextLink includes information for getting the next page of
// results in query operations
type QueryNextLink struct {
	NextLink *string
	ml       MetadataLevel
}

// GetServiceProperties gets the properties of your storage account's table service.
// See: https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/get-table-service-properties
func (t *TableServiceClient) GetServiceProperties() (*ServiceProperties, error) {
	return t.client.getServiceProperties(tableServiceName, t.auth)
}

// SetServiceProperties sets the properties of your storage account's table service.
// See: https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/set-table-service-properties
func (t *TableServiceClient) SetServiceProperties(props ServiceProperties) error {
	return t.client.setServiceProperties(props, tableServiceName, t.auth)
}

// GetTableReference returns a Table object for the specified table name.
func (t *TableServiceClient) GetTableReference(name string) *Table {
	return &Table{
		tsc:  t,
		Name: name,
	}
}

// QueryTablesOptions includes options for some table operations
type QueryTablesOptions struct {
	Top       uint
	Filter    string
	RequestID string
}

func (options *QueryTablesOptions) getParameters() (url.Values, map[string]string) {
	query := url.Values{}
	headers := map[string]string{}
	if options != nil {
		if options.Top > 0 {
			query.Add(OdataTop, strconv.FormatUint(uint64(options.Top), 10))
		}
		if options.Filter != "" {
			query.Add(OdataFilter, options.Filter)
		}
		headers = addToHeaders(headers, "x-ms-client-request-id", options.RequestID)
	}
	return query, headers
}

// QueryTables returns the tables in the storage account.
// You can use query options defined by the OData Protocol specification.
//
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/query-tables
func (t *TableServiceClient) QueryTables(ml MetadataLevel, options *QueryTablesOptions) (*TableQueryResult, error) {
	query, headers := options.getParameters()
	uri := t.client.getEndpoint(tableServiceName, tablesURIPath, query)
	return t.queryTables(uri, headers, ml)
}

// NextResults returns the next page of results
// from a QueryTables or a NextResults operation.
//
// See https://docs.microsoft.com/rest/api/storageservices/fileservices/query-tables
// See https://docs.microsoft.com/rest/api/storageservices/fileservices/query-timeout-and-pagination
func (tqr *TableQueryResult) NextResults(options *TableOptions) (*TableQueryResult, error) {
	if tqr == nil {
		return nil, errNilPreviousResult
	}
	if tqr.NextLink == nil {
		return nil, errNilNextLink
	}
	headers := options.addToHeaders(map[string]string{})

	return tqr.tsc.queryTables(*tqr.NextLink, headers, tqr.ml)
}

// TableQueryResult contains the response from
// QueryTables and QueryTablesNextResults functions.
type TableQueryResult struct {
	OdataMetadata string  `json:"odata.metadata"`
	Tables        []Table `json:"value"`
	QueryNextLink
	tsc *TableServiceClient
}

func (t *TableServiceClient) queryTables(uri string, headers map[string]string, ml MetadataLevel) (*TableQueryResult, error) {
	if ml == EmptyPayload {
		return nil, errEmptyPayload
	}
	headers = mergeHeaders(headers, t.client.getStandardHeaders())
	headers[headerAccept] = string(ml)

	resp, err := t.client.exec(http.MethodGet, uri, headers, nil, t.auth)
	if err != nil {
		return nil, err
	}
	defer resp.body.Close()

	if err := checkRespCode(resp.statusCode, []int{http.StatusOK}); err != nil {
		return nil, err
	}

	respBody, err := ioutil.ReadAll(resp.body)
	if err != nil {
		return nil, err
	}
	var out TableQueryResult
	err = json.Unmarshal(respBody, &out)
	if err != nil {
		return nil, err
	}

	for i := range out.Tables {
		out.Tables[i].tsc = t
	}
	out.tsc = t

	nextLink := resp.headers.Get(http.CanonicalHeaderKey(headerXmsContinuation))
	if nextLink == "" {
		out.NextLink = nil
	} else {
		originalURI, err := url.Parse(uri)
		if err != nil {
			return nil, err
		}
		v := originalURI.Query()
		v.Set(nextTableQueryParameter, nextLink)
		newURI := t.client.getEndpoint(tableServiceName, tablesURIPath, v)
		out.NextLink = &newURI
		out.ml = ml
	}

	return &out, nil
}

func addBodyRelatedHeaders(h map[string]string, length int) map[string]string {
	h[headerContentType] = "application/json"
	h[headerContentLength] = fmt.Sprintf("%v", length)
	h[headerAcceptCharset] = "UTF-8"
	return h
}

func addReturnContentHeaders(h map[string]string, ml MetadataLevel) map[string]string {
	if ml != EmptyPayload {
		h[headerPrefer] = "return-content"
		h[headerAccept] = string(ml)
	} else {
		h[headerPrefer] = "return-no-content"
		// From API version 2015-12-11 onwards, Accept header is required
		h[headerAccept] = string(NoMetadata)
	}
	return h
}
