package live

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"net/url"
	"os"

	"github.com/gorilla/websocket"
)

// ConnectToF1LiveTiming negotiates with the official F1 SignalR hub, subscribes
// to timing topics, and sends defensive snapshots on dataChan until the
// connection closes.
func ConnectToF1LiveTiming(dataChan chan LiveStreamData) error {
	if err := connectToF1SignalRCore(dataChan); err == nil {
		return nil
	} else {
		log.Printf("f1 signalrcore feed unavailable, trying legacy signalr: %v", err)
	}
	return connectToF1LegacySignalR(dataChan)
}

func connectToF1SignalRCore(dataChan chan LiveStreamData) error {
	req, err := http.NewRequest("POST", "https://livetiming.formula1.com/signalrcore/negotiate?negotiateVersion=1", nil)
	if err != nil {
		return err
	}
	req.Header.Set("Origin", "https://www.formula1.com")
	req.Header.Set("User-Agent", "Mozilla/5.0")
	req.Header.Set("Content-Length", "0")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return err
	}
	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("f1 signalrcore negotiate returned %s", resp.Status)
	}

	var neg struct {
		ConnectionToken string `json:"connectionToken"`
	}
	if err := json.Unmarshal(body, &neg); err != nil {
		return err
	}
	if neg.ConnectionToken == "" {
		return fmt.Errorf("f1 signalrcore negotiate returned an empty connection token")
	}

	wsURL := "wss://livetiming.formula1.com/signalrcore?id=" + url.QueryEscape(neg.ConnectionToken)
	header := http.Header{}
	header.Set("Origin", "https://www.formula1.com")
	header.Set("User-Agent", "Mozilla/5.0")
	for _, cookie := range resp.Cookies() {
		header.Add("Cookie", cookie.String())
	}

	c, _, err := websocket.DefaultDialer.Dial(wsURL, header)
	if err != nil {
		return err
	}

	writeCoreFrame := func(payload any) error {
		body, err := json.Marshal(payload)
		if err != nil {
			return err
		}
		body = append(body, signalRRecordSeparator)
		return c.WriteMessage(websocket.TextMessage, body)
	}

	if err := writeCoreFrame(map[string]any{"protocol": "json", "version": 1}); err != nil {
		c.Close()
		return err
	}
	_, handshake, err := c.ReadMessage()
	if err != nil {
		c.Close()
		return err
	}
	if !bytes.Contains(handshake, []byte("{}")) {
		c.Close()
		return fmt.Errorf("f1 signalrcore handshake returned %q", string(handshake))
	}

	topics := []string{
		"Heartbeat",
		"TimingData",
		"DriverList",
		"LapCount",
		"ExtrapolatedClock",
		"TrackStatus",
		"RaceControlMessages",
		"WeatherData",
		"SessionInfo",
		"CurrentTyres",
		"TimingAppData",
		"TimingStats",
		"SessionStatus",
		"TopThree",
	}
	if err := writeCoreFrame(map[string]any{
		"type":         1,
		"target":       "subscribe",
		"arguments":    []any{topics},
		"invocationId": "1",
	}); err != nil {
		c.Close()
		return err
	}

	go func() {
		defer c.Close()
		state := NewState()

		for {
			_, message, err := c.ReadMessage()
			if err != nil {
				log.Println("SignalR Core read error:", err)
				return
			}

			if state.ProcessCoreMessage(message) {
				select {
				case dataChan <- state.Snapshot():
				default:
				}
			}
		}
	}()

	return nil
}

func connectToF1LegacySignalR(dataChan chan LiveStreamData) error {
	hubName := `[{"name":"Streaming"}]`
	negotiateURL := fmt.Sprintf("https://livetiming.formula1.com/signalr/negotiate?clientProtocol=1.5&connectionData=%s", url.QueryEscape(hubName))

	req, err := http.NewRequest("GET", negotiateURL, nil)
	if err != nil {
		return err
	}
	req.Header.Set("User-Agent", "BestHTTP")
	if token := f1LiveBearerToken(); token != "" {
		req.Header.Set("Authorization", "Bearer "+token)
	}
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("f1 signalr negotiate returned %s (auth=%s)", resp.Status, authState())
	}

	cookies := resp.Cookies()
	var neg struct {
		ConnectionToken string `json:"ConnectionToken"`
	}
	if err := json.NewDecoder(resp.Body).Decode(&neg); err != nil {
		return err
	}
	if neg.ConnectionToken == "" {
		return fmt.Errorf("f1 signalr negotiate returned an empty connection token")
	}

	wsURL := fmt.Sprintf("wss://livetiming.formula1.com/signalr/connect?clientProtocol=1.5&transport=webSockets&connectionToken=%s&connectionData=%s",
		url.QueryEscape(neg.ConnectionToken),
		url.QueryEscape(hubName),
	)

	header := http.Header{}
	for _, cookie := range cookies {
		header.Add("Cookie", cookie.String())
	}
	header.Add("User-Agent", "BestHTTP")
	if token := f1LiveBearerToken(); token != "" {
		header.Add("Authorization", "Bearer "+token)
	}

	c, _, err := websocket.DefaultDialer.Dial(wsURL, header)
	if err != nil {
		return err
	}

	subscribeMsg := []byte(`{"H":"Streaming","M":"Subscribe","A":[["Heartbeat","TimingData","DriverList","LapCount","ExtrapolatedClock","TrackStatus","RaceControlMessages","WeatherData","SessionInfo","CurrentTyres","TimingAppData","TimingStats"]],"I":1}`)
	err = c.WriteMessage(websocket.TextMessage, subscribeMsg)
	if err != nil {
		return err
	}

	go func() {
		defer c.Close()
		state := NewState()

		for {
			_, message, err := c.ReadMessage()
			if err != nil {
				log.Println("WS Read Error:", err)
				return
			}

			if state.ProcessMessage(message) {
				select {
				case dataChan <- state.Snapshot():
				default:
				}
			}
		}
	}()

	return nil
}

func f1LiveBearerToken() string {
	if token := os.Getenv("BOXBOX_F1_LIVE_BEARER_TOKEN"); token != "" {
		return token
	}
	return os.Getenv("F1_LIVE_BEARER_TOKEN")
}

func authState() string {
	if f1LiveBearerToken() != "" {
		return "bearer-configured"
	}
	return "no-bearer-token"
}
