package api

import (
	"net"
	"net/http"
	"os"
	"strconv"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/mrhid6/vantage/server/internal/auth"
	"github.com/mrhid6/vantage/server/internal/services"
	"github.com/wwt/guac"
)

func consoleConnect(c *gin.Context) {
	var body struct {
		ServerID    string `json:"server_id" binding:"required"`
		Protocol    string `json:"protocol" binding:"required"`
		KeyID       string `json:"key_id"`
		RDPUsername string `json:"rdp_username"`
		RDPPassword string `json:"rdp_password"`
		SSHUsername string `json:"ssh_username"`
	}
	if err := c.ShouldBindJSON(&body); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	srv, err := services.GetServer(auth.InstanceID(c), body.ServerID)
	if err != nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "server not found"})
		return
	}

	sess, err := services.CreateConsoleSession(auth.InstanceID(c), body.ServerID, body.Protocol, body.KeyID, actorFromCtx(c), c.ClientIP())
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}
	token, err := services.SignSessionToken(sess.SessionID, time.Minute)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}

	if (body.Protocol == "rdp" || body.Protocol == "vnc") && (body.RDPUsername != "" || body.RDPPassword != "") {
		if err := services.StashConsoleRDPCreds(auth.InstanceID(c), sess.SessionID, body.RDPUsername, body.RDPPassword); err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}
	}

	if body.Protocol == "ssh" {
		if err := services.SetConsoleSSHUser(auth.InstanceID(c), sess.SessionID, body.SSHUsername); err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}
	}

	services.LogEvent(auth.InstanceID(c), "console.opened", actorFromCtx(c), srv.ServerID, "",
		"console session opened ("+body.Protocol+")")

	c.JSON(http.StatusOK, gin.H{
		"session_id": sess.SessionID,
		"token":      token,
		"ws_path":    "/api/console/tunnel",
	})
}

func queryIntDefault(r *http.Request, key string, def int) int {
	v, err := strconv.Atoi(r.URL.Query().Get(key))
	if err != nil || v <= 0 {
		return def
	}
	return v
}

func consoleTunnel(c *gin.Context) {
	token := c.Query("token")
	sessionID, err := services.VerifySessionToken(token)
	if err != nil {
		c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid token"})
		return
	}
	instanceID := auth.InstanceID(c)
	sess, err := services.GetConsoleSession(instanceID, sessionID)
	if err != nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "session not found"})
		return
	}

	if actor := actorFromCtx(c); actor != sess.User {
		c.JSON(http.StatusForbidden, gin.H{"error": "session belongs to another user"})
		return
	}

	if err := services.ConsumeSessionToken(instanceID, sessionID); err != nil {
		c.JSON(http.StatusUnauthorized, gin.H{"error": "token already used"})
		return
	}

	srv, err := services.GetServer(auth.InstanceID(c), sess.ServerID)
	if err != nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "server not found"})
		return
	}

	var privKey, passphrase string
	if sess.Protocol == "ssh" && sess.KeyID != "" {
		privKey, err = services.GetPrivateKey(auth.InstanceID(c), sess.KeyID)
		if err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": "selected key has no private material"})
			return
		}
		passphrase, _ = services.GetPassphrase(sess.KeyID)
	}

	var rdpUser, rdpPass string
	if sess.Protocol == "rdp" || sess.Protocol == "vnc" {
		rdpUser, rdpPass, err = services.ConsumeConsoleRDPCreds(instanceID, sessionID)
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": "could not load credentials"})
			return
		}
	}
	gp, err := services.BuildGuacParams(srv, sess.Protocol, sess.SSHUsername, privKey, passphrase, rdpUser, rdpPass)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	guacdAddr := os.Getenv("GUACD_ADDR")
	if guacdAddr == "" {
		guacdAddr = "guacd:4822"
	}

	connect := func(r *http.Request) (guac.Tunnel, error) {
		config := guac.NewGuacamoleConfiguration()
		config.Protocol = gp.Protocol
		for k, v := range gp.Params {
			config.Parameters[k] = v
		}
		config.OptimalScreenWidth = queryIntDefault(r, "width", 1024)
		config.OptimalScreenHeight = queryIntDefault(r, "height", 768)
		config.OptimalResolution = queryIntDefault(r, "dpi", 96)

		addr, err := net.ResolveTCPAddr("tcp", guacdAddr)
		if err != nil {
			return nil, err
		}
		conn, err := net.DialTCP("tcp", nil, addr)
		if err != nil {
			return nil, err
		}
		stream := guac.NewStream(conn, guac.SocketTimeout)
		if err := stream.Handshake(config); err != nil {
			return nil, err
		}
		return guac.NewSimpleTunnel(stream), nil
	}

	wsServer := guac.NewWebsocketServer(connect)
	wsServer.OnDisconnect = func(id string, r *http.Request, t guac.Tunnel) {
		_ = services.EndConsoleSession(instanceID, sessionID)
	}
	wsServer.ServeHTTP(c.Writer, c.Request)
}
