package profile

import (
	"fmt"
	"log/slog"
	"net/url"
	"os"
	"path/filepath"
	"regexp"
	"sort"
	"strings"
	"time"
)

const mcpServersFileName = "mcp-servers.yaml"

// MCPServerTransport identifies the MCP server transport protocol.
type MCPServerTransport string

const (
	// MCPServerTransportHTTP indicates an HTTP/SSE MCP server.
	MCPServerTransportHTTP MCPServerTransport = "http"
	// MCPServerTransportStdio indicates a stdio-based MCP server.
	MCPServerTransportStdio MCPServerTransport = "stdio"
)

// MCPServerManager provides CRUD operations for user-managed external MCP servers.
type MCPServerManager struct {
	mgr *Manager
}

type mcpServersFile struct {
	Servers []MCPServer `yaml:"servers"`
}

// MCPServer describes one AgentTeam-managed external MCP server.
type MCPServer struct {
	ID          string             `yaml:"id"`
	DisplayName string             `yaml:"display_name,omitempty"`
	Description string             `yaml:"description,omitempty"`
	Enabled     bool               `yaml:"enabled"`
	Transport   MCPServerTransport `yaml:"transport"`
	HTTP        *MCPHTTPConfig     `yaml:"http,omitempty"`
	Stdio       *MCPStdioConfig    `yaml:"stdio,omitempty"`
	CreatedAt   *time.Time         `yaml:"created_at,omitempty"`
	UpdatedAt   *time.Time         `yaml:"updated_at,omitempty"`
	LastCheck   *MCPServerCheck    `yaml:"last_check,omitempty"`
}

// MCPHTTPConfig holds HTTP transport configuration for an MCP server.
// Headers entries use "Name: Value" format with $ENV_VAR expansion support.
type MCPHTTPConfig struct {
	URL     string   `yaml:"url"`
	Headers []string `yaml:"headers,omitempty"`
}

// MCPStdioConfig holds stdio transport configuration for an MCP server.
// Env entries use "NAME=VALUE" format with $ENV_VAR expansion support.
type MCPStdioConfig struct {
	Command string   `yaml:"command"`
	Args    []string `yaml:"args,omitempty"`
	Env     []string `yaml:"env,omitempty"`
	CWD     string   `yaml:"cwd,omitempty"`
}

// MCPServerCheck records the result of an MCP server connectivity test.
type MCPServerCheck struct {
	OK        bool             `yaml:"ok" json:"ok"`
	Stage     string           `yaml:"stage,omitempty" json:"stage,omitempty"`
	Message   string           `yaml:"message,omitempty" json:"message,omitempty"`
	CheckedAt time.Time        `yaml:"checked_at" json:"checkedAt"`
	Tools     []MCPToolSummary `yaml:"tools,omitempty" json:"tools,omitempty"`
}

// MCPToolSummary describes a single tool advertised by an MCP server.
type MCPToolSummary struct {
	Name        string `yaml:"name" json:"name"`
	Description string `yaml:"description,omitempty" json:"description,omitempty"`
}

