package main

import (
	"context"
	"encoding/base64"
	"encoding/json"
	"flag"
	"fmt"
	"net/http"
	"os"
	"time"

	"github.com/gorilla/websocket"
)

const (
	userAgent    = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
	appVersion   = "4364"
	wsURL        = "wss://engine.kosmi.io/gql-ws"
	tokenURL     = "https://engine.kosmi.io/"
)

func main() {
	roomID := flag.String("room", "@hyperspaceout", "Room ID")
	testMode := flag.Int("mode", 1, "Test mode: 1=with-token, 2=no-auth, 3=origin-only")
	flag.Parse()

	fmt.Printf("Test Mode %d: Testing WebSocket connection to Kosmi\n\n", *testMode)

	var conn *websocket.Conn
	var err error

	switch *testMode {
	case 1:
		fmt.Println("Mode 1: Testing with JWT token (full auth)")
		conn, err = testWithToken(*roomID)
	case 2:
		fmt.Println("Mode 2: Testing without authentication")
		conn, err = testWithoutAuth()
	case 3:
		fmt.Println("Mode 3: Testing with Origin header only")
		conn, err = testWithOriginOnly()
	default:
		fmt.Fprintf(os.Stderr, "Invalid test mode: %d\n", *testMode)
		os.Exit(1)
	}

	if err != nil {
		fmt.Fprintf(os.Stderr, "❌ Connection failed: %v\n", err)
		os.Exit(1)
	}

	fmt.Println("✅ WebSocket connected successfully!")
	defer conn.Close()

	// Try to do the GraphQL-WS handshake
	fmt.Println("\n📤 Sending connection_init...")
	if err := waitForAck(conn); err != nil {
		fmt.Fprintf(os.Stderr, "❌ Handshake failed: %v\n", err)
		os.Exit(1)
	}

	fmt.Println("✅ WebSocket handshake successful!")
	fmt.Println("\n📝 Testing message subscription...")
	
	if err := subscribeToMessages(conn, *roomID); err != nil {
		fmt.Fprintf(os.Stderr, "❌ Subscription failed: %v\n", err)
		os.Exit(1)
	}

	fmt.Println("✅ Subscribed to messages!")
	fmt.Println("\n👂 Listening for messages (press Ctrl+C to exit)...")

	// Listen for messages
	for {
		var msg map[string]interface{}
		if err := conn.ReadJSON(&msg); err != nil {
			fmt.Fprintf(os.Stderr, "Error reading message: %v\n", err)
			break
		}

		msgType, _ := msg["type"].(string)
		fmt.Printf("📥 Received: %s\n", msgType)

		if msgType == "next" {
			payload, _ := msg["payload"].(map[string]interface{})
			data, _ := payload["data"].(map[string]interface{})
			if newMessage, ok := data["newMessage"].(map[string]interface{}); ok {
				body, _ := newMessage["body"].(string)
				user, _ := newMessage["user"].(map[string]interface{})
				username, _ := user["displayName"].(string)
				if username == "" {
					username, _ = user["username"].(string)
				}
				fmt.Printf("   💬 %s: %s\n", username, body)
			}
		}
	}
}

// testWithToken attempts connection with full JWT authentication
func testWithToken(roomID string) (*websocket.Conn, error) {
	fmt.Println("   1️⃣  Step 1: Acquiring JWT token...")

	// Try to get token from GraphQL endpoint
	token, err := acquireToken()
	if err != nil {
		return nil, fmt.Errorf("failed to acquire token: %w", err)
	}

	fmt.Printf("   ✅ Got token: %s...\n", truncate(token, 50))

	fmt.Println("   2️⃣  Step 2: Connecting WebSocket with token...")
	return connectWithToken(token)
}

// testWithoutAuth attempts direct connection with no headers
func testWithoutAuth() (*websocket.Conn, error) {
	fmt.Println("   Connecting without any authentication...")
	
	dialer := websocket.Dialer{
		Subprotocols: []string{"graphql-ws"},
	}
	
	conn, resp, err := dialer.Dial(wsURL, nil)
	if resp != nil {
		fmt.Printf("   Response status: %d\n", resp.StatusCode)
	}
	return conn, err
}

// testWithOriginOnly attempts connection with just Origin header
func testWithOriginOnly() (*websocket.Conn, error) {
	fmt.Println("   Connecting with Origin header only...")
	
	dialer := websocket.Dialer{
		Subprotocols: []string{"graphql-ws"},
	}
	
	headers := http.Header{
		"Origin": []string{"https://app.kosmi.io"},
	}
	
	conn, resp, err := dialer.Dial(wsURL, headers)
	if resp != nil {
		fmt.Printf("   Response status: %d\n", resp.StatusCode)
	}
	return conn, err
}

// acquireToken gets a JWT token from Kosmi's API
func acquireToken() (string, error) {
	// First, let's try a few different approaches
	
	// Approach 1: Try empty POST (some APIs generate anonymous tokens)
	fmt.Println("      Trying empty POST...")
	token, err := tryEmptyPost()
	if err == nil && token != "" {
		return token, nil
	}
	fmt.Printf("      Empty POST failed: %v\n", err)

	// Approach 2: Try GraphQL anonymous login
	fmt.Println("      Trying GraphQL anonymous session...")
	token, err = tryGraphQLSession()
	if err == nil && token != "" {
		return token, nil
	}
	fmt.Printf("      GraphQL session failed: %v\n", err)

	// Approach 3: Try REST endpoint
	fmt.Println("      Trying REST endpoint...")
	token, err = tryRESTAuth()
	if err == nil && token != "" {
		return token, nil
	}
	fmt.Printf("      REST auth failed: %v\n", err)

	return "", fmt.Errorf("all token acquisition methods failed")
}

