package ws

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"sync"
	"time"

	"github.com/gorilla/websocket"
	"github.com/titanappdev/rofs/apps/world/internal/auth"
	"github.com/titanappdev/rofs/apps/world/internal/logging"
	"github.com/titanappdev/rofs/apps/world/internal/player"
	"github.com/titanappdev/rofs/apps/world/internal/zone"
)

const (
	writeWait  = 10 * time.Second
	pongWait   = 60 * time.Second
	pingPeriod = (pongWait * 9) / 10
	maxMsgSize = 4096
)

var upgrader = websocket.Upgrader{
	ReadBufferSize:  1024,
	WriteBufferSize: 1024,
	CheckOrigin:     func(r *http.Request) bool { return true },
}

type Gateway struct {
	log     *logging.Logger
	zone    *zone.Zone
	tickets *auth.Validator
}

func NewGateway(log *logging.Logger, z *zone.Zone, tickets *auth.Validator) *Gateway {
	return &Gateway{log: log, zone: z, tickets: tickets}
}

func (g *Gateway) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	correlationID := r.Header.Get("X-Correlation-ID")
	if correlationID == "" {
		correlationID = fmt.Sprintf("ws-%d", time.Now().UnixNano())
	}
	connLog := g.log.With(map[string]any{"correlation_id": correlationID})

	ticket := r.URL.Query().Get("ticket")
	payload, err := g.tickets.ValidateAndConsume(r.Context(), ticket)
	if err != nil {
		connLog.Warn("ticket validation failed", err, nil)
		http.Error(w, "unauthorized", http.StatusUnauthorized)
		return
	}

	conn, err := upgrader.Upgrade(w, r, nil)
	if err != nil {
		connLog.Error("websocket upgrade failed", err, nil)
		return
	}
	defer conn.Close()

	playerID := player.New(payload.CharacterID, payload.AccountID, payload.TileX, payload.TileY).ID
	tileX, tileY := payload.TileX, payload.TileY
	if tileX <= 0 {
		tileX, tileY = 25, 25
	}

	conn.SetReadLimit(maxMsgSize)
	_ = conn.SetReadDeadline(time.Now().Add(pongWait))
	conn.SetPongHandler(func(string) error {
		return conn.SetReadDeadline(time.Now().Add(pongWait))
	})

	var wg sync.WaitGroup
	ctx, cancel := context.WithCancel(r.Context())
	defer cancel()
	defer g.zone.RemovePlayer(playerID)

	var writeMu sync.Mutex
	broadcast := func(msgType string, pl map[string]any) {
		writeMu.Lock()
		defer writeMu.Unlock()
		_ = g.writeJSON(conn, outboundMessage{Type: msgType, Success: true, Payload: pl})
	}

	enterResult := g.submitAndWait(ctx, zone.Command{
		ID: "enter", Type: "enter", PlayerID: playerID,
		Payload: map[string]any{
			"character_id": float64(payload.CharacterID),
			"account_id":   float64(payload.AccountID),
			"tile_x":       float64(tileX),
			"tile_y":       float64(tileY),
		},
	})
	if enterResult.Success {
		g.zone.SetPlayerConn(playerID, broadcast)
		_ = g.writeJSON(conn, outboundMessage{
			Type: "viewport", Success: true, Payload: enterResult.Payload,
		})
	}

	wg.Add(1)
	go func() {
		defer wg.Done()
		defer cancel()
		g.readPump(ctx, conn, connLog, playerID)
	}()

	wg.Add(1)
	go func() {
		defer wg.Done()
		g.writePump(ctx, conn)
	}()

	wg.Wait()
	connLog.Info("websocket session closed", map[string]any{"player_id": playerID})
}

type inboundMessage struct {
	Type    string         `json:"type"`
	ID      string         `json:"id"`
	Payload map[string]any `json:"payload"`
}

type outboundMessage struct {
	Type    string         `json:"type"`
	ID      string         `json:"id,omitempty"`
	Seq     uint64         `json:"seq,omitempty"`
	Success bool           `json:"success"`
	Error   string         `json:"error,omitempty"`
	Payload map[string]any `json:"payload,omitempty"`
}

func (g *Gateway) readPump(ctx context.Context, conn *websocket.Conn, log *logging.Logger, playerID string) {
	defer func() {
		if r := recover(); r != nil {
			log.Error("readPump panic recovered", fmt.Errorf("%v", r), nil)
		}
	}()

	for {
		if ctx.Err() != nil {
			return
		}

		_, raw, err := conn.ReadMessage()
		if err != nil {
			return
		}

		var msg inboundMessage
		if err := json.Unmarshal(raw, &msg); err != nil {
			continue
		}

		switch msg.Type {
		case "move", "attack", "chat":
			result := g.submitAndWait(ctx, zone.Command{
				ID: msg.ID, Type: msg.Type, PlayerID: playerID, Payload: msg.Payload,
			})
			out := outboundMessage{
				Type: msg.Type + "_result", ID: msg.ID, Seq: result.Seq,
				Success: result.Success, Error: result.Error, Payload: result.Payload,
			}
			_ = g.writeJSON(conn, out)
		}
	}
}

func (g *Gateway) submitAndWait(ctx context.Context, cmd zone.Command) zone.Result {
	replyCh := make(chan zone.Result, 1)
	cmd.ReplyCh = replyCh
	if err := g.zone.Submit(cmd); err != nil {
		return zone.Result{Success: false, Error: err.Error()}
	}
	select {
	case res := <-replyCh:
		return res
	case <-ctx.Done():
		return zone.Result{Success: false, Error: "context cancelled"}
	case <-time.After(5 * time.Second):
		return zone.Result{Success: false, Error: "command timeout"}
	}
}

func (g *Gateway) writePump(ctx context.Context, conn *websocket.Conn) {
	ticker := time.NewTicker(pingPeriod)
	defer ticker.Stop()
	for {
		select {
		case <-ctx.Done():
			return
		case <-ticker.C:
			_ = conn.SetWriteDeadline(time.Now().Add(writeWait))
			if err := conn.WriteMessage(websocket.PingMessage, nil); err != nil {
				return
			}
		}
	}
}

func (g *Gateway) writeJSON(conn *websocket.Conn, msg outboundMessage) error {
	_ = conn.SetWriteDeadline(time.Now().Add(writeWait))
	return conn.WriteJSON(msg)
}
