package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"io"
	"log"
	"net/http"
	"os"
	"strconv"
	"strings"
	"sync"
	"syscall"
	"time"

	"github.com/gorilla/websocket"
)

// ValidateToken checks an HMAC-SHA256 signed token of the form "signature.timestamp".
// It validates the timestamp is within ttl seconds (not too old) and within 5 seconds
// of clock skew (not too far in the future), then verifies the HMAC signature.
func ValidateToken(token, secret string, ttl int64) bool {
	parts := strings.SplitN(token, ".", 2)
	if len(parts) != 2 {
		return false
	}
	sig := parts[0]
	tsStr := parts[1]

	ts, err := strconv.ParseInt(tsStr, 10, 64)
	if err != nil {
		return false
	}

	now := time.Now().Unix()
	if now-ts > ttl {
		return false
	}
	if ts-now > 5 {
		return false
	}

	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write([]byte(tsStr))
	expected := hex.EncodeToString(mac.Sum(nil))

	return hmac.Equal([]byte(sig), []byte(expected))
}

var upgrader = websocket.Upgrader{
	CheckOrigin: func(r *http.Request) bool {
		host := r.Host
		if strings.Contains(host, ":") {
			host = strings.Split(host, ":")[0]
		}
		return host == "localhost" || host == "127.0.0.1"
	},
}

// HandleWebSocket validates the token query parameter and upgrades the connection
// to a WebSocket, then runs the main read loop for control and binary messages.
func HandleWebSocket(pool *Pool, secret string, w http.ResponseWriter, r *http.Request) {
	token := r.URL.Query().Get("token")
	if !ValidateToken(token, secret, 30) {
		http.Error(w, "forbidden", http.StatusForbidden)
		return
	}

	conn, err := upgrader.Upgrade(w, r, nil)
	if err != nil {
		log.Printf("ws upgrade: %v", err)
		return
	}
	defer conn.Close()

	var (
		mu            sync.Mutex
		activeSession string
		activePTY     *os.File
		ptyCancel     chan struct{}
	)

	startRelay := func(sess *Session, name string) {
		cancel := make(chan struct{})
		mu.Lock()
		ptyCancel = cancel
		mu.Unlock()

		go func() {
			buf := make([]byte, 32*1024)
			for {
				select {
				case <-cancel:
					return
				default:
				}
				n, err := sess.PTY.Read(buf)
				if n > 0 {
					if writeErr := conn.WriteMessage(websocket.BinaryMessage, buf[:n]); writeErr != nil {
						return
					}
				}
				if err != nil {
					if err == io.EOF || strings.Contains(err.Error(), "input/output error") {
						conn.WriteMessage(websocket.TextMessage, MarshalExited(name))
					}
					return
				}
			}
		}()
	}

	stopRelay := func() {
		mu.Lock()
		defer mu.Unlock()
		if ptyCancel != nil {
			close(ptyCancel)
			ptyCancel = nil
		}
	}

	defer stopRelay()

	for {
		msgType, data, err := conn.ReadMessage()
		if err != nil {
			return
		}

		switch msgType {
		case websocket.TextMessage:
			kind, msg := ParseControlMessage(data)
			switch kind {
			case "attach":
				attach := msg.(*AttachMsg)
				stopRelay()

				sess, err := pool.Attach(attach.Attach, 80, 24)
				if err != nil {
					conn.WriteMessage(websocket.TextMessage, MarshalError(err.Error()))
					continue
				}

				mu.Lock()
				activeSession = attach.Attach
				activePTY = sess.PTY
				mu.Unlock()

				sendSIGWINCH(sess)
				startRelay(sess, attach.Attach)
				conn.WriteMessage(websocket.TextMessage, MarshalAttached(attach.Attach))

			case "resize":
				resize := msg.(*ResizeMsg)
				mu.Lock()
				name := activeSession
				mu.Unlock()
				if name != "" {
					pool.Resize(name, uint16(resize.Resize.Cols), uint16(resize.Resize.Rows))
				}

			case "detach":
				stopRelay()
				mu.Lock()
				activeSession = ""
				activePTY = nil
				mu.Unlock()
			}

		case websocket.BinaryMessage:
			mu.Lock()
			pty := activePTY
			mu.Unlock()
			if pty != nil {
				pty.Write(data)
			}
		}
	}
}

// sendSIGWINCH sends SIGWINCH to the session's process to trigger tmux repaint.
func sendSIGWINCH(s *Session) {
	if s == nil || s.Cmd == nil || s.Cmd.Process == nil {
		return
	}
	s.Cmd.Process.Signal(syscall.SIGWINCH)
}