// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package setting

import (
	"errors"
	"fmt"
	"path/filepath"
)

// StorageType is a type of Storage
type StorageType string

const (
	// LocalStorageType is the type descriptor for local storage
	LocalStorageType StorageType = "local"
	// MinioStorageType is the type descriptor for minio storage
	MinioStorageType StorageType = "minio"
)

var storageTypes = []StorageType{
	LocalStorageType,
	MinioStorageType,
}

// IsValidStorageType returns true if the given storage type is valid
func IsValidStorageType(storageType StorageType) bool {
	for _, t := range storageTypes {
		if t == storageType {
			return true
		}
	}
	return false
}

// MinioStorageConfig represents the configuration for a minio storage
type MinioStorageConfig struct {
	Endpoint           string `ini:"MINIO_ENDPOINT" json:",omitempty"`
	AccessKeyID        string `ini:"MINIO_ACCESS_KEY_ID" json:",omitempty"`
	SecretAccessKey    string `ini:"MINIO_SECRET_ACCESS_KEY" json:",omitempty"`
	Bucket             string `ini:"MINIO_BUCKET" json:",omitempty"`
	Location           string `ini:"MINIO_LOCATION" json:",omitempty"`
	BasePath           string `ini:"MINIO_BASE_PATH" json:",omitempty"`
	UseSSL             bool   `ini:"MINIO_USE_SSL"`
	InsecureSkipVerify bool   `ini:"MINIO_INSECURE_SKIP_VERIFY"`
	ChecksumAlgorithm  string `ini:"MINIO_CHECKSUM_ALGORITHM" json:",omitempty"`
	ServeDirect        bool   `ini:"SERVE_DIRECT"`
}

// Storage represents configuration of storages
type Storage struct {
	Type          StorageType        // local or minio
	Path          string             `json:",omitempty"` // for local type
	TemporaryPath string             `json:",omitempty"`
	MinioConfig   MinioStorageConfig // for minio type
}

func (storage *Storage) ToShadowCopy() Storage {
	shadowStorage := *storage
	if shadowStorage.MinioConfig.AccessKeyID != "" {
		shadowStorage.MinioConfig.AccessKeyID = "******"
	}
	if shadowStorage.MinioConfig.SecretAccessKey != "" {
		shadowStorage.MinioConfig.SecretAccessKey = "******"
	}
	return shadowStorage
}

const storageSectionName = "storage"

func getDefaultStorageSection(rootCfg ConfigProvider) ConfigSection {
	storageSec := rootCfg.Section(storageSectionName)
	// Global Defaults
	storageSec.Key("STORAGE_TYPE").MustString("local")
	storageSec.Key("MINIO_ENDPOINT").MustString("localhost:9000")
	storageSec.Key("MINIO_ACCESS_KEY_ID").MustString("")
	storageSec.Key("MINIO_SECRET_ACCESS_KEY").MustString("")
	storageSec.Key("MINIO_BUCKET").MustString("gitea")
	storageSec.Key("MINIO_LOCATION").MustString("us-east-1")
	storageSec.Key("MINIO_USE_SSL").MustBool(false)
	storageSec.Key("MINIO_INSECURE_SKIP_VERIFY").MustBool(false)
	storageSec.Key("MINIO_CHECKSUM_ALGORITHM").MustString("default")
	return storageSec
}

