package ws

import (
	"context"
	"encoding/json"
	"fmt"
	"log/slog"
	"net/url"
	"os"
	"runtime"
	"sync"
	"time"

	"github.com/google/uuid"
	"github.com/gorilla/websocket"
	"github.com/hexahost/gamecloud/node-agent/internal/config"
	"github.com/hexahost/gamecloud/node-agent/internal/protocol"
	runtimepkg "github.com/hexahost/gamecloud/node-agent/internal/runtime"
)

const (
	writeWait         = 10 * time.Second
	pongWait          = 60 * time.Second
	pingPeriod        = (pongWait * 9) / 10
	maxReconnectDelay = 60 * time.Second
)

// Client maintains an outbound WebSocket connection to the control plane.
type Client struct {
	cfg          *config.Config
	log          *slog.Logger
	runtime      *runtimepkg.Manager
	agentVersion string

	writeMu sync.Mutex
	conn    *websocket.Conn
}

// NewClient creates a WebSocket client for the control plane.
func NewClient(cfg *config.Config, log *slog.Logger, mgr *runtimepkg.Manager, agentVersion string) *Client {
	return &Client{
		cfg:          cfg,
		log:          log,
		runtime:      mgr,
		agentVersion: agentVersion,
	}
}

// SetRuntime attaches the runtime manager after construction to break init cycles.
func (c *Client) SetRuntime(mgr *runtimepkg.Manager) {
	c.runtime = mgr
}

// Run connects to the control plane and processes messages until ctx is cancelled.
func (c *Client) Run(ctx context.Context) error {
	backoff := c.cfg.ReconnectBackoff
	if backoff <= 0 {
		backoff = 5 * time.Second
	}

	for {
		if ctx.Err() != nil {
			return ctx.Err()
		}

		err := c.session(ctx)
		if ctx.Err() != nil {
			return ctx.Err()
		}

		c.log.Warn("websocket disconnected", "error", err, "retry_in", backoff.String())
		select {
		case <-ctx.Done():
			return ctx.Err()
		case <-time.After(backoff):
		}

		backoff *= 2
		if backoff > maxReconnectDelay {
			backoff = maxReconnectDelay
		}
	}
}

func (c *Client) session(ctx context.Context) error {
	conn, err := c.dial(ctx)
	if err != nil {
		return err
	}

	c.writeMu.Lock()
	c.conn = conn
	c.writeMu.Unlock()

	defer func() {
		c.writeMu.Lock()
		c.conn = nil
		c.writeMu.Unlock()
		_ = conn.Close()
	}()

	conn.SetReadDeadline(time.Now().Add(pongWait))
	conn.SetPongHandler(func(string) error {
		return conn.SetReadDeadline(time.Now().Add(pongWait))
	})

	if err := c.sendHello(ctx); err != nil {
		return fmt.Errorf("send hello: %w", err)
	}

	sessionCtx, cancel := context.WithCancel(ctx)
	defer cancel()

	errCh := make(chan error, 2)
	go func() {
		errCh <- c.heartbeatLoop(sessionCtx)
	}()
	go func() {
		errCh <- c.readLoop(sessionCtx)
	}()

	select {
	case <-ctx.Done():
		cancel()
		return ctx.Err()
	case err := <-errCh:
		cancel()
		return err
	}
}

func (c *Client) dial(ctx context.Context) (*websocket.Conn, error) {
	endpoint, err := c.buildURL()
	if err != nil {
		return nil, err
	}

	dialer := websocket.DefaultDialer
	conn, _, err := dialer.DialContext(ctx, endpoint, nil)
	if err != nil {
		return nil, fmt.Errorf("dial websocket: %w", err)
	}
	c.log.Info("connected to control plane", "url", redactToken(endpoint))
	return conn, nil
}

func (c *Client) buildURL() (string, error) {
	u, err := url.Parse(c.cfg.ControlPlaneURL)
	if err != nil {
		return "", fmt.Errorf("parse control plane url: %w", err)
	}
	q := u.Query()
	q.Set("nodeId", c.cfg.NodeID)
	if c.cfg.NodeToken != "" {
		q.Set("token", c.cfg.NodeToken)
	}
	u.RawQuery = q.Encode()
	return u.String(), nil
}

func (c *Client) sendHello(ctx context.Context) error {
	hostname, _ := os.Hostname()
	payload := protocol.AgentHelloPayload{
		NodeID:          c.cfg.NodeID,
		AgentVersion:    c.agentVersion,
		ProtocolVersion: protocol.CurrentProtocolVersion,
		Hostname:        hostname,
		Capabilities:    []string{"minecraft", "docker"},
	}
	return c.send(ctx, protocol.TypeAgentHello, "", payload)
}

func (c *Client) heartbeatLoop(ctx context.Context) error {
	ticker := time.NewTicker(c.cfg.HeartbeatInterval)
	defer ticker.Stop()

	pingTicker := time.NewTicker(pingPeriod)
	defer pingTicker.Stop()

	for {
		select {
		case <-ctx.Done():
			return ctx.Err()
		case <-ticker.C:
			if err := c.sendHeartbeat(ctx); err != nil {
				return err
			}
		case <-pingTicker.C:
			if err := c.writePing(); err != nil {
				return err
			}
		}
	}
}

