package tui

import (
	"albedo/cli/internal/config"
	"albedo/cli/internal/daemon"
	"cmp"
	"errors"
	"fmt"
	"net/url"
	"path"
	"regexp"
	"slices"
	"strings"

	tea "charm.land/bubbletea/v2"
)

// mcpForm adds or edits one MCP server on a single screen. Nothing is written
// until the whole form is saved, so a server that needs credentials gets them
// before its first connection attempt.
type mcpForm struct {
	form
	// Editing names the server being edited; empty for a new server.
	Editing   string
	Transport string // "http" or "stdio"
	// Stored names the secrets the daemon already holds for this server.
	Stored daemon.MCPSecretNames
	// Base keeps fields the form does not edit (enabled, tools, timeouts).
	Base config.MCPServer
	// NameTouched stops the name following the URL or command once typed.
	NameTouched bool
}

const (
	fieldTransport = "transport"
	fieldURL       = "url"
	fieldCommand   = "command"
	fieldName      = "name"
	fieldToken     = "token"
	fieldHeader    = "header"
	fieldValue     = "value"
	fieldEnv       = "env"
)

// mcpFieldLabels renames the fields whose key is not the words a person
// reads; the rest label themselves.
var mcpFieldLabels = map[string]string{
	fieldToken: "bearer token",
	fieldValue: "header value",
}

func newMCPForm(editing string, server config.MCPServer, stored daemon.MCPSecretNames) *mcpForm {
	transport := "http"
	if server.Type == "stdio" {
		transport = "stdio"
	}
	keys := []string{fieldURL, fieldCommand, fieldName, fieldToken, fieldHeader, fieldValue, fieldEnv}
	f := &mcpForm{Editing: editing, Transport: transport, Base: server, Stored: stored, form: newForm(keys, fieldToken, fieldValue, fieldEnv)}
	f.Inputs[fieldURL].SetValue(server.URL)
	f.Inputs[fieldCommand].SetValue(joinCommand(server.Command, server.Args))
	f.Inputs[fieldName].SetValue(editing)
	f.NameTouched = editing != ""
	if stored.BearerToken {
		f.Inputs[fieldToken].Placeholder = "stored · leave blank to keep it, or enter - to remove it"
	}
	if len(stored.Headers) > 0 {
		f.Inputs[fieldHeader].Placeholder = strings.Join(stored.Headers, ", ") + " stored · add or replace one"
	}
	f.Inputs[fieldEnv].Placeholder = "KEY=value KEY2=value (optional)"
	if len(stored.Env) > 0 {
		f.Inputs[fieldEnv].Placeholder = strings.Join(stored.Env, ", ") + " stored · KEY=value adds or replaces"
	}
	f.Focus = 1 // the address: transport already defaults to http
	f.focus(f.current())
	return f
}

// fields lists the rows for the current transport, in order.
func (f *mcpForm) fields() []string {
	if f.Transport == "stdio" {
		return []string{fieldTransport, fieldCommand, fieldName, fieldEnv}
	}
	return []string{fieldTransport, fieldURL, fieldName, fieldToken, fieldHeader, fieldValue}
}

func (f *mcpForm) current() string {
	fields := f.fields()
	return fields[min(f.Focus, len(fields)-1)]
}

// update handles one key or paste; submit reports that the form should be saved.
func (f *mcpForm) update(msg tea.Msg) (submit bool, cmd tea.Cmd) {
	if submit, handled := f.key(msg, f.fields(), 0); handled {
		return submit, nil
	}
	if f.current() == fieldTransport {
		if key, ok := msg.(tea.KeyPressMsg); ok && f.Editing == "" {
			switch key.String() {
			case "left", "right", "space":
				if f.Transport == "http" {
					f.Transport = "stdio"
				} else {
					f.Transport = "http"
				}
				f.suggestName()
			}
		}
		return false, nil
	}
	if f.current() == fieldName && f.Editing != "" {
		return false, nil // renaming would orphan stored credentials
	}
	input := f.Inputs[f.current()]
	before := input.Value()
	next, cmd := input.Update(msg)
	*input = next
	if input.Value() != before {
		switch f.current() {
		case fieldName:
			f.NameTouched = true
		case fieldURL, fieldCommand:
			f.suggestName()
		}
	}
	return false, cmd
}

