package service

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"path"
	"strconv"
	"strings"
	"time"

	"github.com/1Panel-dev/1Panel/agent/app/dto"
	"github.com/1Panel-dev/1Panel/agent/app/dto/request"
	"github.com/1Panel-dev/1Panel/agent/app/dto/response"
	"github.com/1Panel-dev/1Panel/agent/app/model"
	"github.com/1Panel-dev/1Panel/agent/app/repo"
	"github.com/1Panel-dev/1Panel/agent/app/task"
	"github.com/1Panel-dev/1Panel/agent/buserr"
	"github.com/1Panel-dev/1Panel/agent/cmd/server/ai"
	"github.com/1Panel-dev/1Panel/agent/cmd/server/nginx_conf"
	"github.com/1Panel-dev/1Panel/agent/constant"
	"github.com/1Panel-dev/1Panel/agent/global"
	"github.com/1Panel-dev/1Panel/agent/utils/common"
	"github.com/1Panel-dev/1Panel/agent/utils/compose"
	"github.com/1Panel-dev/1Panel/agent/utils/docker"
	"github.com/1Panel-dev/1Panel/agent/utils/files"
	"github.com/1Panel-dev/1Panel/agent/utils/nginx"
	"github.com/1Panel-dev/1Panel/agent/utils/nginx/components"
	"github.com/1Panel-dev/1Panel/agent/utils/nginx/parser"
	"github.com/docker/docker/api/types/container"
	"github.com/mattn/go-shellwords"
	"github.com/subosito/gotenv"
	"gopkg.in/yaml.v3"
)

type McpServerService struct{}

const (
	defaultMcpGatewayImageNpx        = "supercorp/supergateway:3.4.3"
	defaultMcpGatewayImageUvx        = "supercorp/supergateway:uvx"
	defaultMcpProtocolVersion        = "2025-06-18"
	mcpOutputTransportSSE            = "sse"
	mcpOutputTransportStreamableHTTP = "streamableHttp"
)

type IMcpServerService interface {
	Page(req request.McpServerSearch) response.McpServersRes
	Detail(req request.McpServerDetail) (response.McpServerDTO, error)
	Create(create request.McpServerCreate) error
	Update(req request.McpServerUpdate) error
	Delete(id uint) error
	Operate(req request.McpServerOperate) error
	SyncStatus(req request.McpServerStatusSync) ([]response.McpServerStatusDTO, error)
	TestConnection(req request.McpServerConnectionTest) (response.McpServerConnectionTestRes, error)
	GetBindDomain() (response.McpBindDomainRes, error)
	BindDomain(req request.McpBindDomain) error
	UpdateBindDomain(req request.McpBindDomainUpdate) error
}

func NewIMcpServerService() IMcpServerService {
	return &McpServerService{}
}

func (m McpServerService) Page(req request.McpServerSearch) response.McpServersRes {
	var (
		res   response.McpServersRes
		items []response.McpServerDTO
	)

	total, data, _ := mcpServerRepo.Page(req.PageInfo.Page, req.PageInfo.PageSize)
	for _, item := range data {
		normalizeMcpServerGateway(&item)
		items = append(items, response.McpServerDTO{
			McpServer:    item,
			Environments: make([]request.Environment, 0),
			Volumes:      make([]request.Volume, 0),
		})
	}
	res.Total = total
	res.Items = items
	return res
}

func (m McpServerService) Detail(req request.McpServerDetail) (response.McpServerDTO, error) {
	mcpServer, err := mcpServerRepo.GetFirst(repo.WithByID(req.ID))
	if err != nil {
		return response.McpServerDTO{}, err
	}
	normalizeMcpServerGateway(mcpServer)
	serverDTO := response.McpServerDTO{
		McpServer:    *mcpServer,
		Environments: make([]request.Environment, 0),
		Volumes:      make([]request.Volume, 0),
	}
	if err := loadMcpServerComposeConfig(&serverDTO); err != nil {
		return response.McpServerDTO{}, err
	}
	return serverDTO, nil
}

