package client

import (
	"context"
	"errors"
	"fmt"
	"net"
	"os"
	"path/filepath"
	"runtime"
	"strconv"
	"strings"
	"time"

	log "github.com/sirupsen/logrus"
	"golang.org/x/crypto/ssh"
	"golang.org/x/crypto/ssh/knownhosts"
	"golang.org/x/term"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"

	"github.com/netbirdio/netbird/client/internal/daemonaddr"
	"github.com/netbirdio/netbird/client/internal/profilemanager"
	"github.com/netbirdio/netbird/client/proto"
	nbssh "github.com/netbirdio/netbird/client/ssh"
	"github.com/netbirdio/netbird/client/ssh/detection"
	"github.com/netbirdio/netbird/util"
	"github.com/netbirdio/netbird/util/netrelay"
)

const (
	// DefaultDaemonAddr is the default address for the NetBird daemon
	DefaultDaemonAddr = "unix:///var/run/netbird.sock"
	// DefaultDaemonAddrWindows is the default address for the NetBird daemon on Windows
	DefaultDaemonAddrWindows = "tcp://127.0.0.1:41731"
)

// Client wraps crypto/ssh Client for simplified SSH operations
type Client struct {
	client        *ssh.Client
	terminalState *term.State
	terminalFd    int

	windowsStdoutMode uint32 // nolint:unused
	windowsStdinMode  uint32 // nolint:unused
}

func (c *Client) Close() error {
	return c.client.Close()
}

func (c *Client) OpenTerminal(ctx context.Context) error {
	session, err := c.client.NewSession()
	if err != nil {
		return fmt.Errorf("new session: %w", err)
	}
	defer func() {
		if err := session.Close(); err != nil {
			log.Debugf("session close error: %v", err)
		}
	}()

	if err := c.setupTerminalMode(ctx, session); err != nil {
		return err
	}

	c.setupSessionIO(session)

	if err := session.Shell(); err != nil {
		return fmt.Errorf("start shell: %w", err)
	}

	return c.waitForSession(ctx, session)
}

// setupSessionIO connects session streams to local terminal
func (c *Client) setupSessionIO(session *ssh.Session) {
	session.Stdout = os.Stdout
	session.Stderr = os.Stderr
	session.Stdin = os.Stdin
}

// waitForSession waits for the session to complete with context cancellation
func (c *Client) waitForSession(ctx context.Context, session *ssh.Session) error {
	done := make(chan error, 1)
	go func() {
		done <- session.Wait()
	}()

	defer c.restoreTerminal()

	select {
	case <-ctx.Done():
		return ctx.Err()
	case err := <-done:
		return c.handleSessionError(err)
	}
}

// handleSessionError processes session termination errors
func (c *Client) handleSessionError(err error) error {
	if err == nil {
		return nil
	}

	var e *ssh.ExitError
	var em *ssh.ExitMissingError
	if !errors.As(err, &e) && !errors.As(err, &em) {
		return fmt.Errorf("session wait: %w", err)
	}

	return nil
}

// restoreTerminal restores the terminal to its original state
func (c *Client) restoreTerminal() {
	if c.terminalState != nil {
		_ = term.Restore(c.terminalFd, c.terminalState)
		c.terminalState = nil
		c.terminalFd = 0
	}

	if err := c.restoreWindowsConsoleState(); err != nil {
		log.Debugf("restore Windows console state: %v", err)
	}
}

// ExecuteCommand executes a command on the remote host and returns the output
func (c *Client) ExecuteCommand(ctx context.Context, command string) ([]byte, error) {
	session, cleanup, err := c.createSession(ctx)
	if err != nil {
		return nil, err
	}
	defer cleanup()

	output, err := session.CombinedOutput(command)
	if err != nil {
		var e *ssh.ExitError
		var em *ssh.ExitMissingError
		if !errors.As(err, &e) && !errors.As(err, &em) {
			return output, fmt.Errorf("execute command: %w", err)
		}
	}

	return output, nil
}

// ExecuteCommandWithIO executes a command with interactive I/O connected to local terminal
func (c *Client) ExecuteCommandWithIO(ctx context.Context, command string) error {
	session, cleanup, err := c.createSession(ctx)
	if err != nil {
		return fmt.Errorf("create session: %w", err)
	}
	defer cleanup()

	c.setupSessionIO(session)

	if err := session.Start(command); err != nil {
		return fmt.Errorf("start command: %w", err)
	}

	done := make(chan error, 1)
	go func() {
		done <- session.Wait()
	}()

	select {
	case <-ctx.Done():
		_ = session.Signal(ssh.SIGTERM)
		select {
		case <-done:
			return ctx.Err()
		case <-time.After(100 * time.Millisecond):
			return ctx.Err()
		}
	case err := <-done:
		return c.handleCommandError(err)
	}
}

