package chips

import (
	"bufio"
	"encoding/json"
	"fmt"
	"log/slog"
	"net"
	"net/http"
	"net/url"
	"os"
	"strings"
	"sync"
	"time"

	"github.com/gorilla/websocket"
)

// ConnectionState represents the state of the guacd connection
type ConnectionState int

const (
	StateOpening ConnectionState = iota
	StateOpen
	StateClosed
)

// GuacdOptions holds configuration for guacd connection
type GuacdOptions struct {
	Host string
	Port int
}

// // ConnectionSettings holds the connection configuration
// type ConnectionSettings struct {
// 	Width    int
// 	Height   int
// 	DPI      int
// 	Audio    []string
// 	Video    []string
// 	Image    []string
// 	Timezone []string
// 	// Additional connection parameters as map for dynamic access
// 	Parameters map[string]string
// }

var upgrader = websocket.Upgrader{
	CheckOrigin: func(r *http.Request) bool {
		return true // Allow all origins for simplicity, adjust as needed
	},
	ReadBufferSize:  1024,
	WriteBufferSize: 1024,
	Subprotocols:    []string{"guacamole"},
}

// GuacdClient represents a client connection to guacd
type GuacdClient struct {
	id                    string
	state                 ConnectionState
	logger                *slog.Logger
	connectionSelector    string
	connectionSettings    map[string]interface{}
	guacamoleConnectionID string
	lastActivity          time.Time
	sendBuffer            string
	parser                *Parser
	connection            net.Conn
	ws                    *websocket.Conn
	query                 string
	dataChan              chan string
	stopChan              chan error
	closeChan             chan error

	// Mutex for thread safety
	mu sync.RWMutex
}

// NewGuacdClient creates a new GuacdClient
func NewGuacdClient(websocket *websocket.Conn, query string, logger *slog.Logger, settingsFunc SettingsFunc) (*GuacdClient, error) {
	client := &GuacdClient{
		ws:           websocket,
		query:        query,
		state:        StateOpening,
		logger:       logger,
		lastActivity: time.Now(),
		sendBuffer:   "",
		parser:       NewParser(),
		stopChan:     make(chan error, 1),
		closeChan:    make(chan error, 1),
		dataChan:     make(chan string, 100), // Buffered channel for data
	}

	// Set up parser instruction handler
	client.parser.OnInstruction = client.processInstruction

	params, err := url.ParseQuery(query)
	if err != nil {
		return nil, fmt.Errorf("failed to parse query: %w", err)
	}

	token := params.Get("token")
	if token == "" {
		return nil, fmt.Errorf("%w: token is required", ErrEmptyQueryString)
	}

	token, err = url.QueryUnescape(token)
	if err != nil {
		return nil, fmt.Errorf("failed to unescape token: %w", err)
	}

	decrypted_token, err := decryptToken(token)
	if err != nil {
		return nil, fmt.Errorf("failed to decrypt token: %w", err)
	}

	var tokenObj TokenObject

	err = json.Unmarshal([]byte(decrypted_token), &tokenObj)
	if err != nil {
		return nil, fmt.Errorf("failed to unmarshal connection settings: %w", err)
	}

	if tokenObj.Id != "" {
		client.id = tokenObj.Id
	}

	mergeConnectionSettings(tokenObj.ConnType, &tokenObj.Settings, params)

	err = settingsFunc(&tokenObj)
	if err != nil {
		websocket.Close()
		return nil, fmt.Errorf("failed to run settings callback: %w", err)
	}

	logger.Info("Token object", "tokenObj", tokenObj)

	client.connectionSelector = tokenObj.ConnType
	client.connectionSettings = tokenObj.Settings

	guac_host := os.Getenv("CHIPS_GUAC_HOST")
	guac_port := os.Getenv("CHIPS_GUAC_PORT")

	if val, ok := tokenObj.Settings["guac_host"]; ok {
		guac_host = val.(string)
	}

	if val, ok := tokenObj.Settings["guac_port"]; ok {
		guac_port = val.(string)
	}

	// Connect to guacd
	address := fmt.Sprintf("%v:%v", guac_host, guac_port)
	conn, err := net.Dial("tcp", address)
	if err != nil {
		slog.Default().Error("Failed to connect to guacd", "address", address, "err", err)
		websocket.Close()
		return nil, fmt.Errorf("failed to connect to guacd: %w", err)
	}
	client.connection = conn

	go client.Handle()

	// Start goroutines for handling connection
	go client.activityChecker()

	return client, nil
}

