/*
 * Minio Cloud Storage, (C) 2017 Minio, Inc.
 *
 * 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.
 *
 */

package madmin

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

const (
	maxUploadsList = 1000
)

// listBucketHealResult container for listObjects response.
type listBucketHealResult struct {
	// A response can contain CommonPrefixes only if you have
	// specified a delimiter.
	CommonPrefixes []commonPrefix
	// Metadata about each object returned.
	Contents  []ObjectInfo
	Delimiter string

	// Encoding type used to encode object keys in the response.
	EncodingType string

	// A flag that indicates whether or not ListObjects returned all of the results
	// that satisfied the search criteria.
	IsTruncated bool
	Marker      string
	MaxKeys     int64
	Name        string

	// When response is truncated (the IsTruncated element value in
	// the response is true), you can use the key name in this field
	// as marker in the subsequent request to get next set of objects.
	// Object storage lists objects in alphabetical order Note: This
	// element is returned only if you have delimiter request
	// parameter specified. If response does not include the NextMaker
	// and it is truncated, you can use the value of the last Key in
	// the response as the marker in the subsequent request to get the
	// next set of object keys.
	NextMarker string
	Prefix     string
}

// commonPrefix container for prefix response.
type commonPrefix struct {
	Prefix string
}

// Owner - bucket owner/principal
type Owner struct {
	ID          string
	DisplayName string
}

// Bucket container for bucket metadata
type Bucket struct {
	Name         string
	CreationDate string // time string of format "2006-01-02T15:04:05.000Z"

	HealBucketInfo *HealBucketInfo `xml:"HealBucketInfo,omitempty"`
}

// ListBucketsHealResponse - format for list buckets response
type ListBucketsHealResponse struct {
	XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ ListAllMyBucketsResult" json:"-"`

	Owner Owner

	// Container for one or more buckets.
	Buckets struct {
		Buckets []Bucket `xml:"Bucket"`
	} // Buckets are nested
}

// HealStatus - represents different states of healing an object could be in.
type HealStatus int

const (
	// Healthy - Object that is already healthy
	Healthy HealStatus = iota
	// CanHeal - Object can be healed
	CanHeal
	// Corrupted - Object can't be healed
	Corrupted
	// QuorumUnavailable - Object can't be healed until read
	// quorum is available
	QuorumUnavailable
	// CanPartiallyHeal - Object can't be healed completely until
	// disks with missing parts come online
	CanPartiallyHeal
)

// HealBucketInfo - represents healing related information of a bucket.
type HealBucketInfo struct {
	Status HealStatus
}

// BucketInfo - represents bucket metadata.
type BucketInfo struct {
	// Name of the bucket.
	Name string

	// Date and time when the bucket was created.
	Created time.Time

	// Healing information
	HealBucketInfo *HealBucketInfo `xml:"HealBucketInfo,omitempty"`
}

// HealObjectInfo - represents healing related information of an object.
type HealObjectInfo struct {
	Status             HealStatus
	MissingDataCount   int
	MissingParityCount int
}

// ObjectInfo container for object metadata.
type ObjectInfo struct {
	// An ETag is optionally set to md5sum of an object.  In case of multipart objects,
	// ETag is of the form MD5SUM-N where MD5SUM is md5sum of all individual md5sums of
	// each parts concatenated into one string.
	ETag string `json:"etag"`

	Key          string    `json:"name"`         // Name of the object
	LastModified time.Time `json:"lastModified"` // Date and time the object was last modified.
	Size         int64     `json:"size"`         // Size in bytes of the object.
	ContentType  string    `json:"contentType"`  // A standard MIME type describing the format of the object data.

	// Collection of additional metadata on the object.
	// eg: x-amz-meta-*, content-encoding etc.
	Metadata http.Header `json:"metadata"`

	// Owner name.
	Owner struct {
		DisplayName string `json:"name"`
		ID          string `json:"id"`
	} `json:"owner"`

	// The class of storage used to store the object.
	StorageClass string `json:"storageClass"`

	// Error
	Err            error           `json:"-"`
	HealObjectInfo *HealObjectInfo `json:"healObjectInfo,omitempty"`
}