// ExecuteCommandWithPTY executes a command with a pseudo-terminal for interactive sessions
func (c *Client) ExecuteCommandWithPTY(ctx context.Context, command string) error {
	session, cleanup, err := c.createSession(ctx)
	if err != nil {
		return fmt.Errorf("create session: %w", err)
	}
	defer cleanup()

	if err := c.setupTerminalMode(ctx, session); err != nil {
		return fmt.Errorf("setup terminal mode: %w", err)
	}

	c.setupSessionIO(session)

	if err := session.Start(command); err != nil {
		return fmt.Errorf("start command: %w", err)
	}

	defer c.restoreTerminal()

	done := make(chan error, 1)
	go func() {
		done <- session.Wait()
	}()

	select {
	case <-ctx.Done():
		_ = session.Signal(ssh.SIGTERM)
		select {
		case <-done:
			return ctx.Err()
		case <-time.After(100 * time.Millisecond):
			return ctx.Err()
		}
	case err := <-done:
		return c.handleCommandError(err)
	}
}

// handleCommandError processes command execution errors
func (c *Client) handleCommandError(err error) error {
	if err == nil {
		return nil
	}

	var e *ssh.ExitError
	var em *ssh.ExitMissingError
	if errors.As(err, &e) || errors.As(err, &em) {
		return err
	}

	return fmt.Errorf("execute command: %w", err)
}

// setupContextCancellation sets up context cancellation for a session
func (c *Client) setupContextCancellation(ctx context.Context, session *ssh.Session) func() {
	done := make(chan struct{})
	go func() {
		select {
		case <-ctx.Done():
			_ = session.Signal(ssh.SIGTERM)
			_ = session.Close()
		case <-done:
		}
	}()
	return func() { close(done) }
}

// createSession creates a new SSH session with context cancellation setup
func (c *Client) createSession(ctx context.Context) (*ssh.Session, func(), error) {
	session, err := c.client.NewSession()
	if err != nil {
		return nil, nil, fmt.Errorf("new session: %w", err)
	}

	cancel := c.setupContextCancellation(ctx, session)
	cleanup := func() {
		cancel()
		_ = session.Close()
	}

	return session, cleanup, nil
}

// getDefaultDaemonAddr returns the daemon address from environment or default for the OS
func getDefaultDaemonAddr() string {
	if addr := os.Getenv("NB_DAEMON_ADDR"); addr != "" {
		return addr
	}
	if runtime.GOOS == "windows" {
		return DefaultDaemonAddrWindows
	}
	return daemonaddr.ResolveUnixDaemonAddr(DefaultDaemonAddr)
}

// DialOptions contains options for SSH connections
type DialOptions struct {
	KnownHostsFile     string
	IdentityFile       string
	DaemonAddr         string
	SkipCachedToken    bool
	InsecureSkipVerify bool
	NoBrowser          bool
}

// Dial connects to the given ssh server with specified options
func Dial(ctx context.Context, addr, user string, opts DialOptions) (*Client, error) {
	daemonAddr := opts.DaemonAddr
	if daemonAddr == "" {
		daemonAddr = getDefaultDaemonAddr()
	}
	opts.DaemonAddr = daemonAddr

	hostKeyCallback, err := createHostKeyCallback(opts)
	if err != nil {
		return nil, fmt.Errorf("create host key callback: %w", err)
	}

	config := &ssh.ClientConfig{
		User:            user,
		Timeout:         30 * time.Second,
		HostKeyCallback: hostKeyCallback,
	}

	if opts.IdentityFile != "" {
		authMethod, err := createSSHKeyAuth(opts.IdentityFile)
		if err != nil {
			return nil, fmt.Errorf("create SSH key auth: %w", err)
		}
		config.Auth = append(config.Auth, authMethod)
	}

	return dialWithJWT(ctx, "tcp", addr, config, daemonAddr, opts.SkipCachedToken, opts.NoBrowser)
}

// dialSSH establishes an SSH connection without JWT authentication
func dialSSH(ctx context.Context, network, addr string, config *ssh.ClientConfig) (*Client, error) {
	dialer := &net.Dialer{}
	conn, err := dialer.DialContext(ctx, network, addr)
	if err != nil {
		return nil, fmt.Errorf("dial %s: %w", addr, err)
	}

	clientConn, chans, reqs, err := ssh.NewClientConn(conn, addr, config)
	if err != nil {
		if closeErr := conn.Close(); closeErr != nil {
			log.Debugf("connection close after handshake failure: %v", closeErr)
		}
		return nil, fmt.Errorf("ssh handshake: %w", err)
	}

	client := ssh.NewClient(clientConn, chans, reqs)
	return &Client{
		client: client,
	}, nil
}