func (m McpServerService) Update(req request.McpServerUpdate) error {
	mcpServer, err := mcpServerRepo.GetFirst(repo.WithByID(req.ID))
	if err != nil {
		return err
	}
	if mcpServer.Port != req.Port {
		if err := checkPortExist(req.Port); err != nil {
			return err
		}
	}
	if mcpServer.ContainerName != req.ContainerName {
		if err := checkContainerName(req.ContainerName); err != nil {
			return err
		}
	}
	req.Command = strings.TrimSuffix(req.Command, "\n")
	mcpServer.Name = req.Name
	mcpServer.ContainerName = req.ContainerName
	mcpServer.Port = req.Port
	mcpServer.Command = req.Command
	mcpServer.BaseURL = req.BaseURL
	mcpServer.HostIP = req.HostIP
	mcpServer.OutputTransport = req.OutputTransport
	mcpServer.Type = req.Type
	mcpServer.GatewayImage = req.GatewayImage
	mcpServer.ProtocolVersion = req.ProtocolVersion
	mcpServer.GatewayArgs = req.GatewayArgs
	normalizeMcpServerGateway(mcpServer)
	if req.OutputTransport == mcpOutputTransportSSE {
		mcpServer.SsePath = req.SsePath
	} else {
		mcpServer.StreamableHttpPath = req.StreamableHttpPath
	}
	if err := handleCreateParams(mcpServer, req.Environments, req.Volumes); err != nil {
		return err
	}
	env := handleEnv(mcpServer)
	mcpDir := path.Join(global.Dir.McpDir, mcpServer.Name)
	envPath := path.Join(mcpDir, ".env")
	if err := gotenv.Write(env, envPath); err != nil {
		return err
	}
	dockerComposePath := path.Join(mcpDir, "docker-compose.yml")
	if err := files.NewFileOp().SaveFile(dockerComposePath, mcpServer.DockerCompose, 0644); err != nil {
		return err
	}
	mcpServer.Status = constant.StatusStarting
	if err := mcpServerRepo.Save(mcpServer); err != nil {
		return err
	}
	go runMcpServerTask(mcpServer, req.TaskID, task.TaskUpdate)
	return nil
}

func (m McpServerService) Create(create request.McpServerCreate) error {
	servers, _ := mcpServerRepo.List()
	for _, server := range servers {
		if server.Port == create.Port {
			return buserr.New("ErrPortInUsed")
		}
		if server.ContainerName == create.ContainerName {
			return buserr.New("ErrContainerName")
		}
		if server.Name == create.Name {
			return buserr.New("ErrNameIsExist")
		}
		if server.SsePath == create.SsePath {
			return buserr.New("ErrSsePath")
		}
	}
	create.Command = strings.TrimSuffix(create.Command, "\n")
	if err := checkPortExist(create.Port); err != nil {
		return err
	}
	if err := checkContainerName(create.ContainerName); err != nil {
		return err
	}
	mcpDir := path.Join(global.Dir.McpDir, create.Name)
	mcpServer := &model.McpServer{
		Name:            create.Name,
		ContainerName:   create.ContainerName,
		Port:            create.Port,
		Command:         create.Command,
		Status:          constant.StatusStarting,
		BaseURL:         create.BaseURL,
		Dir:             mcpDir,
		HostIP:          create.HostIP,
		OutputTransport: create.OutputTransport,
		Type:            create.Type,
		GatewayImage:    create.GatewayImage,
		ProtocolVersion: create.ProtocolVersion,
		GatewayArgs:     create.GatewayArgs,
	}
	normalizeMcpServerGateway(mcpServer)
	if create.OutputTransport == mcpOutputTransportSSE {
		mcpServer.SsePath = create.SsePath
	} else {
		mcpServer.StreamableHttpPath = create.StreamableHttpPath
	}

	if err := handleCreateParams(mcpServer, create.Environments, create.Volumes); err != nil {
		return err
	}

	env := handleEnv(mcpServer)
	filesOP := files.NewFileOp()
	if !filesOP.Stat(mcpDir) {
		_ = filesOP.CreateDir(mcpDir, 0644)
	}
	envPath := path.Join(mcpDir, ".env")
	if err := gotenv.Write(env, envPath); err != nil {
		return err
	}
	dockerComposePath := path.Join(mcpDir, "docker-compose.yml")
	if err := filesOP.SaveFile(dockerComposePath, mcpServer.DockerCompose, 0644); err != nil {
		return err
	}
	if err := mcpServerRepo.Create(mcpServer); err != nil {
		return err
	}
	addProxy(mcpServer)
	go runMcpServerTask(mcpServer, create.TaskID, task.TaskCreate)
	return nil
}

func (m McpServerService) Delete(id uint) error {
	mcpServer, err := mcpServerRepo.GetFirst(repo.WithByID(id))
	if err != nil {
		return err
	}
	composePath := path.Join(global.Dir.McpDir, mcpServer.Name, "docker-compose.yml")
	_, _ = compose.Down(composePath)
	_ = files.NewFileOp().DeleteDir(path.Join(global.Dir.McpDir, mcpServer.Name))

	websiteID := GetWebsiteID()
	if websiteID > 0 {
		websiteService := NewIWebsiteService()
		delProxyReq := request.WebsiteProxyDel{
			ID:   websiteID,
			Name: mcpServer.Name,
		}
		_ = websiteService.DeleteProxy(delProxyReq)
	}
	return mcpServerRepo.DeleteBy(repo.WithByID(id))
}