func (gc *GuacdClient) GetConnectionId() string {
	gc.mu.RLock()
	defer gc.mu.RUnlock()
	if gc.id == "" {
		return gc.guacamoleConnectionID
	}
	return gc.id
}

func (gc *GuacdClient) Handle() {

	gc.sendInstruction([]interface{}{"select", gc.connectionSelector})

	// This go routine handles the reading of data from the Guacd connection
	// and processes it by using the parser. It will then send the data from
	// the guacd conn and forward it to the websocket connection through
	// the data channel.
	go func() {
		reader := bufio.NewReader(gc.connection)
		buffer := make([]byte, 4096)

		for {
			select {
			case <-gc.closeChan:
				gc.logger.Info("close signal received, stopping read loop")
				gc.connection.Close()
				return
			default:
				// Set read timeout to make it non-blocking
				gc.connection.SetReadDeadline(time.Now().Add(100 * time.Millisecond))
				n, err := reader.Read(buffer)
				if err != nil {
					if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
						continue // Timeout, continue reading
					}
					gc.logger.Error("Error reading from guacd connection", "err", err)
					// Real error or connection closed
					gc.Close(err)
					return
				}

				if n > 0 {
					data := string(buffer[:n])
					gc.processReceivedData(data)
				}
			}
		}
	}()

	//TODO: Build go routine that takes data from the data channel and sends to websocket
	go func() {
		for {
			select {
			case data := <-gc.dataChan:
				if gc.ws != nil {
					err := gc.ws.WriteMessage(websocket.TextMessage, []byte(data))
					if err != nil {
						gc.logger.Error("failed to write message to websocket", "err", err)
						gc.Close(err)
						if gc.ws != nil {
							gc.ws.Close()
							gc.ws = nil
						}
						return
					}
				}
			// case err := <-gc.stopChan:
			// 	gc.logger.Info("Stopping GuacdClient", "err", err)
			// 	gc.Close(err)
			// 	return
			case err := <-gc.closeChan:
				gc.logger.Info("close event recieved, closing websocket", "err", err)
				if gc.ws != nil {
					gc.ws.Close()
					gc.ws = nil
				}
				return
			}
		}
	}()

	// this.webSocket.send(message, {binary: false, mask: false}, (error) => {
	//         if (error && ![this.webSocket.CLOSED, this.webSocket.CLOSING].includes(this.webSocket.readyState)) {
	//             this.close(error);
	//         }
	//     });
	go func() {
		for {
			select {
			case <-gc.closeChan:
				if gc.ws != nil {
					gc.ws.Close()
					gc.ws = nil
				}

			default:
				// Read messages from the websocket connection
				_, message, err := gc.ws.ReadMessage()
				if err != nil {
					gc.logger.Error("failed to read message from websocket", "err", err)
					gc.Close(err)
					if gc.ws != nil {
						gc.ws.Close()
					}
					return
				}

				// Process the received message
				gc.Send(string(message), true)
			}
		}

	}()

	//TODO: Build go routine that takes data from websocket connection and sends to guacd connection

}

func (gc *GuacdClient) State() ConnectionState {
	gc.mu.RLock()
	defer gc.mu.RUnlock()
	return gc.state
}