// dialWithJWT establishes an SSH connection with optional JWT authentication based on server detection
func dialWithJWT(ctx context.Context, network, addr string, config *ssh.ClientConfig, daemonAddr string, skipCache, noBrowser bool) (*Client, error) {
	host, portStr, err := net.SplitHostPort(addr)
	if err != nil {
		return nil, fmt.Errorf("parse address %s: %w", addr, err)
	}
	port, err := strconv.Atoi(portStr)
	if err != nil {
		return nil, fmt.Errorf("parse port %s: %w", portStr, err)
	}

	detectionCtx, cancel := context.WithTimeout(ctx, config.Timeout)
	defer cancel()

	dialer := &net.Dialer{}
	serverType, err := detection.DetectSSHServerType(detectionCtx, dialer, host, port)
	if err != nil {
		return nil, fmt.Errorf("SSH server detection: %w", err)
	}

	if !serverType.RequiresJWT() {
		return dialSSH(ctx, network, addr, config)
	}

	jwtCtx, cancel := context.WithTimeout(ctx, config.Timeout)
	defer cancel()

	jwtToken, err := requestJWTToken(jwtCtx, daemonAddr, skipCache, noBrowser)
	if err != nil {
		return nil, fmt.Errorf("request JWT token: %w", err)
	}

	configWithJWT := nbssh.AddJWTAuth(config, jwtToken)
	return dialSSH(ctx, network, addr, configWithJWT)
}

// requestJWTToken requests a JWT token from the NetBird daemon
func requestJWTToken(ctx context.Context, daemonAddr string, skipCache, noBrowser bool) (string, error) {
	hint := profilemanager.GetLoginHint()

	conn, err := connectToDaemon(daemonAddr)
	if err != nil {
		return "", fmt.Errorf("connect to daemon: %w", err)
	}
	defer conn.Close()

	client := proto.NewDaemonServiceClient(conn)

	var browserOpener func(string) error
	if !noBrowser {
		browserOpener = util.OpenBrowser
	}

	return nbssh.RequestJWTToken(ctx, client, os.Stdout, os.Stderr, !skipCache, hint, browserOpener)
}

// verifyHostKeyViaDaemon verifies SSH host key by querying the NetBird daemon
func verifyHostKeyViaDaemon(hostname string, remote net.Addr, key ssh.PublicKey, daemonAddr string) error {
	conn, err := connectToDaemon(daemonAddr)
	if err != nil {
		return err
	}
	defer func() {
		if err := conn.Close(); err != nil {
			log.Debugf("daemon connection close error: %v", err)
		}
	}()

	client := proto.NewDaemonServiceClient(conn)
	verifier := nbssh.NewDaemonHostKeyVerifier(client)
	callback := nbssh.CreateHostKeyCallback(verifier)
	return callback(hostname, remote, key)
}

func connectToDaemon(daemonAddr string) (*grpc.ClientConn, error) {
	addr := strings.TrimPrefix(daemonAddr, "tcp://")

	conn, err := grpc.NewClient(
		addr,
		grpc.WithTransportCredentials(insecure.NewCredentials()),
	)
	if err != nil {
		log.Debugf("failed to create gRPC client for NetBird daemon at %s: %v", daemonAddr, err)
		return nil, fmt.Errorf("failed to connect to NetBird daemon: %w", err)
	}

	return conn, nil
}

// getKnownHostsFiles returns paths to known_hosts files in order of preference
func getKnownHostsFiles() []string {
	var files []string

	// User's known_hosts file (highest priority)
	if homeDir, err := os.UserHomeDir(); err == nil {
		userKnownHosts := filepath.Join(homeDir, ".ssh", "known_hosts")
		files = append(files, userKnownHosts)
	}

	// NetBird managed known_hosts files
	if runtime.GOOS == "windows" {
		programData := os.Getenv("PROGRAMDATA")
		if programData == "" {
			programData = `C:\ProgramData`
		}
		netbirdKnownHosts := filepath.Join(programData, "ssh", "ssh_known_hosts.d", "99-netbird")
		files = append(files, netbirdKnownHosts)
	} else {
		files = append(files, "/etc/ssh/ssh_known_hosts.d/99-netbird")
		files = append(files, "/etc/ssh/ssh_known_hosts")
	}

	return files
}

