// Copyright (c) 2022 Alibaba Group Holding Ltd.
//
// 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 mcpserver

import (
	"encoding/json"
	"fmt"
	"regexp"
	"strings"

	"github.com/nacos-group/nacos-sdk-go/v2/clients"
	"github.com/nacos-group/nacos-sdk-go/v2/clients/config_client"
	"github.com/nacos-group/nacos-sdk-go/v2/clients/naming_client"
	"github.com/nacos-group/nacos-sdk-go/v2/common/constant"
	"github.com/nacos-group/nacos-sdk-go/v2/model"
	"github.com/nacos-group/nacos-sdk-go/v2/vo"
)

const McpServerVersionGroup = "mcp-server-versions"
const McpServerSpecGroup = "mcp-server"
const McpToolSpecGroup = "mcp-tools"
const SystemConfigIdPrefix = "system-"
const CredentialPrefix = "credentials-"
const DefaultNacosListConfigMode = "blur"

const ListMcpServerConfigIdPattern = "*mcp-versions.json"

const DefaultNacosListConfigPageSize = 50

type ServerSpecInfo struct {
	RemoteServerConfig *RemoteServerConfig `json:"remoteServerConfig"`
}

type RemoteServerConfig struct {
	ServiceRef *ServiceRef `json:"serviceRef"`
}

type ServiceRef struct {
	ServiceName string `json:"serviceName"`
	GroupName   string `json:"groupName"`
	NamespaceId string `json:"namespaceId"`
}

type NacosRegistryClient struct {
	namespaceId  string
	configClient config_client.IConfigClient
	namingClient naming_client.INamingClient
	servers      map[string]*ServerContext
}

type VersionedMcpServerInfo struct {
	serverInfo *BasicMcpServerInfo
	version    string
}

type ServerContext struct {
	id                     string
	versionedMcpServerInfo *VersionedMcpServerInfo
	serverChangeListener   McpServerListener
	configsMap             map[string]*ConfigListenerWrap
	serviceInfo            *model.Service
	namingCallback         func(services []model.Instance, err error)
}

type McpServerConfig struct {
	ServerSpecConfig string
	ToolsSpecConfig  string
	ServiceInfo      *model.Service
	Credentials      map[string]interface{}
}

type ConfigListenerWrap struct {
	dataId   string
	group    string
	data     string
	listener func(namespace, group, dataId, data string)
}

type BasicMcpServerInfo struct {
	Name          string `json:"name"`
	Id            string `json:"id"`
	FrontProtocol string `json:"frontProtocol"`
	Protocol      string `json:"protocol"`
}

type VersionsMcpServerInfo struct {
	BasicMcpServerInfo
	LatestPublishedVersion string           `json:"latestPublishedVersion"`
	Versions               []*VersionDetail `json:"versionDetails"`
}

type VersionDetail struct {
	Version  string `json:"version"`
	IsLatest bool   `json:"is_latest"`
}

type McpServerListener func(info *McpServerConfig)

func NewMcpRegistryClient(clientConfig *constant.ClientConfig, serverConfig []constant.ServerConfig, namespaceId string) (*NacosRegistryClient, error) {
	clientParam := vo.NacosClientParam{
		ClientConfig:  clientConfig,
		ServerConfigs: serverConfig,
	}
	configClient, err := clients.NewConfigClient(clientParam)
	namingClient, err := clients.NewNamingClient(clientParam)

	if err != nil {
		return nil, err
	}

	return &NacosRegistryClient{
		namespaceId:  namespaceId,
		configClient: configClient,
		namingClient: namingClient,
		servers:      map[string]*ServerContext{},
	}, nil
}

func (n *NacosRegistryClient) listMcpServerConfigs() ([]model.ConfigItem, error) {
	currentPageNum := 1
	result := make([]model.ConfigItem, 0)
	for {
		configPage, err := n.configClient.SearchConfig(vo.SearchConfigParam{
			Search:   DefaultNacosListConfigMode,
			DataId:   ListMcpServerConfigIdPattern,
			Group:    McpServerVersionGroup,
			PageNo:   currentPageNum,
			PageSize: DefaultNacosListConfigPageSize,
		})

		if err != nil {
			mcpServerLog.Errorf("List mcp server configs for page size %d, page number %d error %v", currentPageNum, DefaultNacosListConfigPageSize, err)
			break
		}

		if configPage == nil {
			mcpServerLog.Errorf("List mcp server configs for page size %d, page number %d null", currentPageNum, DefaultNacosListConfigPageSize)
			break
		}

		result = append(result, configPage.PageItems...)

		if configPage.PageNumber >= configPage.PagesAvailable {
			break
		}

		currentPageNum += 1
	}
	return result, nil
}