func (m McpServerService) Operate(req request.McpServerOperate) error {
	mcpServer, err := mcpServerRepo.GetFirst(repo.WithByID(req.ID))
	if err != nil {
		return err
	}
	if err := refreshMcpServerFiles(mcpServer); err != nil {
		return err
	}
	composePath := path.Join(mcpServer.Dir, "docker-compose.yml")
	var out string
	switch req.Operate {
	case "start":
		out, err = compose.Up(composePath)
		mcpServer.Status = constant.StatusRunning
	case "stop":
		out, err = compose.Down(composePath)
		mcpServer.Status = constant.StatusStopped
	case "restart":
		out, err = compose.Restart(composePath)
		mcpServer.Status = constant.StatusRunning
	}
	if err != nil {
		mcpServer.Status = constant.StatusError
		mcpServer.Message = out
	}
	return mcpServerRepo.Save(mcpServer)
}

func (m McpServerService) SyncStatus(req request.McpServerStatusSync) ([]response.McpServerStatusDTO, error) {
	var (
		servers []model.McpServer
		err     error
	)
	if len(req.IDs) > 0 {
		servers, err = mcpServerRepo.List(repo.WithByIDs(req.IDs))
	} else {
		servers, err = mcpServerRepo.List()
	}
	if err != nil {
		return nil, err
	}
	if len(servers) == 0 {
		return []response.McpServerStatusDTO{}, nil
	}

	containerNames := make([]string, 0, len(servers))
	for _, server := range servers {
		if server.ContainerName != "" {
			containerNames = append(containerNames, server.ContainerName)
		}
	}
	containerMap := make(map[string]*container.Summary)
	if len(containerNames) > 0 {
		cli, err := docker.NewClient()
		if err != nil {
			return nil, err
		}
		defer cli.Close()
		containers, err := cli.ListContainersByName(containerNames)
		if err != nil {
			return nil, err
		}
		for i := range containers {
			for _, name := range containers[i].Names {
				containerMap[strings.TrimPrefix(name, "/")] = &containers[i]
			}
		}
	}

	res := make([]response.McpServerStatusDTO, 0, len(servers))
	for i := range servers {
		oldStatus := servers[i].Status
		applyMcpContainerStatus(&servers[i], containerMap[servers[i].ContainerName])
		if servers[i].Status != oldStatus {
			if err := mcpServerRepo.Save(&servers[i]); err != nil {
				return nil, err
			}
		}
		res = append(res, response.McpServerStatusDTO{
			ID:      servers[i].ID,
			Status:  servers[i].Status,
			Message: servers[i].Message,
		})
	}
	return res, nil
}

func (m McpServerService) TestConnection(req request.McpServerConnectionTest) (response.McpServerConnectionTestRes, error) {
	mcpServer, err := mcpServerRepo.GetFirst(repo.WithByID(req.ID))
	if err != nil {
		return response.McpServerConnectionTestRes{}, err
	}
	normalizeMcpServerGateway(mcpServer)
	endpoint := buildMcpEndpoint(mcpServer)
	res := response.McpServerConnectionTestRes{
		Endpoint:        endpoint,
		OutputTransport: mcpServer.OutputTransport,
		ProtocolVersion: mcpServer.ProtocolVersion,
	}
	if endpoint == "" {
		res.Message = "empty endpoint"
		return res, nil
	}

	client := &http.Client{Timeout: 10 * time.Second}
	if mcpServer.OutputTransport == mcpOutputTransportSSE {
		httpReq, err := http.NewRequest(http.MethodGet, endpoint, nil)
		if err != nil {
			res.Message = err.Error()
			return res, nil
		}
		httpRes, err := client.Do(httpReq)
		if err != nil {
			res.Message = err.Error()
			return res, nil
		}
		defer httpRes.Body.Close()
		contentType := httpRes.Header.Get("Content-Type")
		if httpRes.StatusCode >= 200 && httpRes.StatusCode < 300 && strings.Contains(contentType, "text/event-stream") {
			res.Success = true
			res.Message = "ok"
			return res, nil
		}
		res.Message = fmt.Sprintf("unexpected response: status=%d content-type=%s", httpRes.StatusCode, contentType)
		return res, nil
	}

	payload := map[string]interface{}{
		"jsonrpc": "2.0",
		"id":      1,
		"method":  "initialize",
		"params": map[string]interface{}{
			"protocolVersion": mcpServer.ProtocolVersion,
			"capabilities":    map[string]interface{}{},
			"clientInfo": map[string]string{
				"name":    "1Panel",
				"version": "1.0.0",
			},
		},
	}
	body, _ := json.Marshal(payload)
	httpReq, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewReader(body))
	if err != nil {
		res.Message = err.Error()
		return res, nil
	}
	httpReq.Header.Set("Content-Type", "application/json")
	httpReq.Header.Set("Accept", "application/json, text/event-stream")
	httpRes, err := client.Do(httpReq)
	if err != nil {
		res.Message = err.Error()
		return res, nil
	}
	defer httpRes.Body.Close()
	res.Success, res.Message = handleMcpConnectionHTTPResponse(httpRes)
	return res, nil
}

