package storage

// Copyright 2017 Microsoft Corporation
//
//  Licensed under the Apache License, Version 2.0 (the "License");
//  you may not use this file except in compliance with the License.
//  You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
//  Unless required by applicable law or agreed to in writing, software
//  distributed under the License is distributed on an "AS IS" BASIS,
//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//  See the License for the specific language governing permissions and
//  limitations under the License.

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

const (
	tablesURIPath                  = "/Tables"
	nextTableQueryParameter        = "NextTableName"
	headerNextPartitionKey         = "x-ms-continuation-NextPartitionKey"
	headerNextRowKey               = "x-ms-continuation-NextRowKey"
	nextPartitionKeyQueryParameter = "NextPartitionKey"
	nextRowKeyQueryParameter       = "NextRowKey"
)

// TableAccessPolicy are used for SETTING table policies
type TableAccessPolicy struct {
	ID         string
	StartTime  time.Time
	ExpiryTime time.Time
	CanRead    bool
	CanAppend  bool
	CanUpdate  bool
	CanDelete  bool
}

// Table represents an Azure table.
type Table struct {
	tsc           *TableServiceClient
	Name          string `json:"TableName"`
	OdataEditLink string `json:"odata.editLink"`
	OdataID       string `json:"odata.id"`
	OdataMetadata string `json:"odata.metadata"`
	OdataType     string `json:"odata.type"`
}

// EntityQueryResult contains the response from
// ExecuteQuery and ExecuteQueryNextResults functions.
type EntityQueryResult struct {
	OdataMetadata string    `json:"odata.metadata"`
	Entities      []*Entity `json:"value"`
	QueryNextLink
	table *Table
}

type continuationToken struct {
	NextPartitionKey string
	NextRowKey       string
}

func (t *Table) buildPath() string {
	return fmt.Sprintf("/%s", t.Name)
}

func (t *Table) buildSpecificPath() string {
	return fmt.Sprintf("%s('%s')", tablesURIPath, t.Name)
}

// Get gets the referenced table.
// See: https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/querying-tables-and-entities
func (t *Table) Get(timeout uint, ml MetadataLevel) error {
	if ml == EmptyPayload {
		return errEmptyPayload
	}

	query := url.Values{
		"timeout": {strconv.FormatUint(uint64(timeout), 10)},
	}
	headers := t.tsc.client.getStandardHeaders()
	headers[headerAccept] = string(ml)

	uri := t.tsc.client.getEndpoint(tableServiceName, t.buildSpecificPath(), query)
	resp, err := t.tsc.client.exec(http.MethodGet, uri, headers, nil, t.tsc.auth)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

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

	respBody, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return err
	}
	err = json.Unmarshal(respBody, t)
	if err != nil {
		return err
	}
	return nil
}

// Create creates the referenced table.
// This function fails if the name is not compliant
// with the specification or the tables already exists.
// ml determines the level of detail of metadata in the operation response,
// or no data at all.
// See https://docs.microsoft.com/rest/api/storageservices/fileservices/create-table
func (t *Table) Create(timeout uint, ml MetadataLevel, options *TableOptions) error {
	uri := t.tsc.client.getEndpoint(tableServiceName, tablesURIPath, url.Values{
		"timeout": {strconv.FormatUint(uint64(timeout), 10)},
	})

	type createTableRequest struct {
		TableName string `json:"TableName"`
	}
	req := createTableRequest{TableName: t.Name}
	buf := new(bytes.Buffer)
	if err := json.NewEncoder(buf).Encode(req); err != nil {
		return err
	}

	headers := t.tsc.client.getStandardHeaders()
	headers = addReturnContentHeaders(headers, ml)
	headers = addBodyRelatedHeaders(headers, buf.Len())
	headers = options.addToHeaders(headers)

	resp, err := t.tsc.client.exec(http.MethodPost, uri, headers, buf, t.tsc.auth)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	if ml == EmptyPayload {
		if err := checkRespCode(resp, []int{http.StatusNoContent}); err != nil {
			return err
		}
	} else {
		if err := checkRespCode(resp, []int{http.StatusCreated}); err != nil {
			return err
		}
	}

	if ml != EmptyPayload {
		data, err := ioutil.ReadAll(resp.Body)
		if err != nil {
			return err
		}
		err = json.Unmarshal(data, t)
		if err != nil {
			return err
		}
	}

	return nil
}

