package agent

import (
	"crypto/tls"
	"encoding/json"
	"flag"
	"fmt"
	"io"
	"log"
	"net"
	"os"
	"os/signal"
	"syscall"
	"time"

	"github.com/xtaci/smux"
	"github.com/wormhole/wormhole/internal/bridge"
	"github.com/wormhole/wormhole/internal/protocol"
	"github.com/wormhole/wormhole/internal/version"
)

func Run(args []string) {
	fs := flag.NewFlagSet("agent", flag.ExitOnError)
	serverAddr := fs.String("server", "127.0.0.1:8528", "wormhole server bridge address")
	verifyKey := fs.String("key", "", "client verify key")
	useTLS := fs.Bool("tls", false, "use TLS for bridge connection")
	fs.Parse(args)

	if *verifyKey == "" {
		log.Fatal("verify key required: -key")
	}

	sig := make(chan os.Signal, 1)
	signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)

	backoff := time.Second
	for {
		select {
		case <-sig:
			log.Println("[agent] shutting down")
			return
		default:
		}

		shutdown, hadSession := runSession(*serverAddr, *verifyKey, *useTLS, sig)
		if shutdown {
			return
		}
		if hadSession {
			backoff = time.Second
		}

		log.Printf("[agent] reconnecting in %v...", backoff)
		select {
		case <-sig:
			log.Println("[agent] shutting down")
			return
		case <-time.After(backoff):
		}
		if backoff < 30*time.Second {
			backoff *= 2
		}
	}
}

func dialBridge(addr string, useTLS bool) (net.Conn, error) {
	var conn net.Conn
	var err error
	if useTLS {
		conn, err = tls.Dial("tcp", addr, &tls.Config{
			MinVersion:         tls.VersionTLS12,
			InsecureSkipVerify: true,
		})
	} else {
		conn, err = net.Dial("tcp", addr)
	}
	if err != nil {
		return nil, err
	}
	bridge.SetTCPKeepAlive(conn)
	return conn, nil
}

// runSession connects and blocks until disconnect.
// Returns shutdown=true when SIGINT/SIGTERM received; hadSession=true if registration succeeded.
func runSession(serverAddr, verifyKey string, useTLS bool, sig chan os.Signal) (shutdown, hadSession bool) {
	conn, err := dialBridge(serverAddr, useTLS)
	if err != nil {
		log.Printf("[agent] dial server: %v", err)
		return false, false
	}
	defer conn.Close()

	if err := protocol.WriteMessage(conn, protocol.MsgRegister, protocol.RegisterPayload{
		VerifyKey: verifyKey,
		Version:   version.Version,
	}); err != nil {
		log.Printf("[agent] register: %v", err)
		return false, false
	}
	msgType, body, err := protocol.ReadMessage(conn)
	if err != nil {
		log.Printf("[agent] register ack: %v", err)
		return false, false
	}
	if msgType != protocol.MsgRegisterAck {
		log.Printf("[agent] unexpected register response")
		return false, false
	}
	var ack protocol.RegisterAckPayload
	if len(body) > 0 {
		if err := json.Unmarshal(body, &ack); err != nil {
			log.Printf("[agent] parse ack: %v", err)
			return false, false
		}
	}
	if !ack.OK {
		log.Printf("[agent] register failed: %s", ack.Message)
		return false, false
	}
	log.Printf("[agent] registered as client %d", ack.ClientID)
	hadSession = true

	session, err := smux.Server(conn, bridge.SmuxConfig())
	if err != nil {
		log.Printf("[agent] smux: %v", err)
		return false, hadSession
	}
	defer session.Close()

	sessionDead := make(chan string, 1)
	go func() {
		reason := heartbeat(session)
		sessionDead <- reason
	}()

	go func() {
		for {
			stream, err := session.AcceptStream()
			if err != nil {
				select {
				case sessionDead <- "accept stream: " + err.Error():
				default:
				}
				return
			}
			go handleStream(stream)
		}
	}()

	select {
	case <-sig:
		return true, hadSession
	case reason := <-sessionDead:
		log.Printf("[agent] session closed (%s)", reason)
		return false, hadSession
	}
}

const heartbeatMaxFailures = 3

func heartbeat(session *smux.Session) string {
	ticker := time.NewTicker(bridge.HeartbeatInterval)
	defer ticker.Stop()
	failures := 0
	for range ticker.C {
		if err := pingOnce(session); err != nil {
			failures++
			log.Printf("[agent] heartbeat failed (%d/%d): %v", failures, heartbeatMaxFailures, err)
			if failures >= heartbeatMaxFailures {
				return "heartbeat timeout"
			}
			continue
		}
		failures = 0
	}
	return "heartbeat stopped"
}

func pingOnce(session *smux.Session) error {
	stream, err := session.OpenStream()
	if err != nil {
		return err
	}
	defer stream.Close()
	if err := protocol.WriteMessage(stream, protocol.MsgPing, nil); err != nil {
		return err
	}
	msgType, _, err := protocol.ReadMessage(stream)
	if err != nil {
		return err
	}
	if msgType != protocol.MsgPong {
		return fmt.Errorf("unexpected response type %d", msgType)
	}
	return nil
}

func handleStream(stream *smux.Stream) {
	defer stream.Close()
	msgType, body, err := protocol.ReadMessage(stream)
	if err != nil {
		return
	}
	switch msgType {
	case protocol.MsgPing:
		_ = protocol.WriteMessage(stream, protocol.MsgPong, nil)
		return
	case protocol.MsgOpenStream:
		handleDataStream(stream, body)
	}
}

func handleDataStream(stream *smux.Stream, body []byte) {
	meta, err := protocol.ParseLinkMeta(body)
	if err != nil {
		return
	}
	target, err := net.Dial("tcp", meta.Target)
	if err != nil {
		return
	}
	defer target.Close()
	_ = protocol.WriteMessage(stream, protocol.MsgStreamReady, nil)
	go func() {
		_, _ = io.Copy(target, stream)
	}()
	_, _ = io.Copy(stream, target)
}