// createHostKeyCallback creates a host key verification callback
func createHostKeyCallback(opts DialOptions) (ssh.HostKeyCallback, error) {
	if opts.InsecureSkipVerify {
		return ssh.InsecureIgnoreHostKey(), nil // #nosec G106 - User explicitly requested insecure mode
	}

	return func(hostname string, remote net.Addr, key ssh.PublicKey) error {
		if err := tryDaemonVerification(hostname, remote, key, opts.DaemonAddr); err == nil {
			return nil
		}
		return tryKnownHostsVerification(hostname, remote, key, opts.KnownHostsFile)
	}, nil
}

func tryDaemonVerification(hostname string, remote net.Addr, key ssh.PublicKey, daemonAddr string) error {
	if daemonAddr == "" {
		return fmt.Errorf("no daemon address")
	}
	return verifyHostKeyViaDaemon(hostname, remote, key, daemonAddr)
}

func tryKnownHostsVerification(hostname string, remote net.Addr, key ssh.PublicKey, knownHostsFile string) error {
	knownHostsFiles := getKnownHostsFilesList(knownHostsFile)
	hostKeyCallbacks := buildHostKeyCallbacks(knownHostsFiles)

	for _, callback := range hostKeyCallbacks {
		if err := callback(hostname, remote, key); err == nil {
			return nil
		}
	}
	return fmt.Errorf("host key verification failed: key for %s not found in any known_hosts file", hostname)
}

func getKnownHostsFilesList(knownHostsFile string) []string {
	if knownHostsFile != "" {
		return []string{knownHostsFile}
	}
	return getKnownHostsFiles()
}

func buildHostKeyCallbacks(knownHostsFiles []string) []ssh.HostKeyCallback {
	var hostKeyCallbacks []ssh.HostKeyCallback
	for _, file := range knownHostsFiles {
		if callback, err := knownhosts.New(file); err == nil {
			hostKeyCallbacks = append(hostKeyCallbacks, callback)
		}
	}
	return hostKeyCallbacks
}

// createSSHKeyAuth creates SSH key authentication from a private key file
func createSSHKeyAuth(keyFile string) (ssh.AuthMethod, error) {
	keyData, err := os.ReadFile(keyFile)
	if err != nil {
		return nil, fmt.Errorf("read SSH key file %s: %w", keyFile, err)
	}

	signer, err := ssh.ParsePrivateKey(keyData)
	if err != nil {
		return nil, fmt.Errorf("parse SSH private key: %w", err)
	}

	return ssh.PublicKeys(signer), nil
}

// LocalPortForward sets up local port forwarding, binding to localAddr and forwarding to remoteAddr
func (c *Client) LocalPortForward(ctx context.Context, localAddr, remoteAddr string) error {
	localListener, err := net.Listen("tcp", localAddr)
	if err != nil {
		return fmt.Errorf("listen on %s: %w", localAddr, err)
	}

	go func() {
		defer func() {
			if err := localListener.Close(); err != nil && !errors.Is(err, net.ErrClosed) {
				log.Debugf("local listener close error: %v", err)
			}
		}()
		for {
			localConn, err := localListener.Accept()
			if err != nil {
				if ctx.Err() != nil {
					return
				}
				continue
			}

			go c.handleLocalForward(ctx, localConn, remoteAddr)
		}
	}()

	<-ctx.Done()
	if err := localListener.Close(); err != nil && !errors.Is(err, net.ErrClosed) {
		log.Debugf("local listener close error: %v", err)
	}
	return ctx.Err()
}

// handleLocalForward handles a single local port forwarding connection
func (c *Client) handleLocalForward(ctx context.Context, localConn net.Conn, remoteAddr string) {
	defer func() {
		if err := localConn.Close(); err != nil {
			log.Debugf("local port forwarding: close local connection: %v", err)
		}
	}()

	channel, err := c.client.Dial("tcp", remoteAddr)
	if err != nil {
		var openErr *ssh.OpenChannelError
		if errors.As(err, &openErr) && openErr.Reason == ssh.Prohibited {
			_, _ = fmt.Fprintf(os.Stderr, "channel open failed: port forwarding is disabled\n")
		} else {
			log.Debugf("local port forwarding to %s failed: %v", remoteAddr, err)
		}
		return
	}
	defer func() {
		if err := channel.Close(); err != nil {
			log.Debugf("local port forwarding: close remote channel: %v", err)
		}
	}()

	netrelay.Relay(ctx, localConn, channel, netrelay.Options{Logger: log.NewEntry(log.StandardLogger())})
}

