package cli

import (
	"context"
	"errors"
	"fmt"
	"os"
	"time"

	"charm.land/bubbles/v2/spinner"
	tea "charm.land/bubbletea/v2"
	"charm.land/lipgloss/v2"
	"github.com/psviderski/uncloud/internal/cli/config"
	"github.com/psviderski/uncloud/internal/cli/tui"
	"github.com/psviderski/uncloud/internal/fs"
	"github.com/psviderski/uncloud/pkg/client"
	"github.com/psviderski/uncloud/pkg/client/connector"
)

// ConnectOptions configures the behavior of cluster connection attempts.
type ConnectOptions struct {
	// Whether to show connection progress spinner if stdout is a terminal or progress logs if not.
	ShowProgress bool
}

func ConnectCluster(ctx context.Context, conn config.MachineConnection, opts ConnectOptions) (*client.Client, error) {
	if opts.ShowProgress {
		return connectClusterWithProgress(ctx, conn)
	}
	return connectCluster(ctx, conn)
}

// connectClusterWithProgress connects to the cluster while displaying a progress spinner.
// If the stdout is not a terminal, it falls back to simple progress logs to stderr.
func connectClusterWithProgress(ctx context.Context, conn config.MachineConnection) (*client.Client, error) {
	// If stdout is not a terminal, fall back to simple progress logs.
	if !tui.IsStdoutTerminal() {
		fmt.Fprintln(os.Stderr, "Connecting to", conn.String())
		cli, err := connectCluster(ctx, conn)
		if err != nil {
			fmt.Fprintln(os.Stderr, "Connection failed:", err)
		} else {
			fmt.Fprintln(os.Stderr, "Connected to cluster.")
		}
		return cli, err
	}

	// Run the connection TUI model.
	p := tea.NewProgram(newConnectModel(ctx, conn))
	model, err := p.Run()
	if err != nil {
		return nil, fmt.Errorf("run connection TUI: %w", err)
	}

	m := model.(connectModel)
	return m.result.client, m.result.err
}

func connectCluster(ctx context.Context, conn config.MachineConnection) (*client.Client, error) {
	// Determine which SSH type is configured.
	var sshDest config.SSHDestination
	var useGoSSH bool

	// Validate connection configuration early to provide clear error messages.
	if err := conn.Validate(); err != nil {
		return nil, fmt.Errorf("invalid connection configuration: %w", err)
	}

	if conn.SSH != "" {
		// SSH uses the system ssh CLI command (default).
		sshDest = conn.SSH
	} else if conn.SSHCLI != "" {
		// SSHCLI is a backward-compatible alias for SSH.
		sshDest = conn.SSHCLI
	} else if conn.SSHGo != "" {
		// SSHGo uses Go's built-in SSH library.
		sshDest = conn.SSHGo
		useGoSSH = true
	} else if conn.TCP != nil && conn.TCP.IsValid() {
		return client.New(ctx, connector.NewTCPConnector(*conn.TCP))
	} else if conn.Unix != "" {
		return client.New(ctx, connector.NewUnixConnector(conn.Unix))
	} else {
		return nil, errors.New("connection configuration is invalid")
	}

	// Parse SSH destination and create config (shared for both types)
	user, host, port, err := sshDest.Parse()
	if err != nil {
		return nil, fmt.Errorf("parse SSH connection %q: %w", sshDest, err)
	}

	keyPath := fs.ExpandHomeDir(conn.SSHKeyFile)

	sshConfig := &connector.SSHConnectorConfig{
		User:    user,
		Host:    host,
		Port:    port,
		KeyPath: keyPath,
	}

	// Create appropriate connector based on type.
	if useGoSSH {
		return client.New(ctx, connector.NewSSHConnector(sshConfig))
	}
	return client.New(ctx, connector.NewSSHCLIConnector(sshConfig))
}

// connectModel is a TUI model for connecting to a cluster with a progress spinner.
type connectModel struct {
	ctx     context.Context
	conn    config.MachineConnection
	spinner spinner.Model
	// showSpinner controls whether the spinner is visible (delayed to avoid flashing).
	showSpinner bool
	// done indicates whether the connection attempt has completed (successfully or with error).
	done bool
	// result holds the result of the connection attempt.
	result connectResultMsg
}

type connectResultMsg struct {
	client *client.Client
	err    error
}

// showSpinnerMsg is sent after a delay to show the spinner.
type showSpinnerMsg struct{}

func newConnectModel(ctx context.Context, conn config.MachineConnection) connectModel {
	s := spinner.New()
	s.Spinner = spinner.MiniDot
	s.Style = lipgloss.NewStyle().Foreground(lipgloss.Yellow) // the same yellow as in compose progress

	return connectModel{
		ctx:     ctx,
		conn:    conn,
		spinner: s,
	}
}

func (m connectModel) Init() tea.Cmd {
	return tea.Batch(
		m.spinner.Tick,
		m.connect(),
		m.delayShowSpinner(),
	)
}

func (m connectModel) connect() tea.Cmd {
	return func() tea.Msg {
		cli, err := connectCluster(m.ctx, m.conn)
		return connectResultMsg{
			client: cli,
			err:    err,
		}
	}
}

// delayShowSpinner returns a command that sends a message to show the spinner after a delay.
// This avoids flashing the spinner if the connection is fast.
func (m connectModel) delayShowSpinner() tea.Cmd {
	return tea.Tick(500*time.Millisecond, func(t time.Time) tea.Msg {
		return showSpinnerMsg{}
	})
}

func (m connectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	select {
	case <-m.ctx.Done():
		m.result.err = m.ctx.Err()
		m.done = true
		return m, tea.Quit
	default:
	}

	switch msg := msg.(type) {
	case connectResultMsg:
		m.result = msg
		m.done = true
		return m, tea.Quit

	case showSpinnerMsg:
		// Only show spinner if connection hasn't completed yet.
		if !m.done {
			m.showSpinner = true
		}
		return m, nil

	case spinner.TickMsg:
		var cmd tea.Cmd
		m.spinner, cmd = m.spinner.Update(msg)
		return m, cmd

	case tea.KeyPressMsg:
		if msg.String() == "ctrl+c" {
			m.result.err = fmt.Errorf("connection cancelled")
			m.done = true
			return m, tea.Quit
		}
	}

	return m, nil
}

func (m connectModel) View() tea.View {
	// Don't show anything if done or spinner not yet visible.
	if m.done || !m.showSpinner {
		return tea.NewView("")
	}

	style := lipgloss.NewStyle().Foreground(lipgloss.Color("153"))
	return tea.NewView(fmt.Sprintf("%s %s\n",
		m.spinner.View(),
		fmt.Sprintf("Connecting to %s", style.Render(m.conn.String())),
	))
}
