package bridge

import (
	
	
	
	
	
	
	
	

	
	
	
	
)

type Tunnel struct {
	auth     []ssh.AuthMethod
	hostKeys ssh.HostKeyCallback
	mode     byte // '>' for forward, '<' for reverse
	user     string
	hostAddr string
	bindAddr string
	dialAddr string
	dialType string
	password string

	SshClient *ssh.Client

	log logger.Zapper

	ctx    context.Context
	cancel context.CancelFunc

	errHandler func()

	Port           int
	LastConnection time.Time
	Started        bool

	sync.Mutex
}

// Start starts binding to remote end and return error if exists any
func ( *Tunnel) () {
	 := sync.WaitGroup{}
	.Add(1)
	go .bindTunnel(.ctx, &)
	.Wait()
}

// Stop collapses tunnel
func ( *Tunnel) () {
	.Started = false
	.log.Infow("collapsed tunnel", "details", )
	if .SshClient != nil {
		.SshClient.Close()
	}
	.cancel()
}

// bindTunnel Binds tunnel with our tunnel object
func ( *Tunnel) ( context.Context,  *sync.WaitGroup) {
	 := sync.WaitGroup{}
	.Add(1)
	defer .Done()

	for {
		var  sync.Once // Only print errors once per session
		func() {
			var  *ssh.Client
			var  error

			// Attempt to dial the remote SSH server.
			 = retry.Do(
				func() error {
					,  = ssh.Dial("tcp", .hostAddr, &ssh.ClientConfig{
						User:            .user,
						Auth:            .auth,
						HostKeyCallback: .hostKeys,
						Timeout:         5 * time.Second,
					})
					if  != nil {
						if strings.Contains(.Error(), "unable to authenticate") {
							.log.Errorw("ssh dial error", "details", fmt.Sprintf("%v, %v", , ))
							.Stop()
							return retry.Unrecoverable()
						}
						return 
					}
					return nil
				},
				retry.Attempts(20),
				retry.Delay(1*time.Second),
			)

			if  != nil {
				.Do(func() {
					.log.Errorw("ssh dial error", "details", fmt.Sprintf("%v, %v", , ))
					.Stop()
					.errHandler()
				})
				return
			}
			defer .Close()

			.Add(1)
			.SshClient = 

			// Attempt to bind to the inbound socket.
			var  net.Listener
			switch .mode {
			case '>':
				,  = net.Listen("tcp", .bindAddr)
			case '<':
				,  = .Listen("tcp", .bindAddr)
			}
			if  != nil {
				.Do(func() {
					.log.Errorw("bind error", "details", fmt.Sprintf("%v, %v", , ))
					.Stop()
					.errHandler()
				})
				return
			}

			// The socket is binded. Make sure we close it eventually.
			,  := context.WithCancel()
			defer ()
			go func() {
				.Wait()
				()
			}()
			go func() {
				<-.Done()
				.Close()
			}()

			// Dial once to make sure the connection is established.
			var  net.Conn
			.Started = false
			 = retry.Do(
				func() error {
					switch .mode {
					case '>':
						,  = .Dial(.dialType, .dialAddr)
					case '<':
						,  = net.Dial(.dialType, .dialAddr)
					}

					if  != nil {
						if strings.Contains(.Error(), "open failed") {
							return retry.Unrecoverable()
						}
						return 
					}
					.Close()
					return nil
				},
				retry.Attempts(2),
				retry.Delay(1*time.Second),
			)

			if  != nil {
				.Do(func() {
					.Started = false
					.Port = 0
					.Stop()
					.log.Errorw("ssh dial error", "details", fmt.Sprintf("%v, %v", , ))
					.errHandler()
				})
				return
			}
			// Dial ends

			.Started = true
			.log.Infow("binded tunnel", "details", )
			.Done()

			defer .log.Infow("collapsed tunnel", "details", )
			defer .errHandler()

			// Accept all incoming connections.
			for {
				,  := .Accept()
				if  != nil {
					.Do(func() {
						.log.Errorw("accept error", "details", fmt.Sprintf("%v, %v", , ))
						.Stop()
						.errHandler()
					})
					return
				}
				.Add(1)

				// The inbound connection is established. Make sure we close it eventually.
				go .dialTunnel(, &, , )
			}
		}()

		select {
		case <-.Done():
			return
		case <-time.After(30 * time.Second):
			fmt.Printf("(%v) retrying...\n", )
		}
	}
}