// RemotePortForward sets up remote port forwarding, binding on remote and forwarding to localAddr
func (c *Client) RemotePortForward(ctx context.Context, remoteAddr, localAddr string) error {
	host, port, err := c.parseRemoteAddress(remoteAddr)
	if err != nil {
		return fmt.Errorf("parse remote address: %w", err)
	}

	req := c.buildTCPIPForwardRequest(host, port)
	if err := c.sendTCPIPForwardRequest(req); err != nil {
		return fmt.Errorf("setup remote forward: %w", err)
	}

	go c.handleRemoteForwardChannels(ctx, localAddr)

	<-ctx.Done()

	if err := c.cancelTCPIPForwardRequest(req); err != nil {
		return fmt.Errorf("cancel tcpip-forward: %w", err)
	}
	return ctx.Err()
}

// parseRemoteAddress parses host and port from remote address string
func (c *Client) parseRemoteAddress(remoteAddr string) (string, uint32, error) {
	host, portStr, err := net.SplitHostPort(remoteAddr)
	if err != nil {
		return "", 0, fmt.Errorf("parse remote address %s: %w", remoteAddr, err)
	}

	port, err := strconv.Atoi(portStr)
	if err != nil {
		return "", 0, fmt.Errorf("parse remote port %s: %w", portStr, err)
	}

	return host, uint32(port), nil
}

// buildTCPIPForwardRequest creates a tcpip-forward request message
func (c *Client) buildTCPIPForwardRequest(host string, port uint32) tcpipForwardMsg {
	return tcpipForwardMsg{
		Host: host,
		Port: port,
	}
}

// sendTCPIPForwardRequest sends the tcpip-forward request to establish remote port forwarding
func (c *Client) sendTCPIPForwardRequest(req tcpipForwardMsg) error {
	ok, _, err := c.client.SendRequest("tcpip-forward", true, ssh.Marshal(&req))
	if err != nil {
		return fmt.Errorf("send tcpip-forward request: %w", err)
	}
	if !ok {
		return fmt.Errorf("remote port forwarding denied by server")
	}
	return nil
}

// cancelTCPIPForwardRequest cancels the tcpip-forward request
func (c *Client) cancelTCPIPForwardRequest(req tcpipForwardMsg) error {
	_, _, err := c.client.SendRequest("cancel-tcpip-forward", true, ssh.Marshal(&req))
	if err != nil {
		return fmt.Errorf("send cancel-tcpip-forward request: %w", err)
	}
	return nil
}

// handleRemoteForwardChannels handles incoming forwarded-tcpip channels
func (c *Client) handleRemoteForwardChannels(ctx context.Context, localAddr string) {
	// Get the channel once - subsequent calls return nil!
	channelRequests := c.client.HandleChannelOpen("forwarded-tcpip")
	if channelRequests == nil {
		log.Debugf("forwarded-tcpip channel type already being handled")
		return
	}

	for {
		select {
		case <-ctx.Done():
			return
		case newChan, ok := <-channelRequests:
			if !ok {
				return
			}
			if newChan != nil {
				go c.handleRemoteForwardChannel(ctx, newChan, localAddr)
			}
		}
	}
}

// handleRemoteForwardChannel handles a single forwarded-tcpip channel
func (c *Client) handleRemoteForwardChannel(ctx context.Context, newChan ssh.NewChannel, localAddr string) {
	channel, reqs, err := newChan.Accept()
	if err != nil {
		return
	}
	defer func() {
		if err := channel.Close(); err != nil {
			log.Debugf("remote port forwarding: close remote channel: %v", err)
		}
	}()

	go ssh.DiscardRequests(reqs)

	// Bound the dial so a black-holed localAddr can't pin the accepted SSH
	// channel open indefinitely; the relay itself runs under the outer ctx.
	dialCtx, cancelDial := context.WithTimeout(ctx, 10*time.Second)
	var dialer net.Dialer
	localConn, err := dialer.DialContext(dialCtx, "tcp", localAddr)
	cancelDial()
	if err != nil {
		log.Debugf("remote port forwarding: dial %s: %v", localAddr, err)
		return
	}
	defer func() {
		if err := localConn.Close(); err != nil {
			log.Debugf("remote port forwarding: close local connection: %v", err)
		}
	}()

	netrelay.Relay(ctx, localConn, channel, netrelay.Options{Logger: log.NewEntry(log.StandardLogger())})
}

// tcpipForwardMsg represents the structure for tcpip-forward requests
type tcpipForwardMsg struct {
	Host string
	Port uint32
}