// UploadInfo - represents an ongoing upload that needs to be healed.
type UploadInfo struct {
	Key string `json:"name"` // Name of the object being uploaded.

	UploadID string `json:"uploadId"` // UploadID
	// Owner name.
	Owner struct {
		DisplayName string `json:"name"`
		ID          string `json:"id"`
	} `json:"owner"`

	// The class of storage used to store the object.
	StorageClass string `json:"storageClass"`

	Initiated time.Time `json:"initiated"` // Time at which upload was initiated.

	// Error
	Err            error           `json:"-"`
	HealUploadInfo *HealObjectInfo `json:"healObjectInfo,omitempty"`
}

// Initiator - has same properties as Owner.
type Initiator Owner

// upload - represents an ongoing multipart upload.
type upload struct {
	Key            string
	UploadID       string `xml:"UploadId"`
	Initiator      Initiator
	Owner          Owner
	StorageClass   string
	Initiated      time.Time
	HealUploadInfo *HealObjectInfo `xml:"HealObjectInfo,omitempty"`
}

// listUploadsHealResponse - represents ListUploadsHeal response.
type listUploadsHealResponse struct {
	XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ ListMultipartUploadsResult" json:"-"`

	Bucket             string
	KeyMarker          string
	UploadIDMarker     string `xml:"UploadIdMarker"`
	NextKeyMarker      string
	NextUploadIDMarker string `xml:"NextUploadIdMarker"`
	Delimiter          string
	Prefix             string
	EncodingType       string `xml:"EncodingType,omitempty"`
	MaxUploads         int
	IsTruncated        bool

	// List of pending uploads.
	Uploads []upload `xml:"Upload"`

	// Delimed common prefixes.
	CommonPrefixes []commonPrefix
}

type healQueryKey string

const (
	healBucket         healQueryKey = "bucket"
	healObject         healQueryKey = "object"
	healPrefix         healQueryKey = "prefix"
	healMarker         healQueryKey = "marker"
	healDelimiter      healQueryKey = "delimiter"
	healMaxKey         healQueryKey = "max-key"
	healDryRun         healQueryKey = "dry-run"
	healUploadIDMarker healQueryKey = "upload-id-marker"
	healMaxUpload      healQueryKey = "max-uploads"
	healUploadID       healQueryKey = "upload-id"
)

// mkHealQueryVal - helper function to construct heal REST API query params.
func mkHealQueryVal(bucket, prefix, marker, delimiter, maxKeyStr string) url.Values {
	queryVal := make(url.Values)
	queryVal.Set("heal", "")
	queryVal.Set(string(healBucket), bucket)
	queryVal.Set(string(healPrefix), prefix)
	queryVal.Set(string(healMarker), marker)
	queryVal.Set(string(healDelimiter), delimiter)
	queryVal.Set(string(healMaxKey), maxKeyStr)
	return queryVal
}

// listObjectsHeal - issues heal list API request for a batch of maxKeys objects to be healed.
func (adm *AdminClient) listObjectsHeal(bucket, prefix, marker, delimiter string, maxKeys int) (listBucketHealResult, error) {
	// Construct query params.
	maxKeyStr := fmt.Sprintf("%d", maxKeys)
	queryVal := mkHealQueryVal(bucket, prefix, marker, delimiter, maxKeyStr)

	hdrs := make(http.Header)
	hdrs.Set(minioAdminOpHeader, "list-objects")

	reqData := requestData{
		queryValues:   queryVal,
		customHeaders: hdrs,
	}

	// Empty 'list' of objects to be healed.
	toBeHealedObjects := listBucketHealResult{}

	// Execute GET on /?heal to list objects needing heal.
	resp, err := adm.executeMethod("GET", reqData)

	defer closeResponse(resp)
	if err != nil {
		return listBucketHealResult{}, err
	}

	if resp.StatusCode != http.StatusOK {
		return toBeHealedObjects, httpRespToErrorResponse(resp)

	}

	err = xml.NewDecoder(resp.Body).Decode(&toBeHealedObjects)
	return toBeHealedObjects, err
}