// dialTunnel dials connection and waits until context get cancelled
func ( *Tunnel) ( context.Context,  *sync.WaitGroup,  *ssh.Client,  net.Conn) {
	defer .Done()

	// The inbound connection is established. Make sure we close it eventually.
	,  := context.WithCancel()
	defer ()
	go func() {
		<-.Done()
		.Close()
	}()

	// Establish the outbound connection.
	var  sync.Once
	var  net.Conn
	var  error

	 = retry.Do(
		func() error {
			switch .mode {
			case '>':
				,  = .Dial(.dialType, .dialAddr)
			case '<':
				,  = net.Dial(.dialType, .dialAddr)
			}

			if  != nil {
				if strings.Contains(.Error(), "open failed") {
					return retry.Unrecoverable()
				}
				return 
			}
			return nil
		},
		retry.Attempts(2),
		retry.Delay(1*time.Second),
	)
	if  != nil {
		.Do(func() {
			.Stop()
			.log.Errorw("ssh dial error", "details", fmt.Sprintf("%v, %v", , ))
			.errHandler()
		})
		return
	}

	go func() {
		<-.Done()
		.Close()
	}()

	// Copy bytes from one connection to the other until one side closes.
	var  sync.WaitGroup
	.Add(2)
	go func() {
		defer .Done()
		defer ()
		if ,  := io.Copy(, );  != nil {
			.Do(func() {
				.Stop()
				.errHandler()
				.log.Errorw("connection error", "details", fmt.Sprintf("%v, %v", , ))
			})
		}
	}()
	go func() {
		defer .Done()
		defer ()
		if ,  := io.Copy(, );  != nil {
			.Do(func() {
				.Stop()
				.errHandler()
				.log.Errorw("connection error", "details", fmt.Sprintf("%v, %v", , ))
			})
		}
	}()
	.Wait()
}

// String returns readable format of tunnel struct
func ( *Tunnel) () string {
	var ,  string
	 := "<?>"
	switch .mode {
	case '>':
		, ,  = .bindAddr, "->", .dialAddr
	case '<':
		, ,  = .dialAddr, "<-", .bindAddr
	}
	return fmt.Sprintf("%s@%s | %s %s %s", .user, .hostAddr, , , )
}

// CreateTunnel starts a new tunnel instance and sets it into TunnelPool
func (, , , ,  string) int {
	// Creating a tunnel cannot exceed 25 seconds
	 := make(chan int)
	time.AfterFunc(25*time.Second, func() {
		 <- 1
	})

	// Check if a tunnel exists with this remoteHost, remotePort and username
	,  := Tunnels.Get(, , )

	// Check if existing tunnel started, if not wait until starts (max: 25sec)
	if  == nil {
		if .password !=  {
			return 0
		}

	:
		for {
			if .Started {
				break
			}

			select {
			case <-:
				break 
			case <-.ctx.Done():
				break 
			default:
				time.Sleep(10 * time.Millisecond)
				continue
			}
		}

		.LastConnection = time.Now()
		return .Port
	}

	// This part from now creates a new tunnel
	,  := freeport.GetFreePort()
	if  != nil {
		logger.Sugar().Errorw(.Error())
		return 0
	}

	 := net.JoinHostPort("127.0.0.1", )
	 := "tcp"

	if ,  := strconv.Atoi();  != nil {
		 = 
		 = "unix"
	}

	,  := context.WithCancel(context.Background())
	 := &Tunnel{
		auth:     []ssh.AuthMethod{ssh.RetryableAuthMethod(ssh.Password(), 3)},
		hostKeys: ssh.InsecureIgnoreHostKey(),
		user:     ,
		mode:     '>',
		hostAddr: net.JoinHostPort(, ),
		dialAddr: ,
		dialType: ,
		bindAddr: net.JoinHostPort("127.0.0.1", fmt.Sprintf("%d", )),
		log:      logger.Sugar(),
		errHandler: func() {
			Tunnels.Delete( + ":" +  + ":" + )
		},
		password:       ,
		Port:           ,
		LastConnection: time.Now(),
		Started:        false,
		ctx:            ,
		cancel:         ,
	}

	Tunnels.Set(, , , )
	go .Start()

:
	for {
		if .Started {
			break
		}

		select {
		case <-:
			break 
		case <-.ctx.Done():
			break 
		default:
			time.Sleep(10 * time.Millisecond)
			continue
		}
	}

	if !.Started {
		()
		return 0
	}

	return .Port
}