package dfstore

import (
	"context"
	"errors"
	"fmt"
	"github.com/go-http-utils/headers"
	"io"
	"net/http"
	"net/url"
	"path"
	"strconv"

	"code.gitea.io/gitea/modules/urfs_client/config"
	pkgobjectstorage "code.gitea.io/gitea/modules/urfs_client/objectstorage"
)

// Dfstore is the interface used for object storage.
type Dfstore interface {

	// GetUrfsMetadataRequestWithContext returns *http.Request of getting Urfs metadata.
	GetUrfsMetadataRequestWithContext(ctx context.Context, input *GetUrfsMetadataInput) (*http.Request, error)

	// GetUrfsMetadataWithContext returns matedata of Urfs.
	GetUrfsMetadataWithContext(ctx context.Context, input *GetUrfsMetadataInput) (*pkgobjectstorage.ObjectMetadata, error)

	// GetUrfsRequestWithContext returns *http.Request of getting Urfs.
	GetUrfsRequestWithContext(ctx context.Context, input *GetUrfsInput) (*http.Request, error)

	// GetUrfsWithContext returns data of Urfs.
	GetUrfsWithContext(ctx context.Context, input *GetUrfsInput) (io.ReadCloser, error)

	// GetUrfsStatusRequestWithContext returns *http.Request of getting Urfs status.
	GetUrfsStatusRequestWithContext(ctx context.Context, input *GetUrfsInput) (*http.Request, error)

	// GetUrfsStatusWithContext returns schedule status of Urfs.
	GetUrfsStatusWithContext(ctx context.Context, input *GetUrfsInput) (io.ReadCloser, error)
}

// dfstore provides object storage function.
type dfstore struct {
	endpoint   string
	httpClient *http.Client
}

// Option is a functional option for configuring the dfstore.
type Option func(dfs *dfstore)

// New dfstore instance.
func New(endpoint string, options ...Option) Dfstore {
	dfs := &dfstore{
		endpoint:   endpoint,
		httpClient: http.DefaultClient,
	}

	for _, opt := range options {
		opt(dfs)
	}

	return dfs
}

// GetUrfsMetadataInput is used to construct request of getting object metadata.
type GetUrfsMetadataInput struct {

	// Endpoint is endpoint name.
	Endpoint string

	// BucketName is bucket name.
	BucketName string

	// ObjectKey is object key.
	ObjectKey string

	// DstPeer is target peerHost.
	DstPeer string
}

// Validate validates GetUrfsMetadataInput fields.
func (i *GetUrfsMetadataInput) Validate() error {

	if i.Endpoint == "" {
		return errors.New("invalid Endpoint")

	}

	if i.BucketName == "" {
		return errors.New("invalid BucketName")

	}

	if i.ObjectKey == "" {
		return errors.New("invalid ObjectKey")
	}

	return nil
}

// GetObjectMetadataRequestWithContext returns *http.Request of getting object metadata.
func (dfs *dfstore) GetUrfsMetadataRequestWithContext(ctx context.Context, input *GetUrfsMetadataInput) (*http.Request, error) {
	if err := input.Validate(); err != nil {
		return nil, err
	}

	dstUrl := url.URL{
		Scheme: "http",
		Host:   fmt.Sprintf("%s:%d", input.DstPeer, config.DefaultObjectStorageStartPort),
	}

	u, err := url.Parse(dstUrl.String())
	if err != nil {
		return nil, err
	}

	u.Path = path.Join("buckets", input.BucketName+"."+input.Endpoint, "objects", input.ObjectKey)
	req, err := http.NewRequestWithContext(ctx, http.MethodHead, u.String(), nil)
	if err != nil {
		return nil, err
	}

	return req, nil
}

// GetObjectMetadataWithContext returns metadata of object.
func (dfs *dfstore) GetUrfsMetadataWithContext(ctx context.Context, input *GetUrfsMetadataInput) (*pkgobjectstorage.ObjectMetadata, error) {
	req, err := dfs.GetUrfsMetadataRequestWithContext(ctx, input)
	if err != nil {
		return nil, err
	}

	resp, err := dfs.httpClient.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	if resp.StatusCode/100 != 2 {
		return nil, fmt.Errorf("bad response status %s", resp.Status)
	}

	contentLength, err := strconv.ParseInt(resp.Header.Get(headers.ContentLength), 10, 64)
	if err != nil {
		return nil, err
	}

	return &pkgobjectstorage.ObjectMetadata{
		ContentDisposition: resp.Header.Get(headers.ContentDisposition),
		ContentEncoding:    resp.Header.Get(headers.ContentEncoding),
		ContentLanguage:    resp.Header.Get(headers.ContentLanguage),
		ContentLength:      int64(contentLength),
		ContentType:        resp.Header.Get(headers.ContentType),
		ETag:               resp.Header.Get(headers.ContentType),
		Digest:             resp.Header.Get(config.HeaderDragonflyObjectMetaDigest),
	}, nil
}