// ListObjectsHeal - Lists upto maxKeys objects that needing heal matching bucket, prefix, marker, delimiter.
func (adm *AdminClient) ListObjectsHeal(bucket, prefix string, recursive bool, doneCh <-chan struct{}) (<-chan ObjectInfo, error) {
	// Allocate new list objects channel.
	objectStatCh := make(chan ObjectInfo, 1)
	// Default listing is delimited at "/"
	delimiter := "/"
	if recursive {
		// If recursive we do not delimit.
		delimiter = ""
	}

	// Initiate list objects goroutine here.
	go func(objectStatCh chan<- ObjectInfo) {
		defer close(objectStatCh)
		// Save marker for next request.
		var marker string
		for {
			// Get list of objects a maximum of 1000 per request.
			result, err := adm.listObjectsHeal(bucket, prefix, marker, delimiter, 1000)
			if err != nil {
				objectStatCh <- ObjectInfo{
					Err: err,
				}
				return
			}

			// If contents are available loop through and send over channel.
			for _, object := range result.Contents {
				// Save the marker.
				marker = object.Key
				select {
				// Send object content.
				case objectStatCh <- object:
				// If receives done from the caller, return here.
				case <-doneCh:
					return
				}
			}

			// Send all common prefixes if any.
			// NOTE: prefixes are only present if the request is delimited.
			for _, obj := range result.CommonPrefixes {
				object := ObjectInfo{}
				object.Key = obj.Prefix
				object.Size = 0
				select {
				// Send object prefixes.
				case objectStatCh <- object:
				// If receives done from the caller, return here.
				case <-doneCh:
					return
				}
			}

			// If next marker present, save it for next request.
			if result.NextMarker != "" {
				marker = result.NextMarker
			}

			// Listing ends result is not truncated, return right here.
			if !result.IsTruncated {
				return
			}
		}
	}(objectStatCh)
	return objectStatCh, nil
}

const timeFormatAMZLong = "2006-01-02T15:04:05.000Z" // Reply date format with nanosecond precision.

// ListBucketsHeal - issues heal bucket list API request
func (adm *AdminClient) ListBucketsHeal() ([]BucketInfo, error) {
	queryVal := url.Values{}
	queryVal.Set("heal", "")

	hdrs := make(http.Header)
	hdrs.Set(minioAdminOpHeader, "list-buckets")

	reqData := requestData{
		queryValues:   queryVal,
		customHeaders: hdrs,
	}

	// Execute GET on /?heal to list objects needing heal.
	resp, err := adm.executeMethod("GET", reqData)

	defer closeResponse(resp)
	if err != nil {
		return []BucketInfo{}, err
	}

	if resp.StatusCode != http.StatusOK {
		return []BucketInfo{}, httpRespToErrorResponse(resp)
	}

	var listBucketsHealResult ListBucketsHealResponse

	err = xml.NewDecoder(resp.Body).Decode(&listBucketsHealResult)
	if err != nil {
		return []BucketInfo{}, err
	}

	var bucketsToBeHealed []BucketInfo

	for _, bucket := range listBucketsHealResult.Buckets.Buckets {
		creationDate, err := time.Parse(timeFormatAMZLong, bucket.CreationDate)
		if err != nil {
			return []BucketInfo{}, err
		}
		bucketsToBeHealed = append(bucketsToBeHealed,
			BucketInfo{
				Name:           bucket.Name,
				Created:        creationDate,
				HealBucketInfo: bucket.HealBucketInfo,
			})
	}

	return bucketsToBeHealed, nil
}

// HealBucket - Heal the given bucket
func (adm *AdminClient) HealBucket(bucket string, dryrun bool) error {
	// Construct query params.
	queryVal := url.Values{}
	queryVal.Set("heal", "")
	queryVal.Set(string(healBucket), bucket)
	if dryrun {
		queryVal.Set(string(healDryRun), "")
	}

	hdrs := make(http.Header)
	hdrs.Set(minioAdminOpHeader, "bucket")

	reqData := requestData{
		queryValues:   queryVal,
		customHeaders: hdrs,
	}

	// Execute POST on /?heal&bucket=mybucket to heal a bucket.
	resp, err := adm.executeMethod("POST", reqData)

	defer closeResponse(resp)
	if err != nil {
		return err
	}

	if resp.StatusCode != http.StatusOK {
		return httpRespToErrorResponse(resp)
	}

	return nil
}