// Delete deletes the referenced table.
// This function fails if the table is not present.
// Be advised: Delete deletes all the entries that may be present.
// See https://docs.microsoft.com/rest/api/storageservices/fileservices/delete-table
func (t *Table) Delete(timeout uint, options *TableOptions) error {
	uri := t.tsc.client.getEndpoint(tableServiceName, t.buildSpecificPath(), url.Values{
		"timeout": {strconv.Itoa(int(timeout))},
	})

	headers := t.tsc.client.getStandardHeaders()
	headers = addReturnContentHeaders(headers, EmptyPayload)
	headers = options.addToHeaders(headers)

	resp, err := t.tsc.client.exec(http.MethodDelete, uri, headers, nil, t.tsc.auth)
	if err != nil {
		return err
	}
	defer drainRespBody(resp)

	return checkRespCode(resp, []int{http.StatusNoContent})
}

// QueryOptions includes options for a query entities operation.
// Top, filter and select are OData query options.
type QueryOptions struct {
	Top       uint
	Filter    string
	Select    []string
	RequestID string
}

func (options *QueryOptions) 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)
		}
		if len(options.Select) > 0 {
			query.Add(OdataSelect, strings.Join(options.Select, ","))
		}
		headers = addToHeaders(headers, "x-ms-client-request-id", options.RequestID)
	}
	return query, headers
}

// QueryEntities returns the entities in the table.
// You can use query options defined by the OData Protocol specification.
//
// See: https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/query-entities
func (t *Table) QueryEntities(timeout uint, ml MetadataLevel, options *QueryOptions) (*EntityQueryResult, error) {
	if ml == EmptyPayload {
		return nil, errEmptyPayload
	}
	query, headers := options.getParameters()
	query = addTimeout(query, timeout)
	uri := t.tsc.client.getEndpoint(tableServiceName, t.buildPath(), query)
	return t.queryEntities(uri, headers, ml)
}

// NextResults returns the next page of results
// from a QueryEntities or NextResults operation.
//
// See: https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/query-entities
// See https://docs.microsoft.com/rest/api/storageservices/fileservices/query-timeout-and-pagination
func (eqr *EntityQueryResult) NextResults(options *TableOptions) (*EntityQueryResult, error) {
	if eqr == nil {
		return nil, errNilPreviousResult
	}
	if eqr.NextLink == nil {
		return nil, errNilNextLink
	}
	headers := options.addToHeaders(map[string]string{})
	return eqr.table.queryEntities(*eqr.NextLink, headers, eqr.ml)
}

// SetPermissions sets up table ACL permissions
// See https://docs.microsoft.com/rest/api/storageservices/fileservices/Set-Table-ACL
func (t *Table) SetPermissions(tap []TableAccessPolicy, timeout uint, options *TableOptions) error {
	params := url.Values{"comp": {"acl"},
		"timeout": {strconv.Itoa(int(timeout))},
	}

	uri := t.tsc.client.getEndpoint(tableServiceName, t.Name, params)
	headers := t.tsc.client.getStandardHeaders()
	headers = options.addToHeaders(headers)

	body, length, err := generateTableACLPayload(tap)
	if err != nil {
		return err
	}
	headers["Content-Length"] = strconv.Itoa(length)

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

	return checkRespCode(resp, []int{http.StatusNoContent})
}

