/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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 config

import (
	"strconv"
	"time"
)

import (
	"github.com/dubbogo/gost/log/logger"

	perrors "github.com/pkg/errors"
)

import (
	"dubbo.apache.org/dubbo-go/v3/common"
	"dubbo.apache.org/dubbo-go/v3/common/constant"
	"dubbo.apache.org/dubbo-go/v3/metadata"
)

// MetadataReportConfig is app level configuration
type MetadataReportConfig struct {
	Protocol  string            `required:"true"  yaml:"protocol"  json:"protocol,omitempty"`
	Address   string            `required:"true" yaml:"address" json:"address"`
	Username  string            `yaml:"username" json:"username,omitempty"`
	Password  string            `yaml:"password" json:"password,omitempty"`
	Timeout   string            `yaml:"timeout" json:"timeout,omitempty"`
	Group     string            `yaml:"group" json:"group,omitempty"`
	Namespace string            `yaml:"namespace" json:"namespace,omitempty"`
	Params    map[string]string `yaml:"params"  json:"parameters,omitempty"`
	// metadataType of this application is defined by application config, local or remote
	metadataType string
}

func initMetadata(rc *RootConfig) error {
	opts := metadata.NewOptions(
		metadata.WithAppName(rc.Application.Name),
		metadata.WithMetadataType(rc.Application.MetadataType),
		metadata.WithPort(getMetadataPort(rc)),
	)
	if err := opts.Init(); err != nil {
		return err
	}
	return nil
}

func getMetadataPort(rc *RootConfig) int {
	port := rc.Application.MetadataServicePort
	if port == "" {
		protocolConfig, ok := rootConfig.Protocols[constant.DefaultProtocol]
		if ok {
			port = protocolConfig.Port
		} else {
			logger.Warnf("[Metadata Service] Dubbo-go %s version's MetadataService only support dubbo protocol,"+
				"MetadataService will use random port",
				constant.Version)
		}
	}
	if port == "" {
		return 0
	}
	p, err := strconv.Atoi(port)
	if err != nil {
		logger.Error("MetadataService port parse error %v, MetadataService will use random port", err)
		return 0
	}
	return p
}

// Prefix dubbo.consumer
func (*MetadataReportConfig) Prefix() string {
	return constant.MetadataReportPrefix
}

func (mc *MetadataReportConfig) toReportOptions() (*metadata.ReportOptions, error) {
	opts := metadata.NewReportOptions(
		metadata.WithRegistryId(constant.DefaultKey),
		metadata.WithProtocol(mc.Protocol),
		metadata.WithAddress(mc.Address),
		metadata.WithUsername(mc.Username),
		metadata.WithPassword(mc.Password),
		metadata.WithGroup(mc.Group),
		metadata.WithNamespace(mc.Namespace),
		metadata.WithParams(mc.Params),
	)
	if mc.Timeout != "" {
		timeout, err := time.ParseDuration(mc.Timeout)
		if err != nil {
			return nil, err
		}
		metadata.WithTimeout(timeout)(opts)
	}
	return opts, nil
}

func registryToReportOptions(id string, rc *RegistryConfig) (*metadata.ReportOptions, error) {
	opts := metadata.NewReportOptions(
		metadata.WithRegistryId(id),
		metadata.WithProtocol(rc.Protocol),
		metadata.WithAddress(rc.Address),
		metadata.WithUsername(rc.Username),
		metadata.WithPassword(rc.Password),
		metadata.WithGroup(rc.Group),
		metadata.WithNamespace(rc.Namespace),
		metadata.WithParams(rc.Params),
	)
	if rc.Timeout != "" {
		timeout, err := time.ParseDuration(rc.Timeout)
		if err != nil {
			return nil, err
		}
		metadata.WithTimeout(timeout)(opts)
	}
	return opts, nil
}

func (mc *MetadataReportConfig) Init(rc *RootConfig) error {
	if mc == nil {
		return nil
	}
	mc.metadataType = rc.Application.MetadataType
	if mc.Address != "" {
		// if metadata report config is avaible, then init metadata report instance
		opts, err := mc.toReportOptions()
		if err != nil {
			return err
		}
		return opts.Init()
	}
	if len(rc.Registries) > 0 {
		// if metadata report config is not available, then init metadata report instance with registries
		for id, reg := range rc.Registries {
			if isValid(reg.Address) {
				ok, err := strconv.ParseBool(reg.UseAsMetaReport)
				if err != nil {
					return err
				}
				if ok {
					opts, err := registryToReportOptions(id, reg)
					if err != nil {
						return err
					}
					if err = opts.Init(); err != nil {
						return err
					}
				}
			}
		}
	}
	return nil
}

func (mc *MetadataReportConfig) ToUrl() (*common.URL, error) {
	res, err := common.NewURL(mc.Address,
		common.WithUsername(mc.Username),
		common.WithPassword(mc.Password),
		common.WithLocation(mc.Address),
		common.WithProtocol(mc.Protocol),
		common.WithParamsValue(constant.TimeoutKey, mc.Timeout),
		common.WithParamsValue(constant.MetadataReportGroupKey, mc.Group),
		common.WithParamsValue(constant.MetadataReportNamespaceKey, mc.Namespace),
		common.WithParamsValue(constant.MetadataTypeKey, mc.metadataType),
		common.WithParamsValue(constant.ClientNameKey, clientNameID(mc, mc.Protocol, mc.Address)),
	)
	if err != nil || len(res.Protocol) == 0 {
		return nil, perrors.New("Invalid MetadataReport Config.")
	}
	res.SetParam("metadata", res.Protocol)
	for key, val := range mc.Params {
		res.SetParam(key, val)
	}
	return res, nil
}

type MetadataReportConfigBuilder struct {
	metadataReportConfig *MetadataReportConfig
}

func NewMetadataReportConfigBuilder() *MetadataReportConfigBuilder {
	return &MetadataReportConfigBuilder{metadataReportConfig: newEmptyMetadataReportConfig()}
}

func (mrcb *MetadataReportConfigBuilder) SetProtocol(protocol string) *MetadataReportConfigBuilder {
	mrcb.metadataReportConfig.Protocol = protocol
	return mrcb
}

func (mrcb *MetadataReportConfigBuilder) SetAddress(address string) *MetadataReportConfigBuilder {
	mrcb.metadataReportConfig.Address = address
	return mrcb
}

func (mrcb *MetadataReportConfigBuilder) SetUsername(username string) *MetadataReportConfigBuilder {
	mrcb.metadataReportConfig.Username = username
	return mrcb
}

func (mrcb *MetadataReportConfigBuilder) SetPassword(password string) *MetadataReportConfigBuilder {
	mrcb.metadataReportConfig.Password = password
	return mrcb
}

func (mrcb *MetadataReportConfigBuilder) SetTimeout(timeout string) *MetadataReportConfigBuilder {
	mrcb.metadataReportConfig.Timeout = timeout
	return mrcb
}

func (mrcb *MetadataReportConfigBuilder) SetGroup(group string) *MetadataReportConfigBuilder {
	mrcb.metadataReportConfig.Group = group
	return mrcb
}

func (mrcb *MetadataReportConfigBuilder) Build() *MetadataReportConfig {
	return mrcb.metadataReportConfig
}

func newEmptyMetadataReportConfig() *MetadataReportConfig {
	return &MetadataReportConfig{
		Params: make(map[string]string),
	}
}