// ListMcpServer List all mcp server from nacos mcp registry /**
func (n *NacosRegistryClient) ListMcpServer() ([]BasicMcpServerInfo, error) {
	configs, err := n.listMcpServerConfigs()

	if err != nil {
		return nil, err
	}

	var result []BasicMcpServerInfo
	for _, config := range configs {
		mcpServerBasicConfig, err := n.configClient.GetConfig(vo.ConfigParam{
			Group:  McpServerVersionGroup,
			DataId: config.DataId,
		})

		if err != nil {
			mcpServerLog.Errorf("Get mcp server version config (dataId: %s) error, %v", config.DataId, err)
			continue
		}

		if mcpServerBasicConfig == "" {
			mcpServerLog.Infof("get empty mcp server version config (dataId: %s)", config.DataId)
			continue
		}

		mcpServer := BasicMcpServerInfo{}
		err = json.Unmarshal([]byte(mcpServerBasicConfig), &mcpServer)
		if err != nil {
			mcpServerLog.Errorf("Parse mcp server version config error %v", err)
			continue
		}

		if !isMcpServerShouldBeDiscoveryForGateway(mcpServer) {
			mcpServerLog.Infof("mcp server %s don't need to be discovered for gateway, skip it", mcpServerBasicConfig)
			continue
		}

		result = append(result, mcpServer)
	}
	return result, nil
}

func isMcpServerShouldBeDiscoveryForGateway(info BasicMcpServerInfo) bool {
	return "mcp-sse" == info.FrontProtocol || "mcp-streamable" == info.FrontProtocol
}

// ListenToMcpServer Listen to mcp server config and backend service
func (n *NacosRegistryClient) ListenToMcpServer(id string, listener McpServerListener) error {
	versionConfigId := fmt.Sprintf("%s-mcp-versions.json", id)
	serverVersionConfig, err := n.configClient.GetConfig(vo.ConfigParam{
		Group:  McpServerVersionGroup,
		DataId: versionConfigId,
	})
	if err != nil {
		mcpServerLog.Errorf("Get mcp server(id: %s) version config error, %v", id, err)
	} else {
		mcpServerLog.Infof("Get mcp server(id: %s) version config success, config is:\n %v", id, serverVersionConfig)
	}

	versionConfigCallBack := func(namespace string, group string, dataId string, content string) {
		mcpServerLog.Infof("Call back to mcp server %s", id)
		info := VersionsMcpServerInfo{}
		err = json.Unmarshal([]byte(content), &info)
		if err != nil {
			mcpServerLog.Errorf("Parse mcp server (id: %s) version config callback error, %v", id, err)
			return
		}
		latestVersion := info.LatestPublishedVersion

		ctx := n.servers[id]
		if ctx.versionedMcpServerInfo == nil {
			ctx.versionedMcpServerInfo = &VersionedMcpServerInfo{}
		}
		ctx.versionedMcpServerInfo.serverInfo = &info.BasicMcpServerInfo

		// first time the version is empty so it will trigger the change finally.
		if ctx.versionedMcpServerInfo.version != latestVersion {
			ctx.versionedMcpServerInfo.version = latestVersion
			n.onServerVersionChanged(ctx)
			n.triggerMcpServerChange(id)
		}
	}

	n.servers[id] = &ServerContext{
		id:                   id,
		serverChangeListener: listener,
		configsMap: map[string]*ConfigListenerWrap{
			McpServerVersionGroup: {
				dataId:   versionConfigId,
				group:    McpServerVersionGroup,
				listener: versionConfigCallBack,
			},
		},
	}

	// trigger callback manually
	versionConfigCallBack(n.namespaceId, McpServerVersionGroup, versionConfigId, serverVersionConfig)
	// Listen after get config to avoid multi-callback on same version
	err = n.configClient.ListenConfig(vo.ConfigParam{
		Group:    McpServerVersionGroup,
		DataId:   versionConfigId,
		OnChange: versionConfigCallBack,
	})

	if err != nil {
		return err
	}

	return nil
}

func (n *NacosRegistryClient) onServerVersionChanged(ctx *ServerContext) {
	id := ctx.versionedMcpServerInfo.serverInfo.Id
	version := ctx.versionedMcpServerInfo.version
	configsMap := map[string]string{
		McpServerSpecGroup: fmt.Sprintf("%s-%s-mcp-server.json", id, version),
		McpToolSpecGroup:   fmt.Sprintf("%s-%s-mcp-tools.json", id, version),
	}

	for group, dataId := range configsMap {
		configsKey := fmt.Sprintf(SystemConfigIdPrefix+"%s@@%s", id, group)
		// Only listen to the last version of the server, so we should exist and cancel it first
		if data, exist := ctx.configsMap[configsKey]; exist {
			err := n.cancelListenToConfig(data)
			if err != nil {
				mcpServerLog.Errorf("cancel listen to old config %v error %v", dataId, err)
			}
		}

		configListenerWrap, err := n.ListenToConfig(ctx, dataId, group)
		if err != nil {
			mcpServerLog.Errorf("listen to config %v error %v", dataId, err)
			continue
		}
		ctx.configsMap[configsKey] = configListenerWrap
	}
}