func generateTableACLPayload(policies []TableAccessPolicy) (io.Reader, int, error) {
	sil := SignedIdentifiers{
		SignedIdentifiers: []SignedIdentifier{},
	}
	for _, tap := range policies {
		permission := generateTablePermissions(&tap)
		signedIdentifier := convertAccessPolicyToXMLStructs(tap.ID, tap.StartTime, tap.ExpiryTime, permission)
		sil.SignedIdentifiers = append(sil.SignedIdentifiers, signedIdentifier)
	}
	return xmlMarshal(sil)
}

// GetPermissions gets the table ACL permissions
// See https://docs.microsoft.com/rest/api/storageservices/fileservices/get-table-acl
func (t *Table) GetPermissions(timeout int, options *TableOptions) ([]TableAccessPolicy, error) {
	params := url.Values{"comp": {"acl"},
		"timeout": {strconv.Itoa(int(timeout))},
	}

	uri := t.tsc.client.getEndpoint(tableServiceName, t.Name, params)
	headers := t.tsc.client.getStandardHeaders()
	headers = options.addToHeaders(headers)

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

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

	var ap AccessPolicy
	err = xmlUnmarshal(resp.Body, &ap.SignedIdentifiersList)
	if err != nil {
		return nil, err
	}
	return updateTableAccessPolicy(ap), nil
}

func (t *Table) queryEntities(uri string, headers map[string]string, ml MetadataLevel) (*EntityQueryResult, error) {
	headers = mergeHeaders(headers, t.tsc.client.getStandardHeaders())
	if ml != EmptyPayload {
		headers[headerAccept] = string(ml)
	}

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

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

	data, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}
	var entities EntityQueryResult
	err = json.Unmarshal(data, &entities)
	if err != nil {
		return nil, err
	}

	for i := range entities.Entities {
		entities.Entities[i].Table = t
	}
	entities.table = t

	contToken := extractContinuationTokenFromHeaders(resp.Header)
	if contToken == nil {
		entities.NextLink = nil
	} else {
		originalURI, err := url.Parse(uri)
		if err != nil {
			return nil, err
		}
		v := originalURI.Query()
		v.Set(nextPartitionKeyQueryParameter, contToken.NextPartitionKey)
		v.Set(nextRowKeyQueryParameter, contToken.NextRowKey)
		newURI := t.tsc.client.getEndpoint(tableServiceName, t.buildPath(), v)
		entities.NextLink = &newURI
		entities.ml = ml
	}

	return &entities, nil
}

func extractContinuationTokenFromHeaders(h http.Header) *continuationToken {
	ct := continuationToken{
		NextPartitionKey: h.Get(headerNextPartitionKey),
		NextRowKey:       h.Get(headerNextRowKey),
	}

	if ct.NextPartitionKey != "" && ct.NextRowKey != "" {
		return &ct
	}
	return nil
}

func updateTableAccessPolicy(ap AccessPolicy) []TableAccessPolicy {
	taps := []TableAccessPolicy{}
	for _, policy := range ap.SignedIdentifiersList.SignedIdentifiers {
		tap := TableAccessPolicy{
			ID:         policy.ID,
			StartTime:  policy.AccessPolicy.StartTime,
			ExpiryTime: policy.AccessPolicy.ExpiryTime,
		}
		tap.CanRead = updatePermissions(policy.AccessPolicy.Permission, "r")
		tap.CanAppend = updatePermissions(policy.AccessPolicy.Permission, "a")
		tap.CanUpdate = updatePermissions(policy.AccessPolicy.Permission, "u")
		tap.CanDelete = updatePermissions(policy.AccessPolicy.Permission, "d")

		taps = append(taps, tap)
	}
	return taps
}

func generateTablePermissions(tap *TableAccessPolicy) (permissions string) {
	// generate the permissions string (raud).
	// still want the end user API to have bool flags.
	permissions = ""

	if tap.CanRead {
		permissions += "r"
	}

	if tap.CanAppend {
		permissions += "a"
	}

	if tap.CanUpdate {
		permissions += "u"
	}

	if tap.CanDelete {
		permissions += "d"
	}
	return permissions
}
