/*
 * 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 b2

import (
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strconv"
	"strings"
	"time"

	"github.com/minio/minio/pkg/errors"

	minio "github.com/minio/minio/cmd"
)

// mkRange converts offset, size into Range header equivalent.
func mkRange(offset, size int64) string {
	if offset == 0 && size == 0 {
		return ""
	}
	if size == 0 {
		return fmt.Sprintf("bytes=%d-", offset)
	}
	return fmt.Sprintf("bytes=%d-%d", offset, offset+size-1)
}

// AnonGetObject - performs a plain http GET request on a public resource,
// fails if the resource is not public.
func (l *b2Objects) AnonGetObject(bucket string, object string, startOffset int64, length int64, writer io.Writer, etag string) error {
	uri := fmt.Sprintf("%s/file/%s/%s", l.b2Client.DownloadURI, bucket, object)
	req, err := http.NewRequest("GET", uri, nil)
	if err != nil {
		return b2ToObjectError(errors.Trace(err), bucket, object)
	}
	rng := mkRange(startOffset, length)
	if rng != "" {
		req.Header.Set("Range", rng)
	}
	resp, err := l.anonClient.Do(req)
	if err != nil {
		return b2ToObjectError(errors.Trace(err), bucket, object)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		return b2ToObjectError(errors.Trace(fmt.Errorf(resp.Status)), bucket, object)
	}
	_, err = io.Copy(writer, resp.Body)
	return b2ToObjectError(errors.Trace(err), bucket, object)
}

// Converts http Header into ObjectInfo. This function looks for all the
// standard Backblaze B2 headers to convert into ObjectInfo.
//
// Content-Length is converted to Size.
// X-Bz-Upload-Timestamp is converted to ModTime.
// X-Bz-Info-<header>:<value> is converted to <header>:<value>
// Content-Type is converted to ContentType.
// X-Bz-Content-Sha1 is converted to ETag.
func headerToObjectInfo(bucket, object string, header http.Header) (objInfo minio.ObjectInfo, err error) {
	clen, err := strconv.ParseInt(header.Get("Content-Length"), 10, 64)
	if err != nil {
		return objInfo, b2ToObjectError(errors.Trace(err), bucket, object)
	}

	// Converting upload timestamp in milliseconds to a time.Time value for ObjectInfo.ModTime.
	timeStamp, err := strconv.ParseInt(header.Get("X-Bz-Upload-Timestamp"), 10, 64)
	if err != nil {
		return objInfo, b2ToObjectError(errors.Trace(err), bucket, object)
	}

	// Populate user metadata by looking for all the X-Bz-Info-<name>
	// HTTP headers, ignore other headers since they have their own
	// designated meaning, for more details refer B2 API documentation.
	userMetadata := make(map[string]string)
	for key := range header {
		if strings.HasPrefix(key, "X-Bz-Info-") {
			var name string
			name, err = url.QueryUnescape(strings.TrimPrefix(key, "X-Bz-Info-"))
			if err != nil {
				return objInfo, b2ToObjectError(errors.Trace(err), bucket, object)
			}
			var val string
			val, err = url.QueryUnescape(header.Get(key))
			if err != nil {
				return objInfo, b2ToObjectError(errors.Trace(err), bucket, object)
			}
			userMetadata[name] = val
		}
	}

	return minio.ObjectInfo{
		Bucket:      bucket,
		Name:        object,
		ContentType: header.Get("Content-Type"),
		ModTime:     time.Unix(0, 0).Add(time.Duration(timeStamp) * time.Millisecond),
		Size:        clen,
		ETag:        header.Get("X-Bz-File-Id"),
		UserDefined: userMetadata,
	}, nil
}

// AnonGetObjectInfo - performs a plain http HEAD request on a public resource,
// fails if the resource is not public.
func (l *b2Objects) AnonGetObjectInfo(bucket string, object string) (objInfo minio.ObjectInfo, err error) {
	uri := fmt.Sprintf("%s/file/%s/%s", l.b2Client.DownloadURI, bucket, object)
	req, err := http.NewRequest("HEAD", uri, nil)
	if err != nil {
		return objInfo, b2ToObjectError(errors.Trace(err), bucket, object)
	}
	resp, err := l.anonClient.Do(req)
	if err != nil {
		return objInfo, b2ToObjectError(errors.Trace(err), bucket, object)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		return objInfo, b2ToObjectError(errors.Trace(fmt.Errorf(resp.Status)), bucket, object)
	}
	return headerToObjectInfo(bucket, object, resp.Header)
}