// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause

package ipn

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"net"
	"strconv"

	"tailscale.com/health"
)

// ErrStateNotExist is returned by StateStore.ReadState when the
// requested state ID doesn't exist.
var ErrStateNotExist = errors.New("no state with given ID")

const (
	// MachineKeyStateKey is the key under which we store the machine key,
	// in its key.NodePrivate.MarshalText representation.
	MachineKeyStateKey = StateKey("_machinekey")

	// LegacyGlobalDaemonStateKey is the ipn.StateKey that tailscaled
	// loads on startup.
	//
	// We have to support multiple state keys for other OSes (Windows in
	// particular), but right now Unix daemons run with a single
	// node-global state. To keep open the option of having per-user state
	// later, the global state key doesn't look like a username.
	//
	// As of 2022-10-21, it has been superseded by profiles and is no longer
	// written to disk. It is only read at startup when there are no profiles,
	// to migrate the state to the "default" profile.
	// The existing state is left on disk in case the user downgrades to an
	// older version of Tailscale that doesn't support profiles. We can
	// remove this in a future release.
	LegacyGlobalDaemonStateKey = StateKey("_daemon")

	// ServerModeStartKey's value, if non-empty, is the value of a
	// StateKey containing the prefs to start with which to start the
	// server.
	//
	// For example, the value might be "user-1234", meaning the
	// the server should start with the Prefs JSON loaded from
	// StateKey "user-1234".
	ServerModeStartKey = StateKey("server-mode-start-key")

	// KnownProfilesStateKey is the key under which we store the list of
	// known profiles. The value is a JSON-encoded []LoginProfile.
	KnownProfilesStateKey = StateKey("_profiles")

	// CurrentProfileStateKey is the key under which we store the current
	// profile.
	CurrentProfileStateKey = StateKey("_current-profile")

	// TaildropReceivedKey is the key to indicate whether any taildrop file
	// has ever been received (even if partially).
	// Any non-empty value indicates that at least one file has been received.
	TaildropReceivedKey = StateKey("_taildrop-received")
)

// StateStoreHealth is a Warnable set when store.New fails at startup. If
// unhealthy, we block all login attempts and return a health message in status
// responses.
var StateStoreHealth = health.Register(&health.Warnable{
	Code:     "state-store-health",
	Severity: health.SeverityHigh,
	Title:    "Tailscale state store failed to initialize",
	Text: func(args health.Args) string {
		return fmt.Sprintf("State store failed to initialize, Tailscale will not work until this is resolved. See https://tailscale.com/s/state-store-init-error. Error: %s", args[health.ArgError])
	},
	ImpactsConnectivity: true,
})

// CurrentProfileID returns the StateKey that stores the
// current profile ID. The value is a JSON-encoded LoginProfile.
// If the userID is empty, the key returned is CurrentProfileStateKey,
// otherwise it is "_current/"+userID.
func CurrentProfileKey(userID string) StateKey {
	if userID == "" {
		return CurrentProfileStateKey
	}
	return StateKey("_current/" + userID)
}

// StateStore persists state, and produces it back on request.
// Implementations of StateStore are expected to be safe for concurrent use.
type StateStore interface {
	// ReadState returns the bytes associated with ID. Returns (nil,
	// ErrStateNotExist) if the ID doesn't have associated state.
	ReadState(id StateKey) ([]byte, error)
	// WriteState saves bs as the state associated with ID. If bs is nil,
	// the state associated with ID is deleted, and a subsequent ReadState
	// for ID will return ErrStateNotExist.
	//
	// Callers should generally use the ipn.WriteState wrapper func
	// instead, which only writes if the value is different from what's
	// already in the store.
	WriteState(id StateKey, bs []byte) error
}

// WriteState is a wrapper around store.WriteState that only writes if
// the value is different from what's already in the store.
func WriteState(store StateStore, id StateKey, v []byte) error {
	if was, err := store.ReadState(id); err == nil && bytes.Equal(was, v) {
		return nil
	}
	return store.WriteState(id, v)
}

// StateStoreDialerSetter is an optional interface that StateStores
// can implement to allow the caller to set a custom dialer.
type StateStoreDialerSetter interface {
	SetDialer(d func(ctx context.Context, network, address string) (net.Conn, error))
}

// ReadStoreInt reads an integer from a StateStore.
func ReadStoreInt(store StateStore, id StateKey) (int64, error) {
	v, err := store.ReadState(id)
	if err != nil {
		return 0, err
	}
	return strconv.ParseInt(string(v), 10, 64)
}

// PutStoreInt puts an integer into a StateStore.
func PutStoreInt(store StateStore, id StateKey, val int64) error {
	return WriteState(store, id, fmt.Appendf(nil, "%d", val))
}

// EncryptedStateStore is a marker interface implemented by StateStores that
// encrypt data at rest.
type EncryptedStateStore interface {
	stateStoreIsEncrypted()
}