// HealUpload - Heal the given upload.
func (adm *AdminClient) HealUpload(bucket, object, uploadID string, dryrun bool) (HealResult, error) {
	// Construct query params.
	queryVal := url.Values{}
	queryVal.Set("heal", "")
	queryVal.Set(string(healBucket), bucket)
	queryVal.Set(string(healObject), object)
	queryVal.Set(string(healUploadID), uploadID)
	if dryrun {
		queryVal.Set(string(healDryRun), "")
	}

	hdrs := make(http.Header)
	hdrs.Set(minioAdminOpHeader, "upload")

	reqData := requestData{
		queryValues:   queryVal,
		customHeaders: hdrs,
	}

	// Execute POST on
	// /?heal&bucket=mybucket&object=myobject&upload-id=uploadID
	// to heal an upload.
	resp, err := adm.executeMethod("POST", reqData)

	defer closeResponse(resp)
	if err != nil {
		return HealResult{}, err
	}

	if resp.StatusCode != http.StatusOK {
		return HealResult{}, httpRespToErrorResponse(resp)
	}

	// Healing is not performed so heal object result is empty.
	if dryrun {
		return HealResult{}, nil
	}

	jsonBytes, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return HealResult{}, err
	}

	healResult := HealResult{}
	err = json.Unmarshal(jsonBytes, &healResult)
	if err != nil {
		return HealResult{}, err
	}

	return healResult, nil
}

// HealResult - represents result of heal-object admin API.
type HealResult struct {
	State HealState `json:"state"`
}

// HealState - different states of heal operation
type HealState int

const (
	// HealNone - none of the disks healed
	HealNone HealState = iota
	// HealPartial - some disks were healed, others were offline
	HealPartial
	// HealOK - all disks were healed
	HealOK
)

// HealObject - Heal the given object.
func (adm *AdminClient) HealObject(bucket, object string, dryrun bool) (HealResult, error) {
	// Construct query params.
	queryVal := url.Values{}
	queryVal.Set("heal", "")
	queryVal.Set(string(healBucket), bucket)
	queryVal.Set(string(healObject), object)
	if dryrun {
		queryVal.Set(string(healDryRun), "")
	}

	hdrs := make(http.Header)
	hdrs.Set(minioAdminOpHeader, "object")

	reqData := requestData{
		queryValues:   queryVal,
		customHeaders: hdrs,
	}

	// Execute POST on /?heal&bucket=mybucket&object=myobject to heal an object.
	resp, err := adm.executeMethod("POST", reqData)

	defer closeResponse(resp)
	if err != nil {
		return HealResult{}, err
	}

	if resp.StatusCode != http.StatusOK {
		return HealResult{}, httpRespToErrorResponse(resp)
	}

	// Healing is not performed so heal object result is empty.
	if dryrun {
		return HealResult{}, nil
	}

	jsonBytes, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return HealResult{}, err
	}

	healResult := HealResult{}
	err = json.Unmarshal(jsonBytes, &healResult)
	if err != nil {
		return HealResult{}, err
	}

	return healResult, nil
}

// HealFormat - heal storage format on available disks.
func (adm *AdminClient) HealFormat(dryrun bool) error {
	queryVal := url.Values{}
	queryVal.Set("heal", "")
	if dryrun {
		queryVal.Set(string(healDryRun), "")
	}

	// Set x-minio-operation to format.
	hdrs := make(http.Header)
	hdrs.Set(minioAdminOpHeader, "format")

	reqData := requestData{
		queryValues:   queryVal,
		customHeaders: hdrs,
	}

	// Execute POST on /?heal to heal storage format.
	resp, err := adm.executeMethod("POST", reqData)

	defer closeResponse(resp)
	if err != nil {
		return err
	}

	if resp.StatusCode != http.StatusOK {
		return httpRespToErrorResponse(resp)
	}

	return nil
}

