package gateway

import (
	"context"
	"encoding/json"
	"log/slog"
	"time"

	"github.com/google/uuid"
	"github.com/gorilla/websocket"

	"github.com/nextlevelbuilder/goclaw/internal/permissions"
	"github.com/nextlevelbuilder/goclaw/pkg/protocol"
)

// Client represents a single WebSocket connection.
type Client struct {
	id            string
	conn          *websocket.Conn
	server        *Server
	authenticated bool
	role          permissions.Role
	userID        string // external user ID (TEXT, free-form), set during connect
	send          chan []byte

	connectedAt time.Time // when the client connected
	remoteAddr  string    // peer IP (extracted from proxy headers or RemoteAddr)

	locale string // user's preferred locale (e.g. "en", "vi", "zh")
	scopes []permissions.Scope // API key scopes (empty = role-based auth, no scope restriction)

	// Browser pairing state
	pairingCode     string // 8-char code if pending approval
	pairingPending  bool   // true while waiting for admin approval
	pairedSenderID  string // senderID used for browser pairing auth (for revocation lookup)
	pairedChannel   string // channel used for pairing auth (e.g., "browser")
}

func NewClient(conn *websocket.Conn, server *Server, remoteIP string) *Client {
	return &Client{
		id:          uuid.NewString(),
		conn:        conn,
		server:      server,
		send:        make(chan []byte, 256),
		connectedAt: time.Now(),
		remoteAddr:  remoteIP,
	}
}

// Run starts the read and write pumps for this client.
func (c *Client) Run(ctx context.Context) {
	go c.writePump()
	c.readPump(ctx)
}

// maxWSMessageSize is the maximum allowed WebSocket message size (512KB).
// Gorilla/websocket closes the connection with ErrReadLimit if exceeded.
const maxWSMessageSize = 512 * 1024

// readPump reads frames from the WebSocket connection.
func (c *Client) readPump(ctx context.Context) {
	defer c.conn.Close()

	c.conn.SetReadLimit(maxWSMessageSize)
	c.conn.SetReadDeadline(time.Now().Add(60 * time.Second))
	c.conn.SetPongHandler(func(string) error {
		c.conn.SetReadDeadline(time.Now().Add(60 * time.Second))
		return nil
	})

	for {
		_, data, err := c.conn.ReadMessage()
		if err != nil {
			if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseNormalClosure) {
				slog.Warn("websocket read error", "client", c.id, "error", err)
			}
			return
		}

		// Reset read deadline on activity
		c.conn.SetReadDeadline(time.Now().Add(60 * time.Second))

		c.handleFrame(ctx, data)
	}
}

// writePump writes frames and pings to the WebSocket connection.
func (c *Client) writePump() {
	ticker := time.NewTicker(30 * time.Second)
	defer func() {
		ticker.Stop()
		c.conn.Close()
	}()

	for {
		select {
		case msg, ok := <-c.send:
			if !ok {
				c.conn.WriteMessage(websocket.CloseMessage, []byte{})
				return
			}
			c.conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
			if err := c.conn.WriteMessage(websocket.TextMessage, msg); err != nil {
				return
			}

		case <-ticker.C:
			c.conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
			if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil {
				return
			}
		}
	}
}

// handleFrame parses and dispatches a single frame.
func (c *Client) handleFrame(ctx context.Context, data []byte) {
	frameType, err := protocol.ParseFrameType(data)
	if err != nil {
		c.sendError("", protocol.ErrInvalidRequest, "invalid frame: "+err.Error())
		return
	}

	switch frameType {
	case protocol.FrameTypeRequest:
		var req protocol.RequestFrame
		if err := json.Unmarshal(data, &req); err != nil {
			c.sendError("", protocol.ErrInvalidRequest, "malformed request: "+err.Error())
			return
		}

		// First request must be "connect" (except browser.pairing.status for pending clients)
		if !c.authenticated && req.Method != protocol.MethodConnect {
			if !(c.pairingPending && req.Method == protocol.MethodBrowserPairingStatus) {
				c.sendError(req.ID, protocol.ErrUnauthorized, "first request must be 'connect'")
				return
			}
		}

		// Dispatch to method router
		c.server.router.Handle(ctx, c, &req)

	default:
		c.sendError("", protocol.ErrInvalidRequest, "unexpected frame type: "+frameType)
	}
}

// SendResponse sends a response frame to this client.
func (c *Client) SendResponse(resp *protocol.ResponseFrame) {
	data, err := json.Marshal(resp)
	if err != nil {
		slog.Error("marshal response failed", "error", err)
		return
	}
	select {
	case c.send <- data:
	default:
		slog.Warn("client send buffer full, dropping message", "client", c.id)
	}
}

// SendEvent sends an event frame to this client.
func (c *Client) SendEvent(event protocol.EventFrame) {
	data, err := json.Marshal(event)
	if err != nil {
		slog.Error("marshal event failed", "error", err)
		return
	}
	select {
	case c.send <- data:
	default:
		slog.Warn("client send buffer full, dropping event", "client", c.id)
	}
}

func (c *Client) sendError(id, code, message string) {
	c.SendResponse(protocol.NewErrorResponse(id, code, message))
}

// ID returns the client's unique identifier.
func (c *Client) ID() string { return c.id }

// Role returns the client's permission role.
func (c *Client) Role() permissions.Role { return c.role }

// UserID returns the external user ID set during connect.
func (c *Client) UserID() string { return c.userID }

// ConnectedAt returns when the client connected.
func (c *Client) ConnectedAt() time.Time { return c.connectedAt }

// RemoteAddr returns the peer IP:port.
func (c *Client) RemoteAddr() string { return c.remoteAddr }

// Close shuts down the client connection.
func (c *Client) Close() {
	close(c.send)
}
