package cmd

import (
	"fmt"
	"os"
	"strings"
	"text/tabwriter"

	"github.com/spf13/cobra"
	"go.yaml.in/yaml/v3"

	"github.com/tidbcloud/AgentTeam/desktop/internal/operator"
	"github.com/tidbcloud/AgentTeam/desktop/internal/profile"
	"github.com/tidbcloud/AgentTeam/desktop/internal/types"
)

// ---------------------------------------------------------------------------
// Flags
// ---------------------------------------------------------------------------

var channelOpts struct {
	format string
}

var channelAddOpts struct {
	name  string
	agent string

	// agentteam-specific
	agentteamServerURL string
	agentteamAPIKey    string
}

var channelUpdateOpts struct {
	agent string

	// agentteam-specific
	agentteamServerURL string
	agentteamAPIKey    string
}

const (
	agentteamChannelConfigKeyConnectionStatus  = "connection_status"
	agentteamChannelStatusInvalidMachineAPIKey = "invalid_machine_api_key"
)

// ---------------------------------------------------------------------------
// Serialization struct (for JSON/YAML/TOML output)
// ---------------------------------------------------------------------------

type channelInfo struct {
	ID      string                 `json:"id"                yaml:"id"                toml:"ID"`
	AgentID string                 `json:"agent_id,omitempty" yaml:"agent_id,omitempty" toml:"AgentID,omitempty"`
	Enabled bool                   `json:"enabled"           yaml:"enabled"           toml:"Enabled"`
	Config  map[string]interface{} `json:"config,omitempty"  yaml:"config,omitempty"  toml:"Config,omitempty"`
}

func channelConfigToInfo(ch profile.AgentTeamServer) channelInfo {
	return channelInfo{
		ID:      ch.ID,
		AgentID: ch.AgentID,
		Enabled: ch.Enabled,
		Config:  configToMap(ch.Config),
	}
}

// ---------------------------------------------------------------------------
// Commands
// ---------------------------------------------------------------------------

var channelCmd = &cobra.Command{
	Use:    "channel",
	Hidden: true,
	Short:  "Manage AgentTeam connections (legacy command: channel)",
	Long: `Manage AgentTeam connection definitions.

AgentTeam connections (legacy "channels") connect agentteam to AgentTeam servers and route
messages to AI agents.

Examples:
  agentteam channel list
  agentteam channel add --agentteam.server-url <url> --agentteam.api-key <key>
  agentteam channel get my-channel
  agentteam channel delete my-channel
  agentteam channel enable my-channel
  agentteam channel disable my-channel`,
}

var channelListCmd = &cobra.Command{
	Use:   "list",
	Short: "List all AgentTeam connections",
	Long: `List all configured AgentTeam connections.

Use --format/-f to control the output format.

Examples:
  agentteam channel list
  agentteam channel list -f json
  agentteam channel list --format yaml`,
	RunE: runChannelList,
}

var channelGetCmd = &cobra.Command{
	Use:   "get <id>",
	Short: "Show AgentTeam connection details",
	Long: `Show detailed information about a specific AgentTeam connection.

Examples:
  agentteam channel get my-channel
  agentteam channel get my-channel -f json`,
	Args: cobra.ExactArgs(1),
	RunE: runChannelGet,
}

var channelAddCmd = &cobra.Command{
	Use:   "add [name]",
	Short: "Add a new AgentTeam connection",
	Long: `Add a new AgentTeam connection definition.

The optional connection name can be provided as a positional argument or via
--name. When omitted, agentteam uses its generated connection ID.

Examples:
  agentteam channel add --agentteam.server-url <url> --agentteam.api-key <key>`,
	RunE:              runChannelAdd,
	DisableAutoGenTag: true,
}

var channelUpdateCmd = &cobra.Command{
	Use:   "update <id>",
	Short: "Update an existing AgentTeam connection",
	Long: `Update fields on an existing AgentTeam connection. Only explicitly set flags are applied.

Examples:
  agentteam channel update my-channel --agent new-agent
  agentteam channel update my-agentteam --agentteam.server-url <new-url>`,
	Args: cobra.ExactArgs(1),
	RunE: runChannelUpdate,
}