func (c *Client) sendHeartbeat(ctx context.Context) error {
	var memStats runtime.MemStats
	runtime.ReadMemStats(&memStats)

	payload := protocol.AgentHeartbeatPayload{
		NodeID:           c.cfg.NodeID,
		CPUUsagePercent:  0,
		MemoryUsedBytes:  int64(memStats.Alloc),
		MemoryTotalBytes: int64(memStats.Sys),
		RunningServers:   c.runtime.RunningCount(),
	}
	return c.send(ctx, protocol.TypeAgentHeartbeat, "", payload)
}

func (c *Client) readLoop(ctx context.Context) error {
	for {
		if ctx.Err() != nil {
			return ctx.Err()
		}

		c.writeMu.Lock()
		conn := c.conn
		c.writeMu.Unlock()
		if conn == nil {
			return fmt.Errorf("connection closed")
		}

		_, data, err := conn.ReadMessage()
		if err != nil {
			return fmt.Errorf("read message: %w", err)
		}

		env, err := protocol.DecodeEnvelope(data)
		if err != nil {
			c.log.Warn("invalid inbound envelope", "error", err)
			continue
		}

		c.dispatch(ctx, env)
	}
}

func (c *Client) dispatch(ctx context.Context, env *protocol.Envelope) {
	correlationID := env.CorrelationID
	if correlationID == "" {
		correlationID = env.MessageID
	}

	switch env.Type {
	case protocol.TypeServerProvision:
		var payload protocol.ServerProvisionPayload
		if err := json.Unmarshal(env.Payload, &payload); err != nil {
			c.log.Warn("decode server.provision", "error", err)
			return
		}
		c.runtime.HandleProvision(ctx, correlationID, payload)

	case protocol.TypeServerStart:
		var payload protocol.ServerStartPayload
		if err := json.Unmarshal(env.Payload, &payload); err != nil {
			c.log.Warn("decode server.start", "error", err)
			return
		}
		c.runtime.HandleStart(ctx, correlationID, payload)

	case protocol.TypeServerStop:
		var payload protocol.ServerStopPayload
		if err := json.Unmarshal(env.Payload, &payload); err != nil {
			c.log.Warn("decode server.stop", "error", err)
			return
		}
		c.runtime.HandleStop(ctx, correlationID, payload)

	case protocol.TypeServerDelete:
		var meta protocol.OperationMeta
		if err := json.Unmarshal(env.Payload, &meta); err != nil {
			c.log.Warn("decode server.delete", "error", err)
			return
		}
		c.runtime.HandleDelete(ctx, correlationID, meta)

	default:
		c.log.Debug("ignored control message", "type", env.Type)
	}
}

func (c *Client) send(ctx context.Context, msgType protocol.MessageType, correlationID string, payload any) error {
	env, err := protocol.NewEnvelope(msgType, uuid.NewString(), payload)
	if err != nil {
		return err
	}
	if correlationID != "" {
		env.CorrelationID = correlationID
	}
	return c.writeEnvelope(ctx, env)
}

func (c *Client) writeEnvelope(ctx context.Context, env *protocol.Envelope) error {
	data, err := json.Marshal(env)
	if err != nil {
		return fmt.Errorf("marshal envelope: %w", err)
	}

	c.writeMu.Lock()
	conn := c.conn
	c.writeMu.Unlock()
	if conn == nil {
		return fmt.Errorf("not connected")
	}

	deadline, ok := ctx.Deadline()
	if !ok {
		deadline = time.Now().Add(writeWait)
	}
	if err := conn.SetWriteDeadline(deadline); err != nil {
		return err
	}

	c.writeMu.Lock()
	defer c.writeMu.Unlock()
	if c.conn == nil {
		return fmt.Errorf("not connected")
	}
	return c.conn.WriteMessage(websocket.TextMessage, data)
}

func (c *Client) writePing() error {
	c.writeMu.Lock()
	defer c.writeMu.Unlock()
	if c.conn == nil {
		return fmt.Errorf("not connected")
	}
	return c.conn.WriteMessage(websocket.PingMessage, nil)
}

// SendState implements runtime.Responder.
func (c *Client) SendState(ctx context.Context, correlationID string, payload protocol.ServerStatePayload) error {
	return c.send(ctx, protocol.TypeServerState, correlationID, payload)
}

// SendOperationComplete implements runtime.Responder.
func (c *Client) SendOperationComplete(ctx context.Context, correlationID string, payload protocol.ServerOperationResultPayload) error {
	return c.send(ctx, protocol.TypeServerOperationComplete, correlationID, payload)
}

// SendOperationFailed implements runtime.Responder.
func (c *Client) SendOperationFailed(ctx context.Context, correlationID string, payload protocol.ServerOperationResultPayload) error {
	return c.send(ctx, protocol.TypeServerOperationFailed, correlationID, payload)
}

func redactToken(rawURL string) string {
	u, err := url.Parse(rawURL)
	if err != nil {
		return rawURL
	}
	if u.Query().Get("token") != "" {
		q := u.Query()
		q.Set("token", "REDACTED")
		u.RawQuery = q.Encode()
	}
	return u.String()
}