func handleMcpConnectionHTTPResponse(httpRes *http.Response) (bool, string) {
	if httpRes.StatusCode >= 200 && httpRes.StatusCode < 300 {
		return true, "ok"
	}
	respBody, _ := io.ReadAll(httpRes.Body)
	message := strings.TrimSpace(string(respBody))
	if message == "" {
		message = httpRes.Status
	}
	return false, message
}

func (m McpServerService) GetBindDomain() (response.McpBindDomainRes, error) {
	var res response.McpBindDomainRes
	websiteID := GetWebsiteID()
	if websiteID == 0 {
		return res, nil
	}
	website, err := websiteRepo.GetFirst(repo.WithByID(websiteID))
	if err != nil {
		return res, nil
	}
	res.WebsiteID = website.ID
	res.Domain = website.PrimaryDomain
	if website.WebsiteSSLID > 0 {
		res.SSLID = website.WebsiteSSLID
		ssl, _ := websiteSSLRepo.GetFirst(repo.WithByID(website.WebsiteSSLID))
		res.AcmeAccountID = ssl.AcmeAccountID
	}
	res.ConnUrl = fmt.Sprintf("%s://%s", strings.ToLower(website.Protocol), website.PrimaryDomain)
	res.AllowIPs = GetAllowIps(website)
	return res, nil

}

func (m McpServerService) BindDomain(req request.McpBindDomain) error {
	nginxInstall, _ := getAppInstallByKey(constant.AppOpenresty)
	if nginxInstall.ID == 0 {
		return buserr.New("ErrOpenrestyInstall")
	}
	var (
		ipList []string
		err    error
	)
	if len(req.IPList) > 0 {
		ipList, err = common.HandleIPList(req.IPList)
		if err != nil {
			return err
		}
	}
	if req.SSLID > 0 {
		ssl, err := websiteSSLRepo.GetFirst(repo.WithByID(req.SSLID))
		if err != nil {
			return err
		}
		if ssl.Pem == "" {
			return buserr.New("ErrSSL")
		}
	}
	group, _ := groupRepo.Get(groupRepo.WithByWebsiteDefault())

	domain, err := ParseDomain(req.Domain)
	if err != nil {
		return err
	}
	if domain.Port == 0 {
		domain.Port = nginxInstall.HttpPort
	}
	createWebsiteReq := request.WebsiteCreate{
		Domains: []request.WebsiteDomain{
			{
				Domain: domain.Domain,
				Port:   domain.Port,
			},
		},
		Alias:          strings.ToLower(req.Domain),
		Type:           constant.Static,
		WebsiteGroupID: group.ID,
	}
	if req.SSLID > 0 {
		createWebsiteReq.WebsiteSSLID = req.SSLID
		createWebsiteReq.EnableSSL = true
	}
	websiteService := NewIWebsiteService()
	if err := websiteService.CreateWebsite(createWebsiteReq); err != nil {
		return err
	}
	website, err := websiteRepo.GetFirst(websiteRepo.WithAlias(strings.ToLower(req.Domain)))
	if err != nil {
		return err
	}
	_ = settingRepo.UpdateOrCreate("MCP_WEBSITE_ID", fmt.Sprintf("%d", website.ID))
	if len(ipList) > 0 {
		if err = ConfigAllowIPs(ipList, website); err != nil {
			return err
		}
	}
	if err = addMCPProxy(website.ID); err != nil {
		return err
	}
	return nil
}