// getStorage will find target section and extra special section first and then read override
// items from extra section
func getStorage(rootCfg ConfigProvider, name, typ string, sec ConfigSection) (*Storage, error) {
	if name == "" {
		return nil, errors.New("no name for storage")
	}

	var targetSec ConfigSection
	// check typ first
	if typ != "" {
		var err error
		targetSec, err = rootCfg.GetSection(storageSectionName + "." + typ)
		if err != nil {
			if !IsValidStorageType(StorageType(typ)) {
				return nil, fmt.Errorf("get section via storage type %q failed: %v", typ, err)
			}
		}
		if targetSec != nil {
			targetType := targetSec.Key("STORAGE_TYPE").String()
			if targetType == "" {
				if !IsValidStorageType(StorageType(typ)) {
					return nil, fmt.Errorf("unknow storage type %q", typ)
				}
				targetSec.Key("STORAGE_TYPE").SetValue(typ)
			} else if !IsValidStorageType(StorageType(targetType)) {
				return nil, fmt.Errorf("unknow storage type %q for section storage.%v", targetType, typ)
			}
		}
	}

	if targetSec == nil && sec != nil {
		secTyp := sec.Key("STORAGE_TYPE").String()
		if IsValidStorageType(StorageType(secTyp)) {
			targetSec = sec
		} else if secTyp != "" {
			targetSec, _ = rootCfg.GetSection(storageSectionName + "." + secTyp)
		}
	}

	targetSecIsStoragename := false
	storageNameSec, _ := rootCfg.GetSection(storageSectionName + "." + name)
	if targetSec == nil {
		targetSec = storageNameSec
		targetSecIsStoragename = storageNameSec != nil
	}

	if targetSec == nil {
		targetSec = getDefaultStorageSection(rootCfg)
	} else {
		targetType := targetSec.Key("STORAGE_TYPE").String()
		switch {
		case targetType == "":
			if targetSec != storageNameSec && storageNameSec != nil {
				targetSec = storageNameSec
				targetSecIsStoragename = true
				if targetSec.Key("STORAGE_TYPE").String() == "" {
					return nil, fmt.Errorf("storage section %s.%s has no STORAGE_TYPE", storageSectionName, name)
				}
			} else {
				if targetSec.Key("PATH").String() == "" {
					targetSec = getDefaultStorageSection(rootCfg)
				} else {
					targetSec.Key("STORAGE_TYPE").SetValue("local")
				}
			}
		default:
			newTargetSec, _ := rootCfg.GetSection(storageSectionName + "." + targetType)
			if newTargetSec == nil {
				if !IsValidStorageType(StorageType(targetType)) {
					return nil, fmt.Errorf("invalid storage section %s.%q", storageSectionName, targetType)
				}
			} else {
				targetSec = newTargetSec
				if IsValidStorageType(StorageType(targetType)) {
					tp := targetSec.Key("STORAGE_TYPE").String()
					if tp == "" {
						targetSec.Key("STORAGE_TYPE").SetValue(targetType)
					}
				}
			}
		}
	}

	targetType := targetSec.Key("STORAGE_TYPE").String()
	if !IsValidStorageType(StorageType(targetType)) {
		return nil, fmt.Errorf("invalid storage type %q", targetType)
	}

	// extra config section will be read SERVE_DIRECT, PATH, MINIO_BASE_PATH, MINIO_BUCKET to override the targetsec when possible
	extraConfigSec := sec
	if extraConfigSec == nil {
		extraConfigSec = storageNameSec
	}

	var storage Storage
	storage.Type = StorageType(targetType)

	switch targetType {
	case string(LocalStorageType):
		targetPath := ConfigSectionKeyString(targetSec, "PATH", "")
		if targetPath == "" {
			targetPath = AppDataPath
		} else if !filepath.IsAbs(targetPath) {
			targetPath = filepath.Join(AppDataPath, targetPath)
		}

		var fallbackPath string
		if targetSecIsStoragename {
			fallbackPath = targetPath
		} else {
			fallbackPath = filepath.Join(targetPath, name)
		}

		if extraConfigSec == nil {
			storage.Path = fallbackPath
		} else {
			storage.Path = ConfigSectionKeyString(extraConfigSec, "PATH", fallbackPath)
			if !filepath.IsAbs(storage.Path) {
				storage.Path = filepath.Join(targetPath, storage.Path)
			}
		}

	case string(MinioStorageType):
		if err := targetSec.MapTo(&storage.MinioConfig); err != nil {
			return nil, fmt.Errorf("map minio config failed: %v", err)
		}

		storage.MinioConfig.BasePath = name + "/"

		if extraConfigSec != nil {
			storage.MinioConfig.ServeDirect = ConfigSectionKeyBool(extraConfigSec, "SERVE_DIRECT", storage.MinioConfig.ServeDirect)
			storage.MinioConfig.BasePath = ConfigSectionKeyString(extraConfigSec, "MINIO_BASE_PATH", storage.MinioConfig.BasePath)
			storage.MinioConfig.Bucket = ConfigSectionKeyString(extraConfigSec, "MINIO_BUCKET", storage.MinioConfig.Bucket)
		}
	}

	return &storage, nil
}