package services

import (
	"context"
	"encoding/xml"
	"fmt"
	"io"
	"net/http"
	"path"
	"strings"
	"time"
)

type webDAVStorageBackend struct {
	cfg    StorageBackendConfig
	client *http.Client
}

func (b webDAVStorageBackend) ID() string   { return b.cfg.ID }
func (b webDAVStorageBackend) Type() string { return StorageBackendWebDAV }

func (b webDAVStorageBackend) Put(ctx context.Context, key string, body io.Reader, _ int64, contentType string) error {
	if err := b.mkcolParents(ctx, key); err != nil {
		return err
	}
	request, err := b.request(ctx, http.MethodPut, key, body)
	if err != nil {
		return err
	}
	if contentType != "" {
		request.Header.Set("Content-Type", contentType)
	}
	response, err := b.client.Do(request)
	if err != nil {
		return err
	}
	defer response.Body.Close()
	if response.StatusCode < 200 || response.StatusCode >= 300 {
		return fmt.Errorf("webdav put failed: %s", response.Status)
	}
	return nil
}

func (b webDAVStorageBackend) Get(ctx context.Context, key string) (StorageObject, error) {
	request, err := b.request(ctx, http.MethodGet, key, nil)
	if err != nil {
		return StorageObject{}, err
	}
	response, err := b.client.Do(request)
	if err != nil {
		return StorageObject{}, err
	}
	if response.StatusCode < 200 || response.StatusCode >= 300 {
		response.Body.Close()
		return StorageObject{}, fmt.Errorf("webdav get failed: %s", response.Status)
	}
	modTime, _ := time.Parse(http.TimeFormat, response.Header.Get("Last-Modified"))
	return StorageObject{Key: key, Size: response.ContentLength, ContentType: response.Header.Get("Content-Type"), ModTime: modTime, Body: response.Body}, nil
}

func (b webDAVStorageBackend) Delete(ctx context.Context, key string) error {
	return b.deletePath(ctx, key)
}

func (b webDAVStorageBackend) DeletePrefix(ctx context.Context, prefix string) error {
	return b.deletePath(ctx, strings.TrimSuffix(prefix, "/")+"/")
}

func (b webDAVStorageBackend) Usage(ctx context.Context) (int64, error) {
	request, err := b.request(ctx, "PROPFIND", "", nil)
	if err != nil {
		return 0, err
	}
	request.Header.Set("Depth", "infinity")
	request.Header.Set("Content-Type", "application/xml")
	response, err := b.client.Do(request)
	if err != nil {
		return 0, err
	}
	defer response.Body.Close()
	if response.StatusCode < 200 || response.StatusCode >= 300 {
		return 0, fmt.Errorf("webdav usage failed: %s", response.Status)
	}
	var multi webDAVMultiStatus
	if err := xml.NewDecoder(response.Body).Decode(&multi); err != nil {
		return 0, err
	}
	var total int64
	for _, item := range multi.Responses {
		if item.PropStat.Prop.ResourceType.Collection != nil {
			continue
		}
		total += item.PropStat.Prop.ContentLength
	}
	return total, nil
}

func (b webDAVStorageBackend) Test(ctx context.Context) error {
	key := ".warpbox-storage-test-" + randomID(6)
	if err := b.Put(ctx, key, strings.NewReader("ok"), 2, "text/plain"); err != nil {
		return err
	}
	return b.Delete(ctx, key)
}

func (b webDAVStorageBackend) deletePath(ctx context.Context, key string) error {
	request, err := b.request(ctx, http.MethodDelete, key, nil)
	if err != nil {
		return err
	}
	response, err := b.client.Do(request)
	if err != nil {
		return err
	}
	defer response.Body.Close()
	if response.StatusCode == http.StatusNotFound {
		return nil
	}
	if response.StatusCode < 200 || response.StatusCode >= 300 {
		return fmt.Errorf("webdav delete failed: %s", response.Status)
	}
	return nil
}

func (b webDAVStorageBackend) mkcolParents(ctx context.Context, key string) error {
	dir := path.Dir(cleanObjectKey(key))
	if dir == "." || dir == "/" {
		return nil
	}
	parts := strings.Split(strings.Trim(dir, "/"), "/")
	current := ""
	for _, part := range parts {
		current = path.Join(current, part)
		request, err := b.request(ctx, "MKCOL", strings.TrimSuffix(current, "/")+"/", nil)
		if err != nil {
			return err
		}
		response, err := b.client.Do(request)
		if err != nil {
			return err
		}
		response.Body.Close()
		if response.StatusCode != http.StatusCreated && response.StatusCode != http.StatusMethodNotAllowed && response.StatusCode != http.StatusConflict {
			return fmt.Errorf("webdav mkcol failed: %s", response.Status)
		}
	}
	return nil
}

func (b webDAVStorageBackend) request(ctx context.Context, method, key string, body io.Reader) (*http.Request, error) {
	endpoint := strings.TrimRight(b.cfg.Endpoint, "/")
	if endpoint == "" {
		return nil, fmt.Errorf("webdav url is required")
	}
	remote := path.Join(cleanRemoteRoot(b.cfg.RemotePath), cleanObjectKey(key))
	if strings.HasSuffix(key, "/") && !strings.HasSuffix(remote, "/") {
		remote += "/"
	}
	target := endpoint + "/" + strings.TrimLeft(remote, "/")
	request, err := http.NewRequestWithContext(ctx, method, target, body)
	if err != nil {
		return nil, err
	}
	if b.cfg.Username != "" || b.cfg.Password != "" {
		request.SetBasicAuth(b.cfg.Username, b.cfg.Password)
	}
	return request, nil
}

type webDAVMultiStatus struct {
	Responses []webDAVResponse `xml:"response"`
}

type webDAVResponse struct {
	PropStat webDAVPropStat `xml:"propstat"`
}

type webDAVPropStat struct {
	Prop webDAVProp `xml:"prop"`
}

type webDAVProp struct {
	ContentLength int64              `xml:"getcontentlength"`
	ResourceType  webDAVResourceType `xml:"resourcetype"`
}

type webDAVResourceType struct {
	Collection *struct{} `xml:"collection"`
}

func newWebDAVStorageBackend(cfg StorageBackendConfig) webDAVStorageBackend {
	return webDAVStorageBackend{cfg: cfg, client: http.DefaultClient}
}