func (m McpServerService) UpdateBindDomain(req request.McpBindDomainUpdate) error {
	nginxInstall, _ := getAppInstallByKey(constant.AppOpenresty)
	if nginxInstall.ID == 0 {
		return buserr.New("ErrOpenrestyInstall")
	}
	var (
		ipList []string
		err    error
	)
	if len(req.IPList) > 0 {
		ipList, err = common.HandleIPList(req.IPList)
		if err != nil {
			return err
		}
	}
	if req.SSLID > 0 {
		ssl, err := websiteSSLRepo.GetFirst(repo.WithByID(req.SSLID))
		if err != nil {
			return err
		}
		if ssl.Pem == "" {
			return buserr.New("ErrSSL")
		}
	}
	websiteService := NewIWebsiteService()
	website, err := websiteRepo.GetFirst(repo.WithByID(req.WebsiteID))
	if err != nil {
		return err
	}
	if err = ConfigAllowIPs(ipList, website); err != nil {
		return err
	}
	if req.SSLID > 0 {
		sslReq := request.WebsiteHTTPSOp{
			WebsiteID:    website.ID,
			Enable:       true,
			Type:         "existed",
			WebsiteSSLID: req.SSLID,
			HttpConfig:   "HTTPSOnly",
		}
		if _, err = websiteService.OpWebsiteHTTPS(context.Background(), sslReq); err != nil {
			return err
		}
	}
	if website.WebsiteSSLID > 0 && req.SSLID == 0 {
		sslReq := request.WebsiteHTTPSOp{
			WebsiteID: website.ID,
			Enable:    false,
		}
		if _, err = websiteService.OpWebsiteHTTPS(context.Background(), sslReq); err != nil {
			return err
		}
	}
	go updateMcpConfig(website.ID)
	return nil
}

func updateMcpConfig(websiteID uint) {
	servers, _ := mcpServerRepo.List()
	if len(servers) == 0 {
		return
	}
	website, _ := websiteRepo.GetFirst(repo.WithByID(websiteID))
	websiteDomain := website.Domains[0]
	var baseUrl string
	if website.Protocol == constant.ProtocolHTTP {
		baseUrl = fmt.Sprintf("http://%s", websiteDomain.Domain)
	} else {
		baseUrl = fmt.Sprintf("https://%s", websiteDomain.Domain)
	}

	go func() {
		for _, server := range servers {
			if server.BaseURL != baseUrl {
				server.BaseURL = baseUrl
				server.HostIP = "127.0.0.1"
				_ = updateMcpServer(&server)
			}
		}
	}()
}

func addProxy(server *model.McpServer) {
	websiteID := GetWebsiteID()
	website, err := websiteRepo.GetFirst(repo.WithByID(websiteID))
	if err != nil {
		global.LOG.Errorf("[mcp] add proxy failed, err: %v", err)
		return
	}
	fileOp := files.NewFileOp()
	includeDir := GetSitePath(website, SiteProxyDir)
	if !fileOp.Stat(includeDir) {
		if err = fileOp.CreateDir(includeDir, 0644); err != nil {
			return
		}
	}
	config, err := parser.NewStringParser(string(nginx_conf.SSE)).Parse()
	if err != nil {
		return
	}
	includePath := path.Join(includeDir, server.Name+".conf")
	config.FilePath = includePath
	directives := config.Directives
	location, ok := directives[0].(*components.Location)
	if !ok {
		return
	}
	var proxyPath string
	if server.OutputTransport == "sse" {
		proxyPath = server.SsePath
	} else {
		proxyPath = server.StreamableHttpPath
	}
	location.UpdateDirective("proxy_pass", []string{fmt.Sprintf("http://127.0.0.1:%d%s", server.Port, proxyPath)})
	location.ChangePath("^~", proxyPath)
	if err = nginx.WriteConfig(config, nginx.IndentedStyle); err != nil {
		global.LOG.Errorf("write config failed, err: %v", buserr.WithErr("ErrUpdateBuWebsite", err))
		return
	}
	nginxInclude := fmt.Sprintf("/www/sites/%s/proxy/*.conf", website.Alias)
	if err = updateNginxConfig(constant.NginxScopeServer, []dto.NginxParam{{Name: "include", Params: []string{nginxInclude}}}, &website); err != nil {
		global.LOG.Errorf("update nginx config failed, err: %v", err)
		return
	}
}

func addMCPProxy(websiteID uint) error {
	servers, _ := mcpServerRepo.List()
	if len(servers) == 0 {
		return nil
	}
	website, err := websiteRepo.GetFirst(repo.WithByID(websiteID))
	if err != nil {
		return err
	}
	fileOp := files.NewFileOp()
	includeDir := GetSitePath(website, SiteProxyDir)
	if !fileOp.Stat(includeDir) {
		if err = fileOp.CreateDir(includeDir, 0644); err != nil {
			return err
		}
	}
	config, err := parser.NewStringParser(string(nginx_conf.SSE)).Parse()
	if err != nil {
		return err
	}
	websiteDomain := website.Domains[0]
	var baseUrl string
	if website.Protocol == constant.ProtocolHTTP {
		baseUrl = fmt.Sprintf("http://%s", websiteDomain.Domain)
	} else {
		baseUrl = fmt.Sprintf("https://%s", websiteDomain.Domain)
	}
	if websiteDomain.Port != 80 && websiteDomain.Port != 443 {
		baseUrl = fmt.Sprintf("%s:%d", baseUrl, websiteDomain.Port)
	}
	for _, server := range servers {
		includePath := path.Join(includeDir, server.Name+".conf")
		config.FilePath = includePath
		directives := config.Directives
		location, ok := directives[0].(*components.Location)
		if !ok {
			err = errors.New("error")
			return err
		}
		var proxyPath string
		if server.OutputTransport == "sse" {
			proxyPath = server.SsePath
		} else {
			proxyPath = server.StreamableHttpPath
		}
		location.UpdateDirective("proxy_pass", []string{fmt.Sprintf("http://127.0.0.1:%d%s", server.Port, proxyPath)})
		location.ChangePath("^~", proxyPath)
		if err = nginx.WriteConfig(config, nginx.IndentedStyle); err != nil {
			return buserr.WithErr("ErrUpdateBuWebsite", err)
		}
		server.BaseURL = baseUrl
		server.HostIP = "127.0.0.1"
		go updateMcpServer(&server)
	}
	nginxInclude := fmt.Sprintf("/www/sites/%s/proxy/*.conf", website.Alias)
	if err = updateNginxConfig(constant.NginxScopeServer, []dto.NginxParam{{Name: "include", Params: []string{nginxInclude}}}, &website); err != nil {
		return err
	}
	return nil
}