// activityChecker monitors connection activity
func (gc *GuacdClient) activityChecker() {
	ticker := time.NewTicker(1 * time.Second)
	defer ticker.Stop()

	for {
		select {
		case <-ticker.C:
			gc.mu.RLock()
			if time.Since(gc.lastActivity) > 10*time.Second {
				gc.mu.RUnlock()
				gc.logger.Info("GuacdClient inactive for too long, closing connection")
				gc.Close(fmt.Errorf("guacd was inactive for too long"))
				return
			}
			gc.mu.RUnlock()
		case <-gc.closeChan:
			gc.logger.Info("GuacdClient close signal received, stopping activity checker")
			return
		}
	}
}

// Close closes the guacd connection
func (gc *GuacdClient) Close(err error) {
	gc.mu.Lock()
	defer gc.mu.Unlock()

	if gc.state == StateClosed {
		return
	}

	if err != nil {
		gc.logger.Info("closing guacd connection due to error", "err", err)
	}

	gc.logger.Info("closing guacd connection")

	// // Stop activity checker
	// close(gc.stopChan)

	// Close connection
	if gc.connection != nil {
		gc.connection.Close()
	}

	gc.state = StateClosed

	// Send close event
	select {
	case gc.closeChan <- err:
	default:
	}
}

// Send sends data to guacd
func (gc *GuacdClient) Send(data string, afterOpened bool) {
	gc.mu.Lock()
	defer gc.mu.Unlock()

	if gc.state == StateClosed {
		return
	}

	if afterOpened && gc.state == StateOpening {
		gc.sendBuffer += data
		return
	}

	gc.logger.Debug("sending data to guacd", "data", data)

	_, err := gc.connection.Write([]byte(data))
	if err != nil {
		gc.Close(err)
	}
}

// sendHandshakeReply responds to the guacd handshake
func (gc *GuacdClient) sendHandshakeReply(serverHandshake []string) {
	protocolVersion := "1_0_0"

	gc.logger.Info("Sending guacd handshake reply", "protocol_version", protocolVersion)

	var connectArgs []interface{}

	for _, argName := range serverHandshake {
		var arg interface{}
		if strings.HasPrefix(argName, "VERSION_") {
			version := argName[8:] // Remove "VERSION_" prefix

			// Support 1.0.0 and 1.1.0, default to 1.1.0 for unknown versions
			if version == "1_0_0" || version == "1_1_0" {
				protocolVersion = version
			} else {
				protocolVersion = "1_1_0"
			}

			arg = "VERSION_" + protocolVersion
		} else {
			if val, exists := gc.connectionSettings[argName]; exists {
				arg = val
			} else {
				arg = ""
			}
		}

		connectArgs = append(connectArgs, arg)
	}

	// Send size instruction
	gc.sendInstruction([]interface{}{
		"size",
		gc.connectionSettings["width"],
		gc.connectionSettings["height"],
		gc.connectionSettings["dpi"],
	})

	// Send audio instruction
	audioArgs := []interface{}{"audio"}
	for _, codec := range gc.connectionSettings["audio"].([]string) {
		audioArgs = append(audioArgs, codec)
	}
	gc.sendInstruction(audioArgs)

	// Send video instruction
	videoArgs := []interface{}{"video"}
	vid, ok := gc.connectionSettings["video"]
	if ok && vid != nil {
		for _, codec := range gc.connectionSettings["video"].([]string) {
			videoArgs = append(videoArgs, codec)
		}
	}
	gc.sendInstruction(videoArgs)

	// Send image instruction
	imageArgs := []interface{}{"image"}
	img, ok := gc.connectionSettings["image"]
	if ok && img != nil {
		for _, format := range gc.connectionSettings["image"].([]string) {
			imageArgs = append(imageArgs, format)
		}
	}
	gc.sendInstruction(imageArgs)

	// Send timezone instruction for protocol version 1.1.0
	if protocolVersion == "1_1_0" {
		timezoneArgs := []interface{}{"timezone"}
		tz, ok := gc.connectionSettings["timezone"]
		if ok && tz != nil {
			// If timezone is set, send each timezone as an argument
			for _, tz := range gc.connectionSettings["timezone"].([]string) {
				timezoneArgs = append(timezoneArgs, tz)
			}
		}
		gc.sendInstruction(timezoneArgs)
	}

	// Send connect instruction
	connectInstruction := []interface{}{"connect"}
	connectInstruction = append(connectInstruction, connectArgs...)
	gc.sendInstruction(connectInstruction)

	gc.logger.Info("Handshake reply sent to guacd", "protocol_version", protocolVersion)
}