var channelDeleteCmd = &cobra.Command{
	Use:     "delete <id>",
	Aliases: []string{"rm", "remove"},
	Short:   "Delete a AgentTeam connection",
	Long: `Delete a AgentTeam connection definition.

Examples:
  agentteam channel delete my-channel
  agentteam channel rm my-channel`,
	Args: cobra.ExactArgs(1),
	RunE: runChannelDelete,
}

var channelEnableCmd = &cobra.Command{
	Use:   "enable <id>",
	Short: "Enable a AgentTeam connection",
	Long: `Enable a AgentTeam connection so the daemon will start it.

Examples:
  agentteam channel enable my-channel`,
	Args: cobra.ExactArgs(1),
	RunE: runChannelEnable,
}

var channelDisableCmd = &cobra.Command{
	Use:   "disable <id>",
	Short: "Disable a AgentTeam connection",
	Long: `Disable a AgentTeam connection so the daemon will not start it.

Examples:
  agentteam channel disable my-channel`,
	Args: cobra.ExactArgs(1),
	RunE: runChannelDisable,
}

func init() {
	channelListCmd.Flags().StringVarP(&channelOpts.format, "format", "f", "table", "output format (table|json|jsonc|yaml|toml)")
	channelGetCmd.Flags().StringVarP(&channelOpts.format, "format", "f", "table", "output format (table|json|jsonc|yaml|toml)")

	// Common flags for add.
	channelAddCmd.Flags().StringVar(&channelAddOpts.name, "name", "", "display name (default: positional arg)")
	channelAddCmd.Flags().StringVar(&channelAddOpts.agent, "agent", "", "agent ID to bind")

	// AgentTeam flags for add.
	channelAddCmd.Flags().StringVar(&channelAddOpts.agentteamServerURL, "agentteam.server-url", "https://agentteam.pingcap.com", "agentteam: server URL (default: https://agentteam.pingcap.com)")
	channelAddCmd.Flags().StringVar(&channelAddOpts.agentteamAPIKey, "agentteam.api-key", "", "agentteam: API key (required for agentteam)")

	// Update flags.
	channelUpdateCmd.Flags().StringVar(&channelUpdateOpts.agent, "agent", "", "agent ID to bind")
	channelUpdateCmd.Flags().StringVar(&channelUpdateOpts.agentteamServerURL, "agentteam.server-url", "https://agentteam.pingcap.com", "agentteam: server URL")
	channelUpdateCmd.Flags().StringVar(&channelUpdateOpts.agentteamAPIKey, "agentteam.api-key", "", "agentteam: API key")

	channelCmd.AddCommand(channelListCmd)
	channelCmd.AddCommand(channelGetCmd)
	channelCmd.AddCommand(channelAddCmd)
	channelCmd.AddCommand(channelUpdateCmd)
	channelCmd.AddCommand(channelDeleteCmd)
	channelCmd.AddCommand(channelEnableCmd)
	channelCmd.AddCommand(channelDisableCmd)
}

// ---------------------------------------------------------------------------
// RunE implementations
// ---------------------------------------------------------------------------

func runChannelList(_ *cobra.Command, _ []string) error {
	op, err := getOperator()
	if err != nil {
		return err
	}
	channels, err := op.Servers().All()
	if err != nil {
		return err
	}

	switch strings.ToLower(channelOpts.format) {
	case "table":
		return printChannelListTable(channels)
	case "json":
		return printChannelListJSON(channels, false)
	case "jsonc":
		return printChannelListJSON(channels, true)
	case "yaml":
		return printChannelListYAML(channels)
	case "toml":
		return printChannelListTOML(channels)
	default:
		return fmt.Errorf("unsupported format: %s (valid: table, json, jsonc, yaml, toml)", channelOpts.format)
	}
}

func runChannelGet(_ *cobra.Command, args []string) error {
	op, err := getOperator()
	if err != nil {
		return err
	}
	ch, ok, err := op.Servers().Get(args[0])
	if err != nil {
		return err
	}
	if !ok {
		return fmt.Errorf("channel %q not found", args[0])
	}

	info := channelConfigToInfo(ch)

	switch strings.ToLower(channelOpts.format) {
	case "table":
		return printChannelDetailTable(ch)
	case "json":
		return printJSON(info, false)
	case "jsonc":
		return printJSON(info, true)
	case "yaml":
		return printYAML(info)
	case "toml":
		return printTOML(info)
	default:
		return fmt.Errorf("unsupported format: %s (valid: table, json, jsonc, yaml, toml)", channelOpts.format)
	}
}

