package bridge

import (
	"context"
	"encoding/json"
	"io"
	"time"

	"tailscale.com/ipn"
	"tailscale.com/ipn/ipnstate"
)

const bridgeWatchRetryInterval = 2 * time.Second

const (
	BridgeEventTypeSnapshot           = "snapshot"
	BridgeEventTypeState              = "state"
	BridgeEventTypePrefs              = "prefs"
	BridgeEventTypeNetmap             = "netmap"
	BridgeEventTypeVersion            = "version"
	BridgeEventTypeBrowseURL          = "browse-url"
	BridgeEventTypeError              = "error"
	BridgeEventTypeBackendUnavailable = "backend-unavailable"
)

type Event struct {
	Type       string            `json:"type"`
	Snapshot   *guiSnapshot      `json:"snapshot,omitempty"`
	State      string            `json:"state,omitempty"`
	StateLabel string            `json:"stateLabel,omitempty"`
	Prefs      *guiPrefsSummary  `json:"prefs,omitempty"`
	Netmap     *guiNetmapSummary `json:"netmap,omitempty"`
	Version    string            `json:"version,omitempty"`
	URL        string            `json:"url,omitempty"`
	Error      string            `json:"error,omitempty"`
}

type watchState struct {
	snapshot *snapshot
}

func newWatchState(snap *snapshot) *watchState {
	state := &watchState{snapshot: snap}
	if state.snapshot == nil {
		state.snapshot = &snapshot{}
	}
	if state.snapshot.status == nil {
		state.snapshot.status = &ipnstate.Status{}
	}
	if state.snapshot.prefs == nil {
		state.snapshot.prefs = ipn.NewPrefs()
	}
	return state
}

func (s *watchState) applyNotify(n ipn.Notify) []Event {
	events := make([]Event, 0, 6)
	if n.Version != "" {
		s.snapshot.status.Version = n.Version
		events = append(events, Event{Type: BridgeEventTypeVersion, Version: n.Version})
	}
	if n.ErrMessage != nil {
		events = append(events, Event{Type: BridgeEventTypeError, Error: *n.ErrMessage})
	}
	if n.BrowseToURL != nil {
		events = append(events, Event{Type: BridgeEventTypeBrowseURL, URL: *n.BrowseToURL})
	}
	if n.State != nil {
		s.snapshot.status.BackendState = n.State.String()
		events = append(events, Event{
			Type:       BridgeEventTypeState,
			State:      s.snapshot.status.BackendState,
			StateLabel: stateSummary(s.snapshot.status.BackendState),
		})
	}
	if n.Prefs != nil && n.Prefs.Valid() {
		s.snapshot.prefs = n.Prefs.AsStruct()
		prefsSummary := buildGUIPrefsSummary(s.snapshot)
		events = append(events, Event{Type: BridgeEventTypePrefs, Prefs: &prefsSummary})
	}
	if n.NetMap != nil {
		s.snapshot.netmap = n.NetMap
		events = append(events, Event{Type: BridgeEventTypeNetmap, Netmap: buildGUINetmapSummary(s.snapshot)})
	}
	return events
}

func writeEvent(w io.Writer, event Event) error {
	return json.NewEncoder(w).Encode(event)
}

func (a *App) RunWatch(out io.Writer) error {
	var lastBackendUnavailable string
	for {
		snap, err := a.refreshSnapshot()
		if err != nil {
			if err := writeBackendUnavailableEvent(out, err, &lastBackendUnavailable); err != nil {
				return err
			}
			time.Sleep(bridgeWatchRetryInterval)
			continue
		}
		lastBackendUnavailable = ""
		if err := writeEvent(out, Event{Type: BridgeEventTypeSnapshot, Snapshot: buildGUISnapshot(snap)}); err != nil {
			return err
		}
		if err := a.streamWatch(out, newWatchState(snap), &lastBackendUnavailable); err != nil {
			return err
		}
	}
}

func (a *App) streamWatch(out io.Writer, state *watchState, lastBackendUnavailable *string) error {
	watcher, err := a.local.WatchIPNBus(context.Background(), 0)
	if err != nil {
		if err := writeBackendUnavailableEvent(out, err, lastBackendUnavailable); err != nil {
			return err
		}
		time.Sleep(bridgeWatchRetryInterval)
		return nil
	}
	defer watcher.Close()

	for {
		notify, err := watcher.Next()
		if err != nil {
			if err := writeBackendUnavailableEvent(out, err, lastBackendUnavailable); err != nil {
				return err
			}
			time.Sleep(bridgeWatchRetryInterval)
			return nil
		}

		*lastBackendUnavailable = ""
		for _, event := range state.applyNotify(notify) {
			if err := writeEvent(out, event); err != nil {
				return err
			}
		}
	}
}

func writeBackendUnavailableEvent(out io.Writer, err error, lastMessage *string) error {
	if err == nil {
		return nil
	}
	message := err.Error()
	if lastMessage != nil && *lastMessage == message {
		return nil
	}
	if lastMessage != nil {
		*lastMessage = message
	}
	return writeEvent(out, Event{Type: BridgeEventTypeBackendUnavailable, Error: message})
}