func (n *NacosRegistryClient) triggerMcpServerChange(id string) {
	if context, exist := n.servers[id]; exist {
		if config := mapConfigMapToServerConfig(context); config != nil {
			context.serverChangeListener(config)
		}
	}
}

func mapConfigMapToServerConfig(ctx *ServerContext) *McpServerConfig {
	result := &McpServerConfig{
		Credentials: map[string]interface{}{},
	}
	configMaps := ctx.configsMap
	for key, data := range configMaps {
		if strings.HasPrefix(key, SystemConfigIdPrefix) {
			group := strings.Split(key, "@@")[1]
			if group == McpServerSpecGroup {
				result.ServerSpecConfig = data.data
			} else if group == McpToolSpecGroup {
				result.ToolsSpecConfig = data.data
			}
		} else if strings.HasPrefix(key, CredentialPrefix) {
			credentialId := strings.ReplaceAll(key, CredentialPrefix, "")
			var credData interface{}
			if err := json.Unmarshal([]byte(data.data), &credData); err != nil {
				mcpServerLog.Errorf("parse credential %v error %v", credentialId, err)
				// keep origin data if data is not an object
				result.Credentials[credentialId] = data.data
			} else {
				result.Credentials[credentialId] = credData
			}
		}
	}

	result.ServiceInfo = ctx.serviceInfo
	return result
}

func (n *NacosRegistryClient) replaceTemplateAndExactConfigsItems(ctx *ServerContext, config *ConfigListenerWrap) map[string]*ConfigListenerWrap {
	result := map[string]*ConfigListenerWrap{}
	compile := regexp.MustCompile("\\$\\{nacos\\.([a-zA-Z0-9-_:\\\\.]+/[a-zA-Z0-9-_:\\\\.]+)}")
	allConfigs := compile.FindAllString(config.data, -1)
	allConfigsMap := map[string]string{}
	for _, config := range allConfigs {
		allConfigsMap[config] = config
	}

	newContent := config.data
	for _, data := range allConfigsMap {
		dataIdAndGroup := strings.ReplaceAll(data, "${nacos.", "")
		dataIdAndGroup = dataIdAndGroup[0 : len(dataIdAndGroup)-1]
		dataIdAndGroupArray := strings.Split(dataIdAndGroup, "/")
		dataId := strings.TrimSpace(dataIdAndGroupArray[0])
		group := strings.TrimSpace(dataIdAndGroupArray[1])
		configWrap, err := n.ListenToConfig(ctx, dataId, group)
		if err != nil {
			mcpServerLog.Errorf("extract configs %v from content error %v", dataId, err)
			continue
		}
		result[CredentialPrefix+configWrap.group+"_"+configWrap.dataId] = configWrap
		newContent = strings.Replace(newContent, data, ".config.credentials."+group+"_"+dataId, -1)
	}

	config.data = newContent
	return result
}

func (n *NacosRegistryClient) resetNacosTemplateConfigs(ctx *ServerContext, config *ConfigListenerWrap) {
	newCredentials := n.replaceTemplateAndExactConfigsItems(ctx, config)

	credentialsNeedDelete := []string{}

	// cancel all old config listener
	for key, wrap := range ctx.configsMap {
		if strings.HasPrefix(key, CredentialPrefix) {
			if _, ok := newCredentials[key]; !ok {
				credentialsNeedDelete = append(credentialsNeedDelete, key)
				err := n.cancelListenToConfig(wrap)
				if err != nil {
					mcpServerLog.Errorf("cancel listen to old credential listener error %v", err)
					continue
				}
			}
		}
	}

	for _, credentialKey := range credentialsNeedDelete {
		delete(ctx.configsMap, credentialKey)
	}

	for _, data := range newCredentials {
		ctx.configsMap[CredentialPrefix+data.group+"_"+data.dataId] = data
	}
}