func runChannelAdd(_ *cobra.Command, args []string) error {
	if len(args) > 1 {
		return fmt.Errorf("channel add accepts at most one positional name")
	}

	channelName := ""
	if len(args) == 1 {
		channelName = strings.TrimSpace(args[0])
	}
	if channelName == "" {
		channelName = strings.TrimSpace(channelAddOpts.name)
	}
	op, err := getOperator()
	if err != nil {
		return err
	}

	if channelAddOpts.agentteamAPIKey == "" {
		return fmt.Errorf("agentteam channel requires --agentteam.api-key")
	}

	cfgMap := make(map[string]interface{})
	cfgMap["server_url"] = channelAddOpts.agentteamServerURL
	cfgMap["api_key"] = channelAddOpts.agentteamAPIKey

	ch := profile.AgentTeamServer{
		Name:    channelName,
		Enabled: true,
		AgentID: channelAddOpts.agent,
		Config:  mapToRawMessage(cfgMap),
	}

	if _, err := op.Servers().Add(&ch); err != nil {
		return err
	}

	fmt.Printf("Channel %q added successfully.\n", ch.ID)

	return nil
}

func connectAgentTeamChannel(channels interface {
	operator.ServerReader
	operator.ServerWriter
	operator.ServerEnabler
}, serverURL string, apiKey string, workspaceID string) (profile.AgentTeamServer, error) {
	serverURL = normalizeAgentTeamChannelServerURL(serverURL)
	apiKey = strings.TrimSpace(apiKey)
	workspaceID = strings.TrimSpace(workspaceID)
	if apiKey == "" {
		return profile.AgentTeamServer{}, fmt.Errorf("agentteam daemon requires --api-key when saving AgentTeam machine settings")
	}

	existingChannels, err := channels.All()
	if err != nil {
		return profile.AgentTeamServer{}, err
	}

	var exactMatches []profile.AgentTeamServer
	var reusableInvalidMatches []profile.AgentTeamServer
	for _, ch := range existingChannels {
		// The API key identifies the server-side machine. A second machine in the
		// same AgentTeam workspace uses the same server URL with a different API key, so
		// it must create another local channel instead of replacing this one.
		if agentteamChannelServerURL(ch) != serverURL {
			continue
		}

		if agentteamChannelAPIKey(ch) == apiKey {
			exactMatches = append(exactMatches, ch)
			continue
		}

		if canReuseInvalidAgentTeamChannel(ch, serverURL, apiKey, workspaceID) {
			reusableInvalidMatches = append(reusableInvalidMatches, ch)
		}
	}

	if len(exactMatches) > 0 {
		primary := choosePrimaryAgentTeamChannel(exactMatches)
		persisted, err := persistConnectedAgentTeamChannel(channels, primary, serverURL, apiKey, workspaceID)
		if err != nil {
			return profile.AgentTeamServer{}, err
		}
		if err := disableDuplicateAgentTeamChannels(channels, exactMatches, persisted.ID); err != nil {
			return profile.AgentTeamServer{}, err
		}
		return persisted, nil
	}

	if len(reusableInvalidMatches) == 1 {
		return persistConnectedAgentTeamChannel(channels, reusableInvalidMatches[0], serverURL, apiKey, workspaceID)
	}

	ch := profile.AgentTeamServer{
		Enabled: true,
		Config:  mapToRawMessage(connectedAgentTeamChannelConfig(serverURL, apiKey, workspaceID)),
	}

	return channels.Add(&ch)
}

func connectedAgentTeamChannelConfig(serverURL string, apiKey string, workspaceID string) map[string]interface{} {
	cfg := map[string]interface{}{
		"server_url": serverURL,
		"api_key":    apiKey,
	}
	if workspaceID = strings.TrimSpace(workspaceID); workspaceID != "" {
		cfg["workspace_id"] = workspaceID
	}
	return cfg
}

func connectedAgentTeamAuthConfig(serverURL string, apiKey string, workspaceID string, machineID string) map[string]interface{} {
	cfg := connectedAgentTeamChannelConfig(serverURL, apiKey, workspaceID)
	// A successful server-side resolution proves this credential is usable. Keep
	// the explicit empty marker in the update payload so a running daemon clears
	// any terminal invalid-key state before normalizing the persisted profile.
	cfg[agentteamChannelConfigKeyConnectionStatus] = ""
	if machineID = strings.TrimSpace(machineID); machineID != "" {
		cfg["machine_id"] = machineID
	}
	return cfg
}