func (f *mcpForm) suggestName() {
	if f.NameTouched {
		return
	}
	name := ""
	if f.Transport == "http" {
		name = nameFromURL(f.Inputs[fieldURL].Value())
	} else if argv, err := splitCommand(f.Inputs[fieldCommand].Value()); err == nil {
		name = nameFromCommand(argv)
	}
	f.Inputs[fieldName].SetValue(name)
}

// mcpSubmission is a validated form: the server entry and the change to its
// saved credentials.
type mcpSubmission struct {
	Secrets daemon.MCPSecretsPatch
	Name    string
	Server  config.MCPServer
}

func (f *mcpForm) submission(existing []capabilityItem) (mcpSubmission, error) {
	name := f.value(fieldName)
	if !mcpName.MatchString(name) {
		return mcpSubmission{}, errors.New("use 1–64 letters, digits, underscores, or hyphens for the server name")
	}
	if f.Editing == "" && slices.ContainsFunc(existing, func(item capabilityItem) bool { return item.ID == name }) {
		return mcpSubmission{}, fmt.Errorf("a server called %s already exists; choose another name", name)
	}
	server := f.Base
	server.Type = f.Transport
	secrets := daemon.MCPSecretsPatch{}
	if f.Transport == "http" {
		raw := f.value(fieldURL)
		parsed, err := url.Parse(raw)
		if raw == "" || err != nil || (parsed.Scheme != "http" && parsed.Scheme != "https") || parsed.Host == "" {
			return mcpSubmission{}, errors.New("enter a server URL starting with http:// or https://")
		}
		server.URL, server.Command, server.Args, server.CWD = raw, "", nil, ""
		if token := f.value(fieldToken); token == "-" {
			secrets.RemoveBearerToken = true
		} else if token != "" {
			secrets.BearerToken = &token
		}
		header, value := f.value(fieldHeader), f.value(fieldValue)
		switch {
		case header == "" && value == "":
		case !mcpHeaderName.MatchString(header):
			return mcpSubmission{}, errors.New("enter a valid HTTP header name")
		case value == "-":
			secrets.Headers = map[string]*string{header: nil}
		case value == "":
			return mcpSubmission{}, fmt.Errorf("enter a value for %s, or - to remove it", header)
		default:
			secrets.Headers = map[string]*string{header: &value}
		}
		secrets.ClearEnv = true
	} else {
		argv, err := splitCommand(f.Inputs[fieldCommand].Value())
		if err != nil || len(argv) == 0 {
			return mcpSubmission{}, errors.New("enter the command that starts the server")
		}
		server.Command, server.Args, server.URL = argv[0], argv[1:], ""
		entries, err := splitCommand(f.Inputs[fieldEnv].Value())
		if err != nil {
			return mcpSubmission{}, fmt.Errorf("check environment variables: %w", err)
		}
		var env map[string]*string
		for _, entry := range entries {
			key, value, ok := strings.Cut(entry, "=")
			if !ok || !mcpEnvName.MatchString(key) {
				return mcpSubmission{}, errors.New("enter environment variables as KEY=value pairs separated by spaces")
			}
			if env == nil {
				env = map[string]*string{}
			}
			if value == "-" {
				env[key] = nil
			} else {
				env[key] = &value
			}
		}
		if env != nil {
			secrets.Env = env
		}
		secrets.RemoveBearerToken, secrets.ClearHeaders = true, true
	}
	return mcpSubmission{Name: name, Server: server, Secrets: secrets}, nil
}