func updateMcpServer(mcpServer *model.McpServer) error {
	if err := refreshMcpServerFiles(mcpServer); err != nil {
		return err
	}
	composePath := path.Join(global.Dir.McpDir, mcpServer.Name, "docker-compose.yml")
	_, _ = compose.Down(composePath)
	if _, err := compose.Up(composePath); err != nil {
		mcpServer.Status = constant.StatusError
		mcpServer.Message = err.Error()
	}
	return mcpServerRepo.Save(mcpServer)
}

func refreshMcpServerFiles(mcpServer *model.McpServer) error {
	normalizeMcpServerGateway(mcpServer)
	if err := handleCreateParams(mcpServer, nil, nil); err != nil {
		return err
	}
	env := handleEnv(mcpServer)
	if err := gotenv.Write(env, path.Join(mcpServer.Dir, ".env")); err != nil {
		return err
	}
	composePath := path.Join(mcpServer.Dir, "docker-compose.yml")
	if err := files.NewFileOp().SaveFile(composePath, mcpServer.DockerCompose, 0644); err != nil {
		return err
	}
	return mcpServerRepo.Save(mcpServer)
}

func normalizeMcpServerGateway(mcpServer *model.McpServer) {
	if mcpServer.Type == "" {
		mcpServer.Type = "npx"
	}
	if strings.TrimSpace(mcpServer.GatewayImage) == "" {
		if mcpServer.Type == "uvx" {
			mcpServer.GatewayImage = defaultMcpGatewayImageUvx
		} else {
			mcpServer.GatewayImage = defaultMcpGatewayImageNpx
		}
	}
	if strings.TrimSpace(mcpServer.ProtocolVersion) == "" {
		mcpServer.ProtocolVersion = defaultMcpProtocolVersion
	}
}

var managedMcpGatewayArgs = map[string]struct{}{
	"--":                 {},
	"stdio":              {},
	"sse":                {},
	"streamablehttp":     {},
	"outputtransport":    {},
	"port":               {},
	"baseurl":            {},
	"ssepath":            {},
	"messagepath":        {},
	"streamablehttppath": {},
	"protocolversion":    {},
}

func normalizeMcpGatewayArgName(name string) string {
	if name == "--" || !strings.HasPrefix(name, "--") {
		return name
	}
	name = strings.TrimPrefix(name, "--")
	name = strings.TrimPrefix(name, "no-")
	name = strings.SplitN(name, ".", 2)[0]
	return strings.ToLower(strings.ReplaceAll(name, "-", ""))
}

func parseMcpGatewayArgs(raw string) ([]string, error) {
	if strings.TrimSpace(raw) == "" {
		return nil, nil
	}
	parser := shellwords.NewParser()
	args, err := parser.Parse(raw)
	if err != nil {
		return nil, buserr.New("ErrMcpGatewayArgsParse")
	}
	if parser.Position >= 0 {
		return nil, buserr.New("ErrMcpGatewayArgsParse")
	}
	for _, arg := range args {
		name := strings.SplitN(arg, "=", 2)[0]
		if _, ok := managedMcpGatewayArgs[normalizeMcpGatewayArgName(name)]; ok {
			return nil, buserr.WithName("ErrMcpGatewayArgsConflict", name)
		}
	}
	return args, nil
}