func persistConnectedAgentTeamChannel(channels operator.ServerWriter, ch profile.AgentTeamServer, serverURL string, apiKey string, workspaceID string) (profile.AgentTeamServer, error) {
	if ch.Config == nil {
		ch.Config = types.RawMessage("{}\n")
	}

	cfg := configToMap(ch.Config)
	cfg["server_url"] = serverURL
	cfg["api_key"] = apiKey
	delete(cfg, agentteamChannelConfigKeyConnectionStatus)
	if workspaceID = strings.TrimSpace(workspaceID); workspaceID != "" {
		cfg["workspace_id"] = workspaceID
	}
	ch.Config = mapToRawMessage(cfg)
	ch.Enabled = true

	return channels.Update(ch)
}

func choosePrimaryAgentTeamChannel(matches []profile.AgentTeamServer) profile.AgentTeamServer {
	for _, ch := range matches {
		if ch.Enabled {
			return ch
		}
	}
	return matches[0]
}

func disableDuplicateAgentTeamChannels(channels operator.ServerEnabler, matches []profile.AgentTeamServer, keepID string) error {
	for _, ch := range matches {
		if ch.ID == keepID || !ch.Enabled {
			continue
		}
		if err := channels.SetEnabled(ch.ID, false); err != nil {
			return fmt.Errorf("disable duplicate agentteam channel %q: %w", ch.ID, err)
		}
	}
	return nil
}

func normalizeAgentTeamChannelServerURL(value string) string {
	return strings.TrimRight(strings.TrimSpace(value), "/")
}

func agentteamChannelServerURL(ch profile.AgentTeamServer) string {
	if value := strings.TrimSpace(ch.ServerURL); value != "" {
		return normalizeAgentTeamChannelServerURL(value)
	}
	cfg := configToMap(ch.Config)
	value, ok := cfg["server_url"]
	if !ok || value == nil {
		return ""
	}
	return normalizeAgentTeamChannelServerURL(fmt.Sprint(value))
}

func agentteamChannelAPIKey(ch profile.AgentTeamServer) string {
	if value := strings.TrimSpace(ch.APIKey); value != "" {
		return value
	}
	cfg := configToMap(ch.Config)
	value, ok := cfg["api_key"]
	if !ok || value == nil {
		return ""
	}
	return strings.TrimSpace(fmt.Sprint(value))
}

func agentteamChannelWorkspaceID(ch profile.AgentTeamServer) string {
	if value := strings.TrimSpace(ch.WorkspaceID); value != "" {
		return value
	}
	cfg := configToMap(ch.Config)
	value, ok := cfg["workspace_id"]
	if !ok || value == nil {
		return ""
	}
	return strings.TrimSpace(fmt.Sprint(value))
}

func agentteamChannelMachineID(ch profile.AgentTeamServer) string {
	if value := strings.TrimSpace(ch.MachineID); value != "" {
		return value
	}
	cfg := configToMap(ch.Config)
	value, ok := cfg["machine_id"]
	if !ok || value == nil {
		return ""
	}
	return strings.TrimSpace(fmt.Sprint(value))
}

func agentteamChannelConnectionStatus(ch profile.AgentTeamServer) string {
	if value := strings.TrimSpace(ch.ConnectionStatus); value != "" {
		return value
	}
	cfg := configToMap(ch.Config)
	value, ok := cfg[agentteamChannelConfigKeyConnectionStatus]
	if !ok || value == nil {
		return ""
	}
	return strings.TrimSpace(fmt.Sprint(value))
}

func canReuseInvalidAgentTeamChannel(ch profile.AgentTeamServer, serverURL string, apiKey string, workspaceID string) bool {
	if strings.TrimSpace(workspaceID) == "" {
		return false
	}
	if agentteamChannelServerURL(ch) != serverURL || agentteamChannelAPIKey(ch) == apiKey {
		return false
	}
	if candidateWorkspaceID := agentteamChannelWorkspaceID(ch); candidateWorkspaceID != "" && candidateWorkspaceID != workspaceID {
		return false
	}
	return agentteamChannelConnectionStatus(ch) == agentteamChannelStatusInvalidMachineAPIKey
}