func (f *mcpForm) view(width int) []string {
	f.fit(width - 16)
	title := "add MCP server"
	if f.Editing != "" {
		title = "edit " + f.Editing
	}
	rows := []string{DefaultStyles.Bold.Render(title)}
	for i, key := range f.fields() {
		value := f.Transport
		if key != fieldTransport {
			value = f.Inputs[key].View()
		} else if f.Editing == "" {
			value = " http  ‹stdio›"
			if f.Transport == "http" {
				value = "‹http›  stdio "
			}
		}
		label := cmp.Or(mcpFieldLabels[key], key)
		rows = append(rows, formRow(i == f.Focus, label, 13, value, width))
	}
	var own []hint
	note := ""
	switch f.current() {
	case fieldTransport:
		if f.Editing == "" {
			own = []hint{{"←→", "http or stdio"}}
		}
	case fieldToken:
		note = "optional · sent as an Authorization: Bearer header"
	case fieldHeader, fieldValue:
		note = "optional custom header, stored privately"
	case fieldEnv:
		note = "optional, stored privately"
	}
	return append(rows, "", formFooter(note, width, own...))
}

var nameUnsafe = regexp.MustCompile(`[^A-Za-z0-9_-]+`)

// nameFromURL suggests a server name from its host: mcp.linear.app → linear.
func nameFromURL(raw string) string {
	parsed, err := url.Parse(strings.TrimSpace(raw))
	if err != nil || parsed.Hostname() == "" {
		return ""
	}
	labels := strings.Split(parsed.Hostname(), ".")
	for len(labels) > 1 && (labels[0] == "mcp" || labels[0] == "api" || labels[0] == "www") {
		labels = labels[1:]
	}
	if len(labels) == 4 && strings.Trim(parsed.Hostname(), "0123456789.") == "" {
		return cleanName("mcp-" + strings.ReplaceAll(parsed.Hostname(), ".", "-"))
	}
	return cleanName(labels[0])
}

// nameFromCommand suggests a name from the package or program a command
// runs: npx -y @modelcontextprotocol/server-filesystem → filesystem.
func nameFromCommand(argv []string) string {
	if len(argv) == 0 {
		return ""
	}
	target := path.Base(argv[0])
	switch target {
	case "npx", "uvx", "bunx", "pnpx", "pipx", "node", "python", "python3", "uv", "docker":
		for _, arg := range argv[1:] {
			if !strings.HasPrefix(arg, "-") && arg != "run" && arg != "tool" {
				target = arg
				break
			}
		}
	}
	target = path.Base(target)
	if at := strings.LastIndex(target, "@"); at > 0 {
		target = target[:at]
	}
	for _, affix := range []string{"server-", "mcp-server-", "mcp-"} {
		target = strings.TrimPrefix(target, affix)
	}
	for _, affix := range []string{"-mcp-server", "-mcp", "-server"} {
		target = strings.TrimSuffix(target, affix)
	}
	return cleanName(target)
}

func cleanName(name string) string {
	name = strings.Trim(nameUnsafe.ReplaceAllString(strings.ToLower(name), "-"), "-_")
	if len(name) > 64 {
		name = name[:64]
	}
	return name
}

// splitCommand splits a command line on spaces, honouring single and double
// quotes and backslash escapes, without invoking a shell.
func splitCommand(line string) ([]string, error) {
	var args []string
	var current strings.Builder
	inArg, quote, escaped := false, rune(0), false
	for _, r := range line {
		switch {
		case escaped:
			current.WriteRune(r)
			escaped = false
		case r == '\\' && quote != '\'':
			escaped, inArg = true, true
		case quote != 0:
			if r == quote {
				quote = 0
			} else {
				current.WriteRune(r)
			}
		case r == '\'' || r == '"':
			quote, inArg = r, true
		case r == ' ' || r == '\t':
			if inArg {
				args = append(args, current.String())
				current.Reset()
				inArg = false
			}
		default:
			current.WriteRune(r)
			inArg = true
		}
	}
	if quote != 0 || escaped {
		return nil, errors.New("unterminated quote")
	}
	if inArg {
		args = append(args, current.String())
	}
	return args, nil
}

func joinCommand(command string, args []string) string {
	if command == "" {
		return ""
	}
	parts := []string{quoteArg(command)}
	for _, arg := range args {
		parts = append(parts, quoteArg(arg))
	}
	return strings.Join(parts, " ")
}

func quoteArg(arg string) string {
	if arg != "" && !strings.ContainsAny(arg, " \t'\"\\") {
		return arg
	}
	return "'" + strings.ReplaceAll(arg, "'", `'\''`) + "'"
}
