// Copyright Project Harbor Authors
//
// 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 systeminfo

import (
	"context"
	"fmt"
	"io"
	"os"
	"strings"
	"time"

	"github.com/goharbor/harbor/src/common"
	"github.com/goharbor/harbor/src/common/utils"
	"github.com/goharbor/harbor/src/lib/config"
	"github.com/goharbor/harbor/src/lib/config/models"
	"github.com/goharbor/harbor/src/lib/errors"
	"github.com/goharbor/harbor/src/lib/log"
	"github.com/goharbor/harbor/src/pkg/systeminfo"
	"github.com/goharbor/harbor/src/pkg/systeminfo/imagestorage"
	"github.com/goharbor/harbor/src/pkg/version"
)

const defaultRootCert = "/etc/core/ca/ca.crt"

// for UT only
var testRootCertPath = ""

// Ctl is the default instance of the package
var Ctl = NewController()

// Data wraps common systeminfo data
type Data struct {
	AuthMode          string
	PrimaryAuthMode   bool
	SelfRegistration  bool
	HarborVersion     string
	BannerMessage     string
	AuthProxySettings *models.HTTPAuthProxy
	Protected         *protectedData
	OIDCProviderName  string
}

type protectedData struct {
	CurrentTime                 time.Time
	RegistryURL                 string
	ExtURL                      string
	ProjectCreationRestrict     string
	HasCARoot                   bool
	RegistryStorageProviderName string
	ReadOnly                    bool
	NotificationEnable          bool
}

// Options provide a set of attributes to control what info should be returned
type Options struct {
	// WithProtectedInfo controls if the protected info, which are considered to be sensitive, should be returned
	WithProtectedInfo bool
}

// Controller defines the methods needed for systeminfo API
type Controller interface {

	// GetInfo consolidates the info of the system by checking settings in DB and env vars
	GetInfo(ctx context.Context, opt Options) (*Data, error)

	// GetCapacity returns total and free space of the storage in byte
	GetCapacity(ctx context.Context) (*imagestorage.Capacity, error)

	// GetCA returns a ReadCloser of Harbor's CA if it's configured and accessible from Harbor core
	GetCA(ctx context.Context) (io.ReadCloser, error)
}

type controller struct{}

func (c *controller) GetInfo(ctx context.Context, opt Options) (*Data, error) {
	logger := log.GetLogger(ctx)
	cfg, err := config.GetSystemCfg(ctx)
	if err != nil {
		logger.Errorf("Error occurred getting config: %v", err)
		return nil, err
	}
	mgr := config.GetCfgManager(ctx)
	err = mgr.Load(ctx)
	if err != nil {
		logger.Errorf("Error occurred loading config: %v", err)
		return nil, err
	}
	res := &Data{
		AuthMode:         utils.SafeCastString(cfg[common.AUTHMode]),
		PrimaryAuthMode:  utils.SafeCastBool(cfg[common.PrimaryAuthMode]),
		SelfRegistration: utils.SafeCastBool(cfg[common.SelfRegistration]),
		HarborVersion:    fmt.Sprintf("%s-%s", version.ReleaseVersion, version.GitCommit),
		BannerMessage:    utils.SafeCastString(mgr.Get(ctx, common.BannerMessage).GetString()),
		OIDCProviderName: OIDCProviderName(cfg),
	}
	if res.AuthMode == common.HTTPAuth {
		if s, err := config.HTTPAuthProxySetting(ctx); err == nil {
			res.AuthProxySettings = s
		} else {
			logger.Warningf("Failed to get auth proxy setting, error: %v", err)
		}
	}

	if !opt.WithProtectedInfo {
		return res, nil
	}
	extURL := cfg[common.ExtEndpoint].(string)
	var registryURL string
	if l := strings.Split(extURL, "://"); len(l) > 1 {
		registryURL = l[1]
	} else {
		registryURL = l[0]
	}
	_, caStatErr := os.Stat(defaultRootCert)
	enableCADownload := caStatErr == nil && strings.HasPrefix(extURL, "https://")
	res.Protected = &protectedData{
		CurrentTime:                 time.Now(),
		ReadOnly:                    config.ReadOnly(ctx),
		ExtURL:                      extURL,
		RegistryURL:                 registryURL,
		HasCARoot:                   enableCADownload,
		ProjectCreationRestrict:     utils.SafeCastString(cfg[common.ProjectCreationRestriction]),
		RegistryStorageProviderName: utils.SafeCastString(cfg[common.RegistryStorageProviderName]),
		NotificationEnable:          utils.SafeCastBool(cfg[common.NotificationEnable]),
	}
	return res, nil
}

func OIDCProviderName(cfg map[string]interface{}) string {
	authMode := utils.SafeCastString(cfg[common.AUTHMode])
	if authMode != common.OIDCAuth {
		return ""
	}
	return utils.SafeCastString(cfg[common.OIDCName])
}

func (c *controller) GetCapacity(_ context.Context) (*imagestorage.Capacity, error) {
	systeminfo.Init()
	return imagestorage.GlobalDriver.Cap()
}

func (c *controller) GetCA(ctx context.Context) (io.ReadCloser, error) {
	logger := log.GetLogger(ctx)
	path := defaultRootCert
	if len(testRootCertPath) > 0 {
		path = testRootCertPath
	}
	_, err := os.Stat(path)
	if err == nil {
		return os.Open(path)
	} else if os.IsNotExist(err) {
		return nil, errors.NotFoundError(fmt.Errorf("cert not found in path: %s", path))
	}
	// else
	logger.Errorf("Failed to stat the cert, path: %s, error: %v", path, err)
	return nil, err
}

// NewController return an instance of controller
func NewController() Controller {
	return &controller{}
}