func runChannelUpdate(cmd *cobra.Command, args []string) error {
	op, err := getOperator()
	if err != nil {
		return err
	}
	id := args[0]

	ch, ok, err := op.Servers().Get(id)
	if err != nil {
		return err
	}
	if !ok {
		return fmt.Errorf("channel %q not found", id)
	}

	if ch.Config == nil {
		ch.Config = types.RawMessage("{}\n")
	}

	if cmd.Flags().Changed("agent") {
		ch.AgentID = channelUpdateOpts.agent
	}

	cfg := configToMap(ch.Config)
	if cmd.Flags().Changed("agentteam.server-url") {
		cfg["server_url"] = channelUpdateOpts.agentteamServerURL
	}
	if cmd.Flags().Changed("agentteam.api-key") {
		cfg["api_key"] = channelUpdateOpts.agentteamAPIKey
	}
	ch.Config = mapToRawMessage(cfg)

	if _, err := op.Servers().Update(ch); err != nil {
		return err
	}

	fmt.Printf("Channel %q updated successfully.\n", id)
	return nil
}

func runChannelDelete(_ *cobra.Command, args []string) error {
	op, err := getOperator()
	if err != nil {
		return err
	}
	if err := op.Servers().Delete(args[0]); err != nil {
		return err
	}
	fmt.Printf("Channel %q deleted successfully.\n", args[0])
	return nil
}

func runChannelEnable(_ *cobra.Command, args []string) error {
	op, err := getOperator()
	if err != nil {
		return err
	}
	if err := op.Servers().SetEnabled(args[0], true); err != nil {
		return err
	}
	fmt.Printf("Channel %q enabled.\n", args[0])
	return nil
}

func runChannelDisable(_ *cobra.Command, args []string) error {
	op, err := getOperator()
	if err != nil {
		return err
	}
	if err := op.Servers().SetEnabled(args[0], false); err != nil {
		return err
	}
	fmt.Printf("Channel %q disabled.\n", args[0])
	return nil
}

// ---------------------------------------------------------------------------
// Output helpers
// ---------------------------------------------------------------------------

func printChannelListTable(channels []profile.AgentTeamServer) error {
	w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
	fmt.Fprintln(w, "ID\tAGENT\tENABLED")
	for _, ch := range channels {
		fmt.Fprintf(w, "%s\t%s\t%v\n", ch.ID, ch.AgentID, ch.Enabled)
	}
	return w.Flush()
}

func printChannelDetailTable(ch profile.AgentTeamServer) error {
	w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
	fmt.Fprintf(w, "ID\t%s\n", ch.ID)
	if ch.AgentID != "" {
		fmt.Fprintf(w, "Agent\t%s\n", ch.AgentID)
	}
	fmt.Fprintf(w, "Enabled\t%v\n", ch.Enabled)
	if cfg := configToMap(ch.Config); len(cfg) > 0 {
		fmt.Fprintf(w, "Config:\t\n")
		for k, v := range cfg {
			fmt.Fprintf(w, "  %s\t%v\n", k, v)
		}
	}
	return w.Flush()
}

// ---------------------------------------------------------------------------
// Config helpers
// ---------------------------------------------------------------------------

// configToMap unmarshals a yaml.RawMessage into map[string]interface{}.
func configToMap(raw types.RawMessage) map[string]interface{} {
	if len(raw) == 0 {
		return nil
	}
	var m map[string]interface{}
	_ = yaml.Unmarshal(raw, &m)
	return m
}

// mapToRawMessage marshals a map[string]interface{} into yaml.RawMessage.
func mapToRawMessage(m map[string]interface{}) types.RawMessage {
	data, err := yaml.Marshal(m)
	if err != nil {
		return nil
	}
	return data
}

func printChannelListJSON(channels []profile.AgentTeamServer, pretty bool) error {
	infos := make([]channelInfo, len(channels))
	for i, ch := range channels {
		infos[i] = channelConfigToInfo(ch)
	}
	return printJSON(infos, pretty)
}

func printChannelListYAML(channels []profile.AgentTeamServer) error {
	infos := make([]channelInfo, len(channels))
	for i, ch := range channels {
		infos[i] = channelConfigToInfo(ch)
	}
	return printYAML(infos)
}

func printChannelListTOML(channels []profile.AgentTeamServer) error {
	infos := make([]channelInfo, len(channels))
	for i, ch := range channels {
		infos[i] = channelConfigToInfo(ch)
	}
	return printTOML(infos)
}