// tryEmptyPost tries posting an empty body
func tryEmptyPost() (string, error) {
	client := &http.Client{Timeout: 10 * time.Second}
	
	req, err := http.NewRequest("POST", tokenURL, nil)
	if err != nil {
		return "", err
	}

	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Referer", "https://app.kosmi.io/")
	req.Header.Set("User-Agent", userAgent)

	resp, err := client.Do(req)
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()

	if resp.StatusCode != 200 {
		return "", fmt.Errorf("status %d", resp.StatusCode)
	}

	var result map[string]interface{}
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		return "", err
	}

	// Try to extract token from various possible locations
	if token, ok := result["token"].(string); ok {
		return token, nil
	}
	if data, ok := result["data"].(map[string]interface{}); ok {
		if token, ok := data["token"].(string); ok {
			return token, nil
		}
	}

	return "", fmt.Errorf("no token in response: %+v", result)
}

// tryGraphQLSession tries a GraphQL mutation for anonymous session
func tryGraphQLSession() (string, error) {
	query := map[string]interface{}{
		"query": `mutation { createAnonymousSession { token } }`,
	}

	return postGraphQL(query)
}

// tryRESTAuth tries REST-style auth endpoint
func tryRESTAuth() (string, error) {
	body := map[string]interface{}{
		"anonymous": true,
	}

	jsonBody, _ := json.Marshal(body)
	client := &http.Client{Timeout: 10 * time.Second}
	
	req, err := http.NewRequest("POST", tokenURL+"auth/anonymous", nil)
	if err != nil {
		return "", err
	}

	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Referer", "https://app.kosmi.io/")
	req.Header.Set("User-Agent", userAgent)
	req.Body = http.NoBody
	_ = jsonBody // silence unused warning

	resp, err := client.Do(req)
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()

	if resp.StatusCode >= 400 {
		return "", fmt.Errorf("status %d", resp.StatusCode)
	}

	var result map[string]interface{}
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		return "", err
	}

	if token, ok := result["token"].(string); ok {
		return token, nil
	}

	return "", fmt.Errorf("no token in response")
}

// postGraphQL posts a GraphQL query
func postGraphQL(query map[string]interface{}) (string, error) {
	jsonBody, _ := json.Marshal(query)
	client := &http.Client{Timeout: 10 * time.Second}
	
	req, err := http.NewRequest("POST", tokenURL, nil)
	if err != nil {
		return "", err
	}

	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Referer", "https://app.kosmi.io/")
	req.Header.Set("User-Agent", userAgent)
	req.Body = http.NoBody
	_ = jsonBody // silence unused

	resp, err := client.Do(req)
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()

	if resp.StatusCode != 200 {
		return "", fmt.Errorf("status %d", resp.StatusCode)
	}

	var result map[string]interface{}
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		return "", err
	}

	// Navigate nested response
	if data, ok := result["data"].(map[string]interface{}); ok {
		if session, ok := data["createAnonymousSession"].(map[string]interface{}); ok {
			if token, ok := session["token"].(string); ok {
				return token, nil
			}
		}
	}

	return "", fmt.Errorf("no token in response")
}

// connectWithToken connects WebSocket with JWT token
func connectWithToken(token string) (*websocket.Conn, error) {
	dialer := websocket.Dialer{
		Subprotocols: []string{"graphql-ws"},
	}
	
	headers := http.Header{
		"Origin":     []string{"https://app.kosmi.io"},
		"User-Agent": []string{userAgent},
	}
	
	conn, resp, err := dialer.Dial(wsURL, headers)
	if err != nil {
		if resp != nil {
			fmt.Printf("   Response status: %d\n", resp.StatusCode)
		}
		return nil, err
	}

	// Send connection_init with token
	uaEncoded := base64.StdEncoding.EncodeToString([]byte(userAgent))
	
	initMsg := map[string]interface{}{
		"type": "connection_init",
		"payload": map[string]interface{}{
			"token": token,
			"ua":    uaEncoded,
			"v":     appVersion,
			"r":     "",
		},
	}

	if err := conn.WriteJSON(initMsg); err != nil {
		conn.Close()
		return nil, fmt.Errorf("failed to send connection_init: %w", err)
	}

	return conn, nil
}

// waitForAck waits for connection_ack
func waitForAck(conn *websocket.Conn) error {
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	done := make(chan error, 1)
	
	go func() {
		var msg map[string]interface{}
		if err := conn.ReadJSON(&msg); err != nil {
			done <- err
			return
		}

		msgType, _ := msg["type"].(string)
		if msgType != "connection_ack" {
			done <- fmt.Errorf("expected connection_ack, got %s", msgType)
			return
		}

		fmt.Println("✅ Received connection_ack")
		done <- nil
	}()

	select {
	case err := <-done:
		return err
	case <-ctx.Done():
		return fmt.Errorf("timeout waiting for connection_ack")
	}
}

// subscribeToMessages subscribes to room messages
func subscribeToMessages(conn *websocket.Conn, roomID string) error {
	query := fmt.Sprintf(`
		subscription {
			newMessage(roomId: "%s") {
				body
				time
				user {
					displayName
					username
				}
			}
		}
	`, roomID)

	subMsg := map[string]interface{}{
		"id":   "test-subscription-1",
		"type": "subscribe",
		"payload": map[string]interface{}{
			"query":     query,
			"variables": map[string]interface{}{},
		},
	}

	return conn.WriteJSON(subMsg)
}

// truncate truncates a string
func truncate(s string, maxLen int) string {
	if len(s) <= maxLen {
		return s
	}
	return s[:maxLen] + "..."
}