// GetUrfsInput is used to construct request of getting object.
type GetUrfsInput struct {

	// Endpoint is endpoint name.
	Endpoint string

	// BucketName is bucket name.
	BucketName string

	// ObjectKey is object key.
	ObjectKey string

	// Filter is used to generate a unique Task ID by
	// filtering unnecessary query params in the URL,
	// it is separated by & character.
	Filter string

	// Range is the HTTP range header.
	Range string

	// DstPeer is target peerHost.
	DstPeer string
}

// GetObjectWithContext returns data of object.
func (dfs *dfstore) GetUrfsWithContext(ctx context.Context, input *GetUrfsInput) (io.ReadCloser, error) {
	req, err := dfs.GetUrfsRequestWithContext(ctx, input)
	if err != nil {
		return nil, err
	}

	resp, err := dfs.httpClient.Do(req)
	if err != nil {
		return nil, err
	}

	if resp.StatusCode/100 != 2 {
		return nil, fmt.Errorf("bad response status %s", resp.Status)
	}

	return resp.Body, nil
}

// GetObjectRequestWithContext returns *http.Request of getting object.
func (dfs *dfstore) GetUrfsRequestWithContext(ctx context.Context, input *GetUrfsInput) (*http.Request, error) {
	if err := input.Validate(); err != nil {
		return nil, err
	}

	dstUrl := url.URL{
		Scheme: "http",
		Host:   fmt.Sprintf("%s:%d", input.DstPeer, config.DefaultObjectStorageStartPort),
	}

	u, err := url.Parse(dstUrl.String())
	if err != nil {
		return nil, err
	}

	u.Path = path.Join("buckets", input.BucketName+"."+input.Endpoint, "cache_object", input.ObjectKey)

	query := u.Query()
	if input.Filter != "" {
		query.Set("filter", input.Filter)
	}
	u.RawQuery = query.Encode()
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, u.String(), nil)
	if err != nil {
		return nil, err
	}

	if input.Range != "" {
		req.Header.Set(headers.Range, input.Range)
	}

	return req, nil
}

// Validate validates GetUrfsInput fields.
func (i *GetUrfsInput) Validate() error {

	if i.Endpoint == "" {
		return errors.New("invalid Endpoint")

	}

	if i.BucketName == "" {
		return errors.New("invalid BucketName")

	}

	if i.ObjectKey == "" {
		return errors.New("invalid ObjectKey")
	}

	return nil
}

// GetUrfsStatusWithContext returns schedule task status.
func (dfs *dfstore) GetUrfsStatusWithContext(ctx context.Context, input *GetUrfsInput) (io.ReadCloser, error) {
	req, err := dfs.GetUrfsStatusRequestWithContext(ctx, input)
	if err != nil {
		return nil, err
	}

	resp, err := dfs.httpClient.Do(req)
	if err != nil {
		return nil, err
	}

	if resp.StatusCode/100 != 2 {
		return nil, fmt.Errorf("bad response status %s", resp.Status)
	}

	return resp.Body, nil
}

// GetObjectStatusRequestWithContext returns *http.Request of check schedule task status.
func (dfs *dfstore) GetUrfsStatusRequestWithContext(ctx context.Context, input *GetUrfsInput) (*http.Request, error) {
	if err := input.Validate(); err != nil {
		return nil, err
	}

	dstUrl := url.URL{
		Scheme: "http",
		Host:   fmt.Sprintf("%s:%d", input.DstPeer, config.DefaultObjectStorageStartPort),
	}

	u, err := url.Parse(dstUrl.String())
	if err != nil {
		return nil, err
	}

	u.Path = path.Join("buckets", input.BucketName+"."+input.Endpoint, "check_object", input.ObjectKey)

	query := u.Query()
	if input.Filter != "" {
		query.Set("filter", input.Filter)
	}
	u.RawQuery = query.Encode()
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
	if err != nil {
		return nil, err
	}

	if input.Range != "" {
		req.Header.Set(headers.Range, input.Range)
	}

	return req, nil
}