// mkUploadsHealQuery - helper function to construct query params for
// ListUploadsHeal API.
func mkUploadsHealQuery(bucket, prefix, marker, uploadIDMarker, delimiter, maxUploadsStr string) url.Values {

	queryVal := make(url.Values)
	queryVal.Set("heal", "")
	queryVal.Set(string(healBucket), bucket)
	queryVal.Set(string(healPrefix), prefix)
	queryVal.Set(string(healMarker), marker)
	queryVal.Set(string(healUploadIDMarker), uploadIDMarker)
	queryVal.Set(string(healDelimiter), delimiter)
	queryVal.Set(string(healMaxUpload), maxUploadsStr)
	return queryVal
}

func (adm *AdminClient) listUploadsHeal(bucket, prefix, marker, uploadIDMarker, delimiter string, maxUploads int) (listUploadsHealResponse, error) {
	// Construct query params.
	maxUploadsStr := fmt.Sprintf("%d", maxUploads)
	queryVal := mkUploadsHealQuery(bucket, prefix, marker, uploadIDMarker, delimiter, maxUploadsStr)

	hdrs := make(http.Header)
	hdrs.Set(minioAdminOpHeader, "list-uploads")

	reqData := requestData{
		queryValues:   queryVal,
		customHeaders: hdrs,
	}

	// Empty 'list' of objects to be healed.
	toBeHealedUploads := listUploadsHealResponse{}

	// Execute GET on /?heal to list objects needing heal.
	resp, err := adm.executeMethod("GET", reqData)

	defer closeResponse(resp)
	if err != nil {
		return listUploadsHealResponse{}, err
	}

	if resp.StatusCode != http.StatusOK {
		return toBeHealedUploads, httpRespToErrorResponse(resp)

	}

	err = xml.NewDecoder(resp.Body).Decode(&toBeHealedUploads)
	if err != nil {
		return listUploadsHealResponse{}, err
	}

	return toBeHealedUploads, nil
}

// ListUploadsHeal - issues list heal uploads API request
func (adm *AdminClient) ListUploadsHeal(bucket, prefix string, recursive bool,
	doneCh <-chan struct{}) (<-chan UploadInfo, error) {

	// Default listing is delimited at "/"
	delimiter := "/"
	if recursive {
		// If recursive we do not delimit.
		delimiter = ""
	}

	uploadIDMarker := ""

	// Allocate new list objects channel.
	uploadStatCh := make(chan UploadInfo, maxUploadsList)

	// Initiate list objects goroutine here.
	go func(uploadStatCh chan<- UploadInfo) {
		defer close(uploadStatCh)
		// Save marker for next request.
		var marker string
		for {
			// Get list of objects a maximum of 1000 per request.
			result, err := adm.listUploadsHeal(bucket, prefix, marker,
				uploadIDMarker, delimiter, maxUploadsList)
			if err != nil {
				uploadStatCh <- UploadInfo{
					Err: err,
				}
				return
			}

			// If contents are available loop through and
			// send over channel.
			for _, upload := range result.Uploads {
				select {
				// Send upload info.
				case uploadStatCh <- UploadInfo{
					Key:            upload.Key,
					UploadID:       upload.UploadID,
					Initiated:      upload.Initiated,
					HealUploadInfo: upload.HealUploadInfo,
				}:
				// If receives done from the caller, return here.
				case <-doneCh:
					return
				}
			}

			// Send all common prefixes if any.  NOTE:
			// prefixes are only present if the request is
			// delimited.
			for _, prefix := range result.CommonPrefixes {
				upload := UploadInfo{}
				upload.Key = prefix.Prefix
				select {
				// Send object prefixes.
				case uploadStatCh <- upload:
				// If receives done from the caller, return here.
				case <-doneCh:
					return
				}
			}

			// If next uploadID marker is present, set it
			// for the next request.
			if result.NextUploadIDMarker != "" {
				uploadIDMarker = result.NextUploadIDMarker
			}

			// If next marker present, save it for next request.
			if result.KeyMarker != "" {
				marker = result.KeyMarker
			}

			// Listing ends result is not truncated,
			// return right here.
			if !result.IsTruncated {
				return
			}
		}
	}(uploadStatCh)
	return uploadStatCh, nil
}
