package storage

import (
	"context"
	"fmt"
	"io"
	"kra/internal/biz/system"
	"mime"
	"os"
	"path/filepath"
	"sort"
	"strings"

	"kra/internal/conf"
)

type fileStorage struct {
	root      string
	urlPrefix string
}

func New(config *conf.AdminBackend) (system.FileStorage, error) {
	storageType := "local"
	if config != nil && config.Storage != nil && config.Storage.Type != "" {
		storageType = strings.ToLower(config.Storage.Type)
	}
	if storageType == "qiniu" {
		return newQiniuStorage(config.Storage.Qiniu)
	}
	if storageType != "local" {
		var object *conf.AdminBackend_ObjectStore
		switch storageType {
		case "aliyun-oss":
			return newAliyunStorage(config.Storage.AliyunOss)
		case "huawei-obs":
			return newHuaweiStorage(config.Storage.HuaweiObs)
		case "tencent-cos":
			return newTencentStorage(config.Storage.TencentCos)
		case "aws-s3":
			return newAWSStorage(storageType, config.Storage.AwsS3)
		case "cloudflare-r2":
			return newAWSStorage(storageType, config.Storage.CloudflareR2)
		case "minio":
			object = config.Storage.Minio
		default:
			return nil, fmt.Errorf("unsupported storage type %q", storageType)
		}
		return newS3Storage(storageType, object)
	}
	root, prefix := "uploads/file", "uploads/file"
	if config != nil && config.Local != nil {
		if config.Local.StorePath != "" {
			root = config.Local.StorePath
		}
		if config.Local.PathPrefix != "" {
			prefix = config.Local.PathPrefix
		}
	}
	root = filepath.Clean(root)
	if err := os.MkdirAll(root, 0o755); err != nil {
		return nil, fmt.Errorf("create upload directory: %w", err)
	}
	return &fileStorage{root: root, urlPrefix: "/" + strings.Trim(prefix, "/")}, nil
}

func composeFiles(ctx context.Context, storage system.FileStorage, names []string, destination string) (*system.StoredFile, string, error) {
	var stored *system.StoredFile
	hash, err := composeStreams(ctx, names, storage.Open, func(ctx context.Context, destination string, reader io.Reader) error {
		var putErr error
		stored, putErr = storage.Put(ctx, destination, reader)
		return putErr
	}, storage.Delete, destination)
	if err != nil {
		return nil, "", err
	}
	return stored, hash, nil
}

func (s *fileStorage) resolve(name string) (string, error) {
	if name == "" {
		return "", fmt.Errorf("key不能为空")
	}
	clean := filepath.Clean(strings.TrimPrefix(name, "/"))
	if clean == "." || filepath.IsAbs(clean) || clean == ".." || strings.HasPrefix(clean, ".."+string(filepath.Separator)) {
		return "", fmt.Errorf("invalid storage path")
	}
	return filepath.Join(s.root, clean), nil
}

func (s *fileStorage) Put(ctx context.Context, name string, reader io.Reader) (*system.StoredFile, error) {
	path, err := s.resolve(name)
	if err != nil {
		return nil, err
	}
	if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
		return nil, err
	}
	file, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o644)
	if err != nil {
		return nil, err
	}
	size, copyErr := io.Copy(file, reader)
	closeErr := file.Close()
	if copyErr != nil {
		return nil, copyErr
	}
	if closeErr != nil {
		return nil, closeErr
	}
	return &system.StoredFile{Name: filepath.Base(name), Path: name, URL: s.urlPrefix + "/" + strings.TrimPrefix(filepath.ToSlash(name), "/"), Size: size}, nil
}

func (s *fileStorage) Open(ctx context.Context, name string) (io.ReadCloser, error) {
	path, err := s.resolve(name)
	if err != nil {
		return nil, err
	}
	return os.Open(path)
}

func (s *fileStorage) Delete(ctx context.Context, name string) error {
	path, err := s.resolve(name)
	if err != nil {
		return err
	}
	// Match the compatible Local.DeleteFile behavior: a missing object is an error and the
	// media DB record is retained when the physical file cannot be removed.
	if _, err := os.Stat(path); os.IsNotExist(err) {
		return fmt.Errorf("文件不存在")
	}
	if err := os.Remove(path); err != nil {
		return err
	}
	return nil
}

func (s *fileStorage) Compose(ctx context.Context, names []string, destination string) (*system.StoredFile, string, error) {
	return composeFiles(ctx, s, names, destination)
}
func (s *fileStorage) DeletePrefix(ctx context.Context, prefix string) error {
	prefix, err := normalizeDeletePrefix(prefix)
	if err != nil {
		return err
	}
	path, err := s.resolve(prefix + "/placeholder")
	if err != nil {
		return err
	}
	dir := filepath.Dir(path)
	if !strings.HasPrefix(dir, s.root+string(filepath.Separator)) {
		return fmt.Errorf("invalid storage path")
	}
	if err = os.RemoveAll(dir); err != nil {
		return err
	}
	return nil
}
func (s *fileStorage) List(ctx context.Context, prefix, cursor string, limit int) ([]*system.StoredFile, string, bool, error) {
	if limit <= 0 {
		limit = 100
	}
	root, err := s.resolve(strings.TrimSuffix(prefix, "/") + "/placeholder")
	if err != nil {
		return nil, "", false, err
	}
	walkRoot := filepath.Dir(root)
	items := []*system.StoredFile{}
	err = filepath.WalkDir(walkRoot, func(path string, entry os.DirEntry, walkErr error) error {
		if walkErr != nil {
			if os.IsNotExist(walkErr) {
				return nil
			}
			return walkErr
		}
		if ctx.Err() != nil {
			return ctx.Err()
		}
		if entry.IsDir() || entry.Type()&os.ModeSymlink != 0 {
			return nil
		}
		relative, _ := filepath.Rel(s.root, path)
		relative = filepath.ToSlash(relative)
		if relative <= cursor {
			return nil
		}
		info, infoErr := entry.Info()
		if infoErr != nil {
			return infoErr
		}
		items = append(items, &system.StoredFile{Name: entry.Name(), Path: relative, URL: s.urlPrefix + "/" + relative, Size: info.Size(), LastModified: info.ModTime(), ContentType: mime.TypeByExtension(filepath.Ext(entry.Name()))})
		return nil
	})
	if err != nil {
		return nil, "", false, err
	}
	sort.Slice(items, func(i, j int) bool { return items[i].Path < items[j].Path })
	hasMore := len(items) > limit
	if hasMore {
		items = items[:limit]
	}
	next := ""
	if len(items) > 0 {
		next = items[len(items)-1].Path
	}
	return items, next, hasMore, nil
}