// sendInstruction sends a Guacamole instruction to guacd
func (gc *GuacdClient) sendInstruction(instruction []interface{}) {
	// Convert elements to strings, handling null values
	for i, element := range instruction {
		if element == nil {
			instruction[i] = ""
		} else {
			instruction[i] = fmt.Sprintf("%v", element)
		}
	}

	instructionString := ToInstruction(instruction)
	gc.Send(instructionString, false)
}

// processReceivedData processes data received from guacd
func (gc *GuacdClient) processReceivedData(data string) {
	gc.mu.Lock()
	gc.lastActivity = time.Now()
	gc.mu.Unlock()

	gc.logger.Debug("Received data from guacd", "data", data)

	err := gc.parser.Receive(data, false)
	if err != nil {
		gc.logger.Error("Failed to parse received data", "err", err)
		gc.Close(err)
	}
}

// processInstruction handles parsed instructions from guacd
func (gc *GuacdClient) processInstruction(opcode string, params []string) {
	switch opcode {
	case "args":
		// Handle server handshake
		gc.sendHandshakeReply(params)
		return // Do not forward to client

	case "ready":
		// Handle ready instruction
		if len(params) > 0 {
			gc.guacamoleConnectionID = params[0]
		}
		gc.logger.Info("Guacd connection is ready", "connection_id", gc.guacamoleConnectionID)

		gc.mu.Lock()
		if gc.state != StateOpen {
			gc.state = StateOpen

			// Send buffered data if any
			if gc.sendBuffer != "" {
				gc.Send(gc.sendBuffer, false)
				gc.sendBuffer = ""
			}
		}
		gc.mu.Unlock()

		instruction := ToInstruction([]interface{}{"", gc.guacamoleConnectionID})
		gc.dataChan <- instruction
		return
	default:
		gc.logger.Debug("Processing instruction", "opcode", opcode, "params", params)
		ins := genericSliceToInterfaceSlice(append([]string{opcode}, params...))
		instruction := ToInstruction(ins)
		gc.dataChan <- instruction
	}
}

func mergeConnectionSettings(connType string, settings *map[string]interface{}, params url.Values) {

	var defaults map[string]interface{}
	var defaultAllowed []string
	switch connType {
	case "rdp":
		defaults = defaultGuacClientOptions.DefaultConnectionSettings.RDP
		defaultAllowed = defaultGuacClientOptions.AllowedUnencryptedSettings.RDP
	case "vnc":
		defaults = defaultGuacClientOptions.DefaultConnectionSettings.VNC
		defaultAllowed = defaultGuacClientOptions.AllowedUnencryptedSettings.VNC
	case "ssh":
		defaults = defaultGuacClientOptions.DefaultConnectionSettings.SSH
		defaultAllowed = defaultGuacClientOptions.AllowedUnencryptedSettings.SSH
	case "telnet":
		defaults = defaultGuacClientOptions.DefaultConnectionSettings.Telnet
		defaultAllowed = defaultGuacClientOptions.AllowedUnencryptedSettings.Telnet
	case "join":
		defaults = defaultGuacClientOptions.DefaultConnectionSettings.Join
		defaultAllowed = defaultGuacClientOptions.AllowedUnencryptedSettings.Join
	default:
		defaults = make(map[string]interface{})
		defaultAllowed = []string{}
	}

	for key, val := range defaults {
		if _, exists := (*settings)[key]; !exists {
			(*settings)[key] = val
		}
	}

	for key, val := range params {
		v := strings.Join(val, ",")
		for _, allowed := range defaultAllowed {
			if key == allowed {
				(*settings)[key] = v
				break
			}
		}
	}
}