func (n *NacosRegistryClient) refreshServiceListenerIfNeeded(ctx *ServerContext, serverConfig string) {
	var serverInfo ServerSpecInfo
	err := json.Unmarshal([]byte(serverConfig), &serverInfo)
	if err != nil {
		mcpServerLog.Errorf("parse server config error %v", err)
		return
	}

	if serverInfo.RemoteServerConfig != nil && serverInfo.RemoteServerConfig.ServiceRef != nil {
		ref := serverInfo.RemoteServerConfig.ServiceRef
		if ctx.serviceInfo != nil {
			if ctx.serviceInfo.Name == ref.ServiceName && ctx.serviceInfo.GroupName == ref.GroupName {
				return
			}

			err := n.namingClient.Unsubscribe(&vo.SubscribeParam{
				GroupName:         ctx.serviceInfo.GroupName,
				ServiceName:       ctx.serviceInfo.Name,
				SubscribeCallback: ctx.namingCallback,
			})
			if err != nil {
				mcpServerLog.Errorf("unsubscribe service error:%v, groupName:%s, serviceName:%s", err, ctx.serviceInfo.GroupName, ctx.serviceInfo.Name)
			}
		}

		service, err := n.namingClient.GetService(vo.GetServiceParam{
			GroupName:   ref.GroupName,
			ServiceName: ref.ServiceName,
		})

		if err != nil {
			mcpServerLog.Errorf("get service error:%v, groupName:%s, serviceName:%s", err, ref.GroupName, ref.ServiceName)
			return
		}

		ctx.serviceInfo = &service

		if ctx.namingCallback == nil {
			ctx.namingCallback = func(services []model.Instance, err error) {
				if ctx.serviceInfo == nil {
					ctx.serviceInfo = &model.Service{
						GroupName: ctx.serviceInfo.GroupName,
						Name:      ctx.serviceInfo.Name,
					}
				}

				ctx.serviceInfo.Name = ref.ServiceName
				ctx.serviceInfo.GroupName = ref.GroupName
				ctx.serviceInfo.Hosts = services
				n.triggerMcpServerChange(ctx.id)
			}
		}

		err = n.namingClient.Subscribe(&vo.SubscribeParam{
			GroupName:         ctx.serviceInfo.GroupName,
			ServiceName:       ctx.serviceInfo.Name,
			SubscribeCallback: ctx.namingCallback,
		})
		if err != nil {
			mcpServerLog.Errorf("subscribe service error:%v, groupName:%s, serviceName:%s", err, ctx.serviceInfo.GroupName, ctx.serviceInfo.Name)
		}
	}
}

func (n *NacosRegistryClient) ListenToConfig(ctx *ServerContext, dataId string, group string) (*ConfigListenerWrap, error) {
	wrap := ConfigListenerWrap{
		dataId: dataId,
		group:  group,
	}

	configListener := func(namespace, group, dataId, data string) {
		if ctx.serverChangeListener != nil && wrap.data != data {
			wrap.data = data

			if group == McpToolSpecGroup {
				n.resetNacosTemplateConfigs(ctx, &wrap)
			} else if group == McpServerSpecGroup {
				n.refreshServiceListenerIfNeeded(ctx, data)
			}

			n.triggerMcpServerChange(ctx.versionedMcpServerInfo.serverInfo.Id)
		}
	}

	config, err := n.configClient.GetConfig(vo.ConfigParam{
		DataId: dataId,
		Group:  group,
	})

	if err != nil {
		return nil, err
	}

	wrap.listener = configListener
	wrap.data = config
	if group == McpToolSpecGroup {
		n.resetNacosTemplateConfigs(ctx, &wrap)
	} else if group == McpServerSpecGroup {
		n.refreshServiceListenerIfNeeded(ctx, wrap.data)
	}

	err = n.configClient.ListenConfig(vo.ConfigParam{
		DataId:   dataId,
		Group:    group,
		OnChange: configListener,
	})
	if err != nil {
		return nil, err
	}

	return &wrap, nil
}

func (n *NacosRegistryClient) cancelListenToConfig(wrap *ConfigListenerWrap) error {
	return n.configClient.CancelListenConfig(vo.ConfigParam{
		DataId:   wrap.dataId,
		Group:    wrap.group,
		OnChange: wrap.listener,
	})
}

func (n *NacosRegistryClient) CancelListenToServer(id string) error {
	if server, exist := n.servers[id]; exist && server != nil {
		defer delete(n.servers, id)

		for _, wrap := range server.configsMap {
			if wrap != nil {
				err := n.configClient.CancelListenConfig(vo.ConfigParam{
					DataId:   wrap.dataId,
					Group:    wrap.group,
					OnChange: wrap.listener,
				})

				if err != nil {
					mcpServerLog.Errorf("cancel listen config error:%v, dataId:%s, group:%s", err, wrap.dataId, wrap.group)
					continue
				}
			}
		}

		if server.serviceInfo != nil {
			err := n.namingClient.Unsubscribe(&vo.SubscribeParam{
				GroupName:         server.serviceInfo.GroupName,
				ServiceName:       server.serviceInfo.Name,
				SubscribeCallback: server.namingCallback,
			})
			if err != nil {
				mcpServerLog.Errorf("unsubscribe service error:%v, groupName:%s, serviceName:%s", err, server.serviceInfo.GroupName, server.serviceInfo.Name)
				return err
			}
		}
	}
	return nil
}

func (n *NacosRegistryClient) CloseClient() {
	n.namingClient.CloseClient()
	n.configClient.CloseClient()
}
