package client

import (
	"crypto/tls"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net"
	"sync"
	"time"

	"golang.org/x/crypto/ssh"
)

// TunnelRequest is the metadata sent to the server on the tunnel-request channel.
type TunnelRequest struct {
	Domain   string `json:"domain"`
	AuthUser string `json:"auth_user,omitempty"` // optional HTTP Basic Auth username
	AuthPass string `json:"auth_pass,omitempty"` // optional HTTP Basic Auth password
}

// SetupTunnel sends domain metadata and establishes a reverse port forward.
// The server will allocate a port and register Traefik routes for the domain.
// localHost and localPort are the backend address (e.g. "127.0.0.1" or "192.168.0.2", 11001).
// Backend TLS is detected dynamically: TLS is tried first; on failure, plain TCP is used.
// authUser and authPass are optional; if both are non-empty, the server will
// add a Traefik basicauth middleware in front of this tunnel.
func SetupTunnel(client *ssh.Client, domain string, localHost string, localPort int, authUser, authPass string) error {
	// Step 1: Open custom channel to send domain metadata.
	if err := sendMetadata(client, domain, authUser, authPass); err != nil {
		return fmt.Errorf("send metadata: %w", err)
	}

	// Step 2: Request reverse port forward.
	// We use the domain as the bind address so the server can associate it.
	listener, err := client.Listen("tcp", fmt.Sprintf("%s:0", domain))
	if err != nil {
		return fmt.Errorf("reverse listen: %w", err)
	}
	defer listener.Close()

	log.Printf("Reverse tunnel established: %s -> %s:%d", domain, localHost, localPort)

	// Step 3: Accept connections from the server and forward to local service.
	for {
		conn, err := listener.Accept()
		if err != nil {
			return fmt.Errorf("tunnel accept: %w", err)
		}
		go forwardToLocal(conn, localHost, localPort)
	}
}

// sendMetadata opens a custom channel and sends the tunnel request JSON.
func sendMetadata(client *ssh.Client, domain, authUser, authPass string) error {
	ch, _, err := client.OpenChannel("tunnel-request", nil)
	if err != nil {
		return fmt.Errorf("open tunnel-request channel: %w", err)
	}

	req := TunnelRequest{Domain: domain, AuthUser: authUser, AuthPass: authPass}
	data, err := json.Marshal(req)
	if err != nil {
		ch.Close()
		return fmt.Errorf("marshal metadata: %w", err)
	}

	if _, err := ch.Write(data); err != nil {
		ch.Close()
		return fmt.Errorf("write metadata: %w", err)
	}

	if authUser != "" {
		log.Printf("Sent tunnel metadata: domain=%s (with basicauth)", domain)
	} else {
		log.Printf("Sent tunnel metadata: domain=%s", domain)
	}

	// Keep the channel open in a goroutine for disconnect detection.
	go func() {
		io.Copy(io.Discard, ch)
		log.Printf("Metadata channel closed for %s", domain)
	}()

	return nil
}

// forwardToLocal connects an incoming tunnel connection to the backend.
// It tries TLS first; if the backend does not speak TLS, it falls back to plain TCP.
func forwardToLocal(remoteConn net.Conn, localHost string, localPort int) {
	defer remoteConn.Close()

	addr := fmt.Sprintf("%s:%d", localHost, localPort)
	localConn, _ := dialBackend(addr, localHost)
	if localConn == nil {
		return
	}
	defer localConn.Close()

	// Bidirectional copy.
	var wg sync.WaitGroup
	wg.Add(2)
	go func() {
		defer wg.Done()
		io.Copy(localConn, remoteConn)
	}()
	go func() {
		defer wg.Done()
		io.Copy(remoteConn, localConn)
	}()
	wg.Wait()
}

// dialBackend tries TLS first; on failure (e.g. backend is plain HTTP), uses plain TCP.
// Returns (conn, true) if TLS was used, (conn, false) if plain, (nil, false) on error.
func dialBackend(addr, serverName string) (net.Conn, bool) {
	tlsConn, err := tls.DialWithDialer(&net.Dialer{Timeout: 5 * time.Second}, "tcp", addr, &tls.Config{
		ServerName:         serverName,
		InsecureSkipVerify: true,
	})
	if err == nil {
		return tlsConn, true
	}
	// Fall back to plain TCP (backend is HTTP or not TLS).
	plainConn, err := net.DialTimeout("tcp", addr, 5*time.Second)
	if err != nil {
		log.Printf("failed to connect to backend at %s: %v", addr, err)
		return nil, false
	}
	return plainConn, false
}