func buildSupergatewayCommand(mcpServer *model.McpServer) ([]string, error) {
	normalizeMcpServerGateway(mcpServer)
	command := []string{
		"--stdio", mcpServer.Command,
		"--outputTransport", mcpServer.OutputTransport,
		"--port", strconv.Itoa(mcpServer.Port),
	}
	if mcpServer.OutputTransport == mcpOutputTransportSSE {
		command = append(command,
			"--baseUrl", mcpServer.BaseURL,
			"--ssePath", mcpServer.SsePath,
			"--messagePath", fmt.Sprintf("%s/messages", mcpServer.SsePath),
		)
	} else {
		command = append(command,
			"--streamableHttpPath", mcpServer.StreamableHttpPath,
			"--protocolVersion", mcpServer.ProtocolVersion,
		)
	}
	gatewayArgs, err := parseMcpGatewayArgs(mcpServer.GatewayArgs)
	if err != nil {
		return nil, err
	}
	return append(command, gatewayArgs...), nil
}

func buildMcpEndpoint(mcpServer *model.McpServer) string {
	if mcpServer.BaseURL == "" {
		return ""
	}
	if mcpServer.OutputTransport == mcpOutputTransportSSE {
		return strings.TrimRight(mcpServer.BaseURL, "/") + "/" + strings.TrimLeft(mcpServer.SsePath, "/")
	}
	return strings.TrimRight(mcpServer.BaseURL, "/") + "/" + strings.TrimLeft(mcpServer.StreamableHttpPath, "/")
}

func loadMcpServerComposeConfig(serverDTO *response.McpServerDTO) error {
	serverDTO.Environments = make([]request.Environment, 0)
	serverDTO.Volumes = make([]request.Volume, 0)
	dir := serverDTO.Dir
	if dir == "" {
		dir = path.Join(global.Dir.McpDir, serverDTO.Name)
	}
	project, err := docker.GetComposeProject(serverDTO.Name, dir, []byte(serverDTO.DockerCompose), []byte(serverDTO.Env), true)
	if err != nil {
		return err
	}
	for _, service := range project.Services {
		if service.Environment != nil {
			for key, value := range service.Environment {
				if value == nil {
					continue
				}
				serverDTO.Environments = append(serverDTO.Environments, request.Environment{
					Key:   key,
					Value: *value,
				})
			}
		}
		if service.Volumes != nil {
			for _, volume := range service.Volumes {
				mode := "rw"
				if volume.ReadOnly {
					mode = "ro"
				}
				serverDTO.Volumes = append(serverDTO.Volumes, request.Volume{
					Source: volume.Source,
					Target: volume.Target,
					Mode:   mode,
				})
			}
		}
	}
	return nil
}

func handleEnv(mcpServer *model.McpServer) gotenv.Env {
	env := make(gotenv.Env)
	env["CONTAINER_NAME"] = mcpServer.ContainerName
	env["COMMAND"] = mcpServer.Command
	env["PANEL_APP_PORT_HTTP"] = strconv.Itoa(mcpServer.Port)
	env["BASE_URL"] = mcpServer.BaseURL
	env["SSE_PATH"] = mcpServer.SsePath
	env["HOST_IP"] = mcpServer.HostIP
	env["STREAMABLE_HTTP_PATH"] = mcpServer.StreamableHttpPath
	env["OUTPUT_TRANSPORT"] = mcpServer.OutputTransport
	env["GATEWAY_IMAGE"] = mcpServer.GatewayImage
	envStr, _ := gotenv.Marshal(env)
	mcpServer.Env = envStr
	return env
}

func handleCreateParams(mcpServer *model.McpServer, environments []request.Environment, volumes []request.Volume) error {
	var composeContent []byte
	if mcpServer.ID == 0 {
		composeContent = ai.DefaultMcpCompose
	} else {
		composeContent = []byte(mcpServer.DockerCompose)
	}
	composeMap := make(map[string]interface{})
	if err := yaml.Unmarshal(composeContent, &composeMap); err != nil {
		return err
	}
	services, serviceValid := composeMap["services"].(map[string]interface{})
	if !serviceValid {
		return buserr.New("ErrFileParse")
	}
	serviceName := ""
	serviceValue := make(map[string]interface{})

	if mcpServer.ID > 0 {
		serviceName = mcpServer.Name
		serviceValue = services[serviceName].(map[string]interface{})
	} else {
		for name, service := range services {
			serviceName = name
			serviceValue = service.(map[string]interface{})
			break
		}
		delete(services, serviceName)
	}
	normalizeMcpServerGateway(mcpServer)
	command, err := buildSupergatewayCommand(mcpServer)
	if err != nil {
		return err
	}
	serviceValue["command"] = command
	serviceValue["image"] = mcpServer.GatewayImage
	serviceValue["ports"] = []string{
		formatComposePortMapping("HOST_IP", "PANEL_APP_PORT_HTTP", "PANEL_APP_PORT_HTTP", ""),
	}
	oldEnv, hasOldEnv := serviceValue["environment"]
	delete(serviceValue, "environment")
	if environments != nil && len(environments) > 0 {
		envMap := make(map[string]string)
		for _, env := range environments {
			envMap[env.Key] = env.Value
		}
		serviceValue["environment"] = envMap
	} else if environments == nil {
		// Preserve existing custom environment when regenerating compose during start/restart.
		if mcpServer.ID > 0 && hasOldEnv {
			serviceValue["environment"] = oldEnv
		}
	}
	oldVolumes, hasOldVolumes := serviceValue["volumes"]
	delete(serviceValue, "volumes")
	if volumes != nil && len(volumes) > 0 {
		volumeList := make([]string, 0)
		for _, volume := range volumes {
			volumeList = append(volumeList, formatComposeVolume(volume.Source, volume.Target, volume.Mode))
		}
		serviceValue["volumes"] = volumeList
	} else if volumes == nil {
		// Preserve existing custom volumes when regenerating compose during start/restart.
		if mcpServer.ID > 0 && hasOldVolumes {
			serviceValue["volumes"] = oldVolumes
		}
	}

	services[mcpServer.Name] = serviceValue
	composeByte, err := yaml.Marshal(composeMap)
	if err != nil {
		return err
	}
	mcpServer.DockerCompose = string(composeByte)
	return nil
}