var (
	mcpServerIDPattern  = regexp.MustCompile(`^[a-z][a-z0-9_]{1,62}$`)
	envNamePattern      = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`)
	headerNamePattern   = regexp.MustCompile(`^[!#$%&'*+.^_` + "`" + `|~0-9A-Za-z-]+$`)
	reservedMCPServerID = map[string]struct{}{
		"agentteam": {},
		"app":       {},
	}
)

// Get returns the MCP server with the given ID.
func (mm *MCPServerManager) Get(id string) (MCPServer, bool) {
	mm.mgr.mu.RLock()
	defer mm.mgr.mu.RUnlock()
	server, ok := mm.mgr.mcpServers[strings.TrimSpace(id)]
	if !ok {
		return MCPServer{}, false
	}
	return *server, true
}

// All returns all configured external MCP servers sorted by ID.
func (mm *MCPServerManager) All() []MCPServer {
	mm.mgr.mu.RLock()
	defer mm.mgr.mu.RUnlock()
	return sortedMCPServers(mm.mgr.mcpServers)
}

// Add validates and persists a new external MCP server.
func (mm *MCPServerManager) Add(server MCPServer) (MCPServer, error) {
	server.normalize()
	if server.ID == "" {
		return MCPServer{}, fmt.Errorf("mcp server id is required")
	}
	now := time.Now().UTC()
	server.CreatedAt = &now
	server.UpdatedAt = &now
	if err := server.Validate(); err != nil {
		return MCPServer{}, err
	}

	mm.mgr.mu.Lock()
	defer mm.mgr.mu.Unlock()
	return server, withConfigDirLock(mm.mgr.configDir, func() error {
		servers, err := loadMCPServersForMutation(mm.mgr)
		if err != nil {
			return err
		}
		if _, exists := servers[server.ID]; exists {
			return fmt.Errorf("mcp server %q already exists", server.ID)
		}
		servers[server.ID] = &server
		if err := mm.saveMCPServersSnapshotLocked(servers); err != nil {
			return err
		}
		mm.mgr.mcpServers = servers
		return nil
	})
}

// Update validates and persists an existing external MCP server.
func (mm *MCPServerManager) Update(id string, incoming MCPServer) (MCPServer, error) {
	id = strings.TrimSpace(id)
	incoming.ID = id
	incoming.normalize()

	mm.mgr.mu.Lock()
	defer mm.mgr.mu.Unlock()
	var updated MCPServer
	err := withConfigDirLock(mm.mgr.configDir, func() error {
		servers, err := loadMCPServersForMutation(mm.mgr)
		if err != nil {
			return err
		}
		existing, exists := servers[id]
		if !exists {
			return fmt.Errorf("mcp server %q not found", id)
		}
		incoming.CreatedAt = existing.CreatedAt
		now := time.Now().UTC()
		incoming.UpdatedAt = &now
		incoming.LastCheck = existing.LastCheck
		if err := incoming.Validate(); err != nil {
			return err
		}
		servers[id] = &incoming
		if err := mm.saveMCPServersSnapshotLocked(servers); err != nil {
			return err
		}
		mm.mgr.mcpServers = servers
		updated = incoming
		return nil
	})
	return updated, err
}

// Delete removes an external MCP server by ID.
func (mm *MCPServerManager) Delete(id string) error {
	id = strings.TrimSpace(id)
	mm.mgr.mu.Lock()
	defer mm.mgr.mu.Unlock()
	return withConfigDirLock(mm.mgr.configDir, func() error {
		servers, err := loadMCPServersForMutation(mm.mgr)
		if err != nil {
			return err
		}
		if _, exists := servers[id]; !exists {
			return fmt.Errorf("mcp server %q not found", id)
		}
		delete(servers, id)
		if err := mm.saveMCPServersSnapshotLocked(servers); err != nil {
			return err
		}
		mm.mgr.mcpServers = servers
		return nil
	})
}

// SetLastCheck stores the latest test result for an external MCP server.
func (mm *MCPServerManager) SetLastCheck(id string, check MCPServerCheck) (MCPServer, error) {
	id = strings.TrimSpace(id)
	mm.mgr.mu.Lock()
	defer mm.mgr.mu.Unlock()
	var updated MCPServer
	err := withConfigDirLock(mm.mgr.configDir, func() error {
		servers, err := loadMCPServersForMutation(mm.mgr)
		if err != nil {
			return err
		}
		server, exists := servers[id]
		if !exists {
			return fmt.Errorf("mcp server %q not found", id)
		}
		check.CheckedAt = check.CheckedAt.UTC()
		server.LastCheck = &check
		now := time.Now().UTC()
		server.UpdatedAt = &now
		if err := mm.saveMCPServersSnapshotLocked(servers); err != nil {
			return err
		}
		mm.mgr.mcpServers = servers
		updated = *server
		return nil
	})
	return updated, err
}

func (mm *MCPServerManager) saveMCPServersSnapshotLocked(servers map[string]*MCPServer) error {
	return saveMCPServersFileLocked(sortedMCPServers(servers), mm.mgr.filePath(mcpServersFileName), mm.mgr)
}

func loadMCPServersForMutation(m *Manager) (map[string]*MCPServer, error) {
	mf := &mcpServersFile{}
	if err := loadYAMLFile(m.filePath(mcpServersFileName), mf); err != nil {
		return nil, err
	}
	return mcpServersSliceToMap(mf.Servers), nil
}

func reloadMCPServers(m *Manager) {
	mf := &mcpServersFile{}
	if err := loadYAMLFile(m.filePath(mcpServersFileName), mf); err != nil {
		slog.Debug("profile: load mcp servers", "error", err)
	}
	m.mcpServers = mcpServersSliceToMap(mf.Servers)
}

// ReloadMCPServers reloads only mcp-servers.yaml from disk.
func (m *Manager) ReloadMCPServers() {
	m.mu.Lock()
	defer m.mu.Unlock()
	reloadMCPServers(m)
	slog.Info("profile watcher: MCP servers reloaded", "count", len(m.mcpServers))
}

func mcpServersSliceToMap(items []MCPServer) map[string]*MCPServer {
	servers := make(map[string]*MCPServer, len(items))
	for i := range items {
		server := items[i]
		server.normalize()
		servers[server.ID] = &server
	}
	return servers
}

func sortedMCPServers(servers map[string]*MCPServer) []MCPServer {
	result := make([]MCPServer, 0, len(servers))
	for _, server := range servers {
		result = append(result, *server)
	}
	sort.Slice(result, func(i, j int) bool { return result[i].ID < result[j].ID })
	return result
}

func saveMCPServersFileLocked(servers []MCPServer, path string, mgr *Manager) error {
	return saveYAMLFileLockedMode(mcpServersFile{Servers: servers}, path, mgr, 0o600)
}

func (s *MCPServer) normalize() {
	s.ID = strings.TrimSpace(s.ID)
	s.DisplayName = strings.TrimSpace(s.DisplayName)
	s.Description = strings.TrimSpace(s.Description)
	s.Transport = MCPServerTransport(strings.TrimSpace(string(s.Transport)))
	if s.HTTP != nil {
		s.HTTP.URL = strings.TrimSpace(s.HTTP.URL)
		for i, h := range s.HTTP.Headers {
			s.HTTP.Headers[i] = strings.TrimSpace(h)
		}
	}
	if s.Stdio != nil {
		s.Stdio.Command = strings.TrimSpace(s.Stdio.Command)
		s.Stdio.CWD = strings.TrimSpace(s.Stdio.CWD)
		for i, e := range s.Stdio.Env {
			s.Stdio.Env[i] = strings.TrimSpace(e)
		}
	}
}

// Validate checks the MCP server configuration for errors.
func (s MCPServer) Validate() error {
	if !mcpServerIDPattern.MatchString(s.ID) {
		return fmt.Errorf("mcp server id must match %s", mcpServerIDPattern.String())
	}
	if _, reserved := reservedMCPServerID[s.ID]; reserved {
		return fmt.Errorf("mcp server id %q is reserved", s.ID)
	}
	if s.DisplayName == "" {
		return fmt.Errorf("display name is required")
	}
	switch s.Transport {
	case MCPServerTransportHTTP:
		if s.HTTP == nil {
			return fmt.Errorf("http config is required")
		}
		if err := validateMCPHTTPConfig(*s.HTTP); err != nil {
			return err
		}
	case MCPServerTransportStdio:
		if s.Stdio == nil {
			return fmt.Errorf("stdio config is required")
		}
		if err := validateMCPStdioConfig(*s.Stdio); err != nil {
			return err
		}
	default:
		return fmt.Errorf("unsupported MCP transport %q", s.Transport)
	}
	return nil
}

func validateMCPHTTPConfig(cfg MCPHTTPConfig) error {
	parsed, err := url.Parse(cfg.URL)
	if err != nil || parsed == nil || parsed.Host == "" {
		return fmt.Errorf("http url is invalid")
	}
	if parsed.Scheme != "http" && parsed.Scheme != "https" {
		return fmt.Errorf("http url must use http or https")
	}
	seen := make(map[string]struct{}, len(cfg.Headers))
	for _, entry := range cfg.Headers {
		name, value, ok := strings.Cut(entry, ":")
		if !ok {
			return fmt.Errorf("header %q must contain ':'", entry)
		}
		name = strings.TrimSpace(name)
		value = strings.TrimSpace(value)
		if !headerNamePattern.MatchString(name) || strings.ContainsAny(name, ":\r\n") {
			return fmt.Errorf("header name %q is invalid", name)
		}
		if strings.ContainsAny(value, "\r\n") {
			return fmt.Errorf("header %q value must not contain CR/LF", name)
		}
		if value == "" {
			return fmt.Errorf("header %q value is required", name)
		}
		nameKey := strings.ToLower(name)
		if _, ok := seen[nameKey]; ok {
			return fmt.Errorf("header %q is duplicated", name)
		}
		seen[nameKey] = struct{}{}
	}
	return nil
}

func validateMCPStdioConfig(cfg MCPStdioConfig) error {
	if cfg.Command == "" {
		return fmt.Errorf("stdio command is required")
	}
	if strings.ContainsAny(cfg.Command, "\x00") {
		return fmt.Errorf("stdio command contains invalid NUL byte")
	}
	for _, arg := range cfg.Args {
		if strings.ContainsAny(arg, "\x00") {
			return fmt.Errorf("stdio arg contains invalid NUL byte")
		}
	}
	if cfg.CWD != "" {
		info, err := os.Stat(cfg.CWD)
		if err != nil {
			return fmt.Errorf("stdio cwd is invalid: %w", err)
		}
		if !info.IsDir() {
			return fmt.Errorf("stdio cwd must be a directory")
		}
	}
	seen := make(map[string]struct{}, len(cfg.Env))
	for _, entry := range cfg.Env {
		name, value, ok := strings.Cut(entry, "=")
		if !ok {
			return fmt.Errorf("env %q must contain '='", entry)
		}
		name = strings.TrimSpace(name)
		value = strings.TrimSpace(value)
		if !envNamePattern.MatchString(name) {
			return fmt.Errorf("env name %q is invalid", name)
		}
		if _, ok := seen[name]; ok {
			return fmt.Errorf("env %q is duplicated", name)
		}
		seen[name] = struct{}{}
		if value == "" {
			return fmt.Errorf("env %q value is required", name)
		}
	}
	return nil
}

// Redacted returns a copy safe for API responses. Header and environment
// values are write-only through the local API so credentials do not enter the
// Desktop WebView or its network inspector.
func (s MCPServer) Redacted() MCPServer {
	copyServer := s
	if s.HTTP != nil {
		httpCopy := *s.HTTP
		httpCopy.Headers = nil
		copyServer.HTTP = &httpCopy
	}
	if s.Stdio != nil {
		stdioCopy := *s.Stdio
		stdioCopy.Args = append([]string(nil), s.Stdio.Args...)
		stdioCopy.Env = nil
		copyServer.Stdio = &stdioCopy
	}
	return copyServer
}

// ResolvedEnv returns the stdio environment entries with $ENV_VAR references
// expanded from the runtime-visible environment. Each entry is in
// "NAME=VALUE" format.
func (cfg MCPStdioConfig) ResolvedEnv() []string {
	result := make([]string, 0, len(cfg.Env))
	for _, entry := range cfg.Env {
		name, value, ok := strings.Cut(entry, "=")
		if !ok {
			continue
		}
		result = append(result, name+"="+ExpandRuntimeEnv(value))
	}
	return result
}

// ResolveHeaders returns the HTTP header entries with $ENV_VAR references
// expanded from the runtime-visible environment. Each entry is in
// "Name: Value" format with the value portion trimmed and expanded.
func (cfg MCPHTTPConfig) ResolveHeaders() []string {
	result := make([]string, 0, len(cfg.Headers))
	for _, entry := range cfg.Headers {
		name, value, ok := strings.Cut(entry, ":")
		if !ok {
			continue
		}
		result = append(result, strings.TrimSpace(name)+": "+ExpandRuntimeEnv(strings.TrimSpace(value)))
	}
	return result
}

func mcpServersFilePath(configDir string) string {
	return filepath.Join(configDir, mcpServersFileName)
}
