package ws

import (
	"context"
	"errors"
	"sync"
	"time"

	"github.com/gofiber/contrib/websocket"
	"github.com/gofiber/fiber/v2"
)

// localsUserID Fiber Locals içinde kullanıcı kimliğinin saklandığı anahtardır.
const localsUserID = "fiko.userID"

// DefaultUserID kimlik doğrulama yokken (Phase 1) kullanılan tek kullanıcıdır.
const DefaultUserID = "local"

// FiberConfig WebSocket uç noktasının Fiber tarafındaki ayarlarıdır.
type FiberConfig struct {
	// MaxMessageBytes tek bir istemci mesajının üst sınırı; aşılırsa bağlantı kapanır (1009). Varsayılan 1 MiB.
	MaxMessageBytes int64
	// AllowedOrigins boşsa tüm origin'lere izin verilir (yalnızca geliştirme için).
	AllowedOrigins []string
	// UserResolver isteği yapan kullanıcıyı belirler (upgrade'den önce çalışır). Hata dönerse 401 verilir.
	// nil ise DefaultUserID kullanılır.
	UserResolver func(c *fiber.Ctx) (string, error)
	// AuthChecker upgrade öncesi yetkilendirir. nil ise doğrulama yapılmaz
	// (yerel geliştirme). Browser WebSocket API'si özel başlık GÖNDEREMEZ, bu
	// yüzden anahtar query parametresi (ws?key=...) ile de taşınabilir.
	AuthChecker func(c *fiber.Ctx) bool
}

// Mount WebSocket uç noktasını r üzerinde path altına bağlar. ctx sunucu ömrüdür (iptali tüm bağlantıları kapatır).
//
// Fiber v2 + contrib/websocket'in belgelenen kalıbı: kontrol middleware'i ve route AYNI
// `Use(path, ...)` çağrısında, route handler'dan ÖNCE kaydedilir. Ayrı `Use` + `Get`
// çağrıları kullanmak `IsWebSocketUpgrade` kontrolünü bozar (her istek 426 döner) çünkü
// Fiber `Use` eşleştirmesinde middleware'ler `Use`'tan SONRA kaydedilen route'lardan önce
// çalışır ama bağlantıyı fasthttp'ye devrederken `Upgrade` başlıkları kaybolur.
//
// Normal (upgrade'siz) GET /ws isteği 426 alır — bu davranış kasıtlıdır.
func Mount(r fiber.Router, path string, ctx context.Context, hub *Hub, cfg FiberConfig) {
	if cfg.MaxMessageBytes <= 0 {
		cfg.MaxMessageBytes = 1 << 20
	}

	r.Use(path, func(c *fiber.Ctx) error {
		if websocket.IsWebSocketUpgrade(c) {
			// Yetkilendirme ÖNCE: kimlik çözülmeden önce yapılır.
			if cfg.AuthChecker != nil && !cfg.AuthChecker(c) {
				return fiber.ErrUnauthorized
			}
			userID := DefaultUserID
			if cfg.UserResolver != nil {
				id, err := cfg.UserResolver(c)
				if err != nil || id == "" {
					return fiber.ErrUnauthorized
				}
				userID = id
			}
			c.Locals(localsUserID, userID)
			// Normal HTTP isteği: bu uç nokta yalnızca WebSocket'tir.
			return websocket.New(func(wsc *websocket.Conn) {
				id, _ := wsc.Locals(localsUserID).(string)
				if id == "" {
					id = DefaultUserID
				}
				hub.Serve(ctx, newFiberConn(wsc, cfg.MaxMessageBytes), id)
			})(c)
		}
		return fiber.ErrUpgradeRequired
	})
}

// fiberConn *websocket.Conn'u Conn arayüzüne uyarlar.
// Eşzamanlılık: Write/Ping yalnızca yazma döngüsünden çağrılır; Close ve WriteControl (gorilla sözleşmesi gereği)
// diğer çağrılarla eşzamanlı güvenlidir.
type fiberConn struct {
	c         *websocket.Conn
	closeOnce sync.Once
}

var _ Conn = (*fiberConn)(nil)

func newFiberConn(c *websocket.Conn, maxBytes int64) *fiberConn {
	c.SetReadLimit(maxBytes)
	return &fiberConn{c: c}
}

// Read bir sonraki metin mesajını döndürür. İkili (binary) mesajlar şimdilik yoksayılır:
// Phase 1 protokolü yalnızca JSON metin çerçeveleri kullanır (ses için ileride ayrılacak).
func (f *fiberConn) Read() ([]byte, error) {
	for {
		mt, data, err := f.c.ReadMessage()
		if err != nil {
			return nil, err
		}
		if mt == websocket.TextMessage {
			return data, nil
		}
	}
}

func (f *fiberConn) Write(data []byte, deadline time.Time) error {
	if err := f.c.SetWriteDeadline(deadline); err != nil {
		return err
	}
	return f.c.WriteMessage(websocket.TextMessage, data)
}

func (f *fiberConn) Ping(deadline time.Time) error {
	return f.c.WriteControl(websocket.PingMessage, nil, deadline)
}

func (f *fiberConn) SetReadDeadline(t time.Time) error { return f.c.SetReadDeadline(t) }

func (f *fiberConn) SetPongHandler(h func()) {
	f.c.SetPongHandler(func(string) error {
		h()
		return nil
	})
}

// Close önce nazik bir kapanış çerçevesi (1000) göndermeyi dener, sonra bağlantıyı koparır. Birden çok kez çağrılabilir.
func (f *fiberConn) Close() error {
	var err error
	f.closeOnce.Do(func() {
		_ = f.c.WriteControl(websocket.CloseMessage,
			websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""), time.Now().Add(time.Second))
		err = f.c.Close()
	})
	if errors.Is(err, websocket.ErrCloseSent) {
		return nil
	}
	return err
}