func runMcpServerTask(mcpServer *model.McpServer, taskID, operate string) {
	mcpTask, err := task.NewTaskWithOps(mcpServer.Name, operate, task.TaskScopeAI, taskID, mcpServer.ID)
	if err != nil {
		mcpServer.Status = constant.StatusError
		mcpServer.Message = err.Error()
		_ = mcpServerRepo.Save(mcpServer)
		return
	}
	mcpTask.AddSubTask(task.GetTaskName(mcpServer.Name, operate, task.TaskScopeAI), func(t *task.Task) error {
		return startMcpWithTask(mcpServer, t)
	}, nil)
	_ = mcpTask.Execute()
}

func startMcp(mcpServer *model.McpServer) {
	_ = startMcpWithTask(mcpServer, nil)
}

func startMcpWithTask(mcpServer *model.McpServer, taskItem *task.Task) error {
	if err := refreshMcpServerFiles(mcpServer); err != nil {
		mcpServer.Status = constant.StatusError
		mcpServer.Message = err.Error()
		_ = mcpServerRepo.Save(mcpServer)
		return err
	}
	composePath := path.Join(global.Dir.McpDir, mcpServer.Name, "docker-compose.yml")
	if mcpServer.Status != constant.StatusNormal {
		_, _ = compose.Down(composePath)
	}
	var err error
	var out string
	if taskItem != nil {
		err = compose.UpWithTask(composePath, taskItem, false)
	} else {
		out, err = compose.Up(composePath)
	}
	if err != nil {
		mcpServer.Status = constant.StatusError
		if out != "" {
			mcpServer.Message = out
		} else {
			mcpServer.Message = err.Error()
		}
		_ = mcpServerRepo.Save(mcpServer)
		return err
	} else {
		mcpServer.Status = constant.StatusRunning
		mcpServer.Message = ""
	}
	return syncMcpServerContainerStatus(mcpServer)
}

func syncMcpServerContainerStatus(mcpServer *model.McpServer) error {
	containerNames := []string{mcpServer.ContainerName}
	cli, err := docker.NewClient()
	if err != nil {
		return err
	}
	defer cli.Close()
	containers, err := cli.ListContainersByName(containerNames)
	if err != nil {
		return err
	}
	if len(containers) == 0 {
		applyMcpContainerStatus(mcpServer, nil)
		return mcpServerRepo.Save(mcpServer)
	}
	applyMcpContainerStatus(mcpServer, &containers[0])
	return mcpServerRepo.Save(mcpServer)
}

func applyMcpContainerStatus(mcpServer *model.McpServer, containerSummary *container.Summary) {
	if containerSummary == nil {
		mcpServer.Status = constant.StatusStopped
		return
	}
	switch containerSummary.State {
	case "exited":
		mcpServer.Status = constant.StatusError
	case "running":
		mcpServer.Status = constant.StatusRunning
	case "paused":
		mcpServer.Status = constant.StatusStopped
	case "restarting":
		mcpServer.Status = constant.StatusRestarting
	default:
		if mcpServer.Status != constant.StatusStarting {
			mcpServer.Status = constant.StatusStopped
		}
	}
}

func GetWebsiteID() uint {
	websiteID, _ := settingRepo.Get(settingRepo.WithByKey("MCP_WEBSITE_ID"))
	if websiteID.Value == "" {
		return 0
	}
	websiteIDUint, _ := strconv.ParseUint(websiteID.Value, 10, 64)
	return uint(websiteIDUint)
}
