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

//go:build !ts_omit_tailnetlock

package jsonoutput

import (
	"encoding/base64"
	jsonv1 "encoding/json"
	"fmt"
	"io"

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

// PrintNetworkLockStatusJSONV1 prints the current Tailnet Lock status
// as a JSON object to the CLI, in a stable "v1" format.
func PrintNetworkLockStatusJSONV1(out io.Writer, status *ipnstate.NetworkLockStatus) error {
	responseEnvelope := ResponseEnvelope{
		SchemaVersion: "1",
	}

	var result any
	if status.Enabled {
		result = struct {
			ResponseEnvelope
			tailnetLockEnabledStatusV1
		}{
			ResponseEnvelope:           responseEnvelope,
			tailnetLockEnabledStatusV1: toTailnetLockEnabledStatusV1(status),
		}
	} else {
		result = struct {
			ResponseEnvelope
			tailnetLockDisabledStatusV1
		}{
			ResponseEnvelope:            responseEnvelope,
			tailnetLockDisabledStatusV1: toTailnetLockDisabledStatusV1(status),
		}
	}

	enc := jsonv1.NewEncoder(out)
	enc.SetIndent("", "  ")
	return enc.Encode(result)
}

func toTailnetLockDisabledStatusV1(status *ipnstate.NetworkLockStatus) tailnetLockDisabledStatusV1 {
	out := tailnetLockDisabledStatusV1{
		tailnetLockStatusV1Base: tailnetLockStatusV1Base{
			Enabled: status.Enabled,
		},
	}
	if !status.PublicKey.IsZero() {
		out.PublicKey = status.PublicKey.CLIString()
	}
	if nk := status.NodeKey; nk != nil {
		out.NodeKey = nk.String()
	}
	return out
}

func toTailnetLockEnabledStatusV1(status *ipnstate.NetworkLockStatus) tailnetLockEnabledStatusV1 {
	out := tailnetLockEnabledStatusV1{
		tailnetLockStatusV1Base: tailnetLockStatusV1Base{
			Enabled: status.Enabled,
		},
	}

	if status.Head != nil {
		var head tka.AUMHash
		h := status.Head
		copy(head[:], h[:])
		out.Head = head.String()
	}
	if !status.PublicKey.IsZero() {
		out.PublicKey = status.PublicKey.CLIString()
	}
	if nk := status.NodeKey; nk != nil {
		out.NodeKey = nk.String()
	}
	out.NodeKeySigned = status.NodeKeySigned
	if sig := status.NodeKeySignature; sig != nil {
		out.NodeKeySignature = toTKANodeKeySignatureV1(sig)
	}
	for _, key := range status.TrustedKeys {
		out.TrustedKeys = append(out.TrustedKeys, ipnTKAKeytoTKAKeyV1(&key))
	}
	for _, vp := range status.VisiblePeers {
		out.VisiblePeers = append(out.VisiblePeers, tkaTrustedPeerV1{
			tkaPeerV1:        toTKAPeerV1(vp),
			NodeKeySignature: toTKANodeKeySignatureV1(&vp.NodeKeySignature),
		})
	}
	for _, fp := range status.FilteredPeers {
		out.FilteredPeers = append(out.FilteredPeers, toTKAPeerV1(fp))
	}
	out.StateID = status.StateID

	return out
}

// toTKAKeyV1 converts an [ipnstate.TKAKey] to the JSON output returned
// by the CLI.
func ipnTKAKeytoTKAKeyV1(key *ipnstate.TKAKey) tkaKeyV1 {
	return tkaKeyV1{
		Kind:   key.Kind,
		Votes:  key.Votes,
		Public: key.Key.CLIString(),
		Meta:   key.Metadata,
	}
}

type tailnetLockStatusV1Base struct {
	// Enabled is true if Tailnet Lock is enabled.
	Enabled bool

	// PublicKey describes the node's tailnet-lock public key.
	PublicKey string `json:"PublicKey,omitzero"`

	// NodeKey describes the node's current node-key. This field is not
	// populated if the node is not operating (i.e. waiting for a login).
	NodeKey string `json:"NodeKey,omitzero"`
}

// tailnetLockDisabledStatusV1 is the JSON representation of the Tailnet Lock status
// when Tailnet Lock is disabled.
type tailnetLockDisabledStatusV1 struct {
	tailnetLockStatusV1Base
}

// tailnetLockEnabledStatusV1 is the JSON representation of the Tailnet Lock status.
type tailnetLockEnabledStatusV1 struct {
	tailnetLockStatusV1Base

	// Head describes the AUM hash of the leaf AUM.
	Head string `json:"Head,omitzero"`

	// NodeKeySigned is true if our node is authorized by Tailnet Lock.
	NodeKeySigned bool

	// NodeKeySignature is the current signature of this node's key.
	NodeKeySignature *tkaNodeKeySignatureV1

	// TrustedKeys describes the keys currently trusted to make changes
	// to tailnet-lock.
	TrustedKeys []tkaKeyV1

	// VisiblePeers describes peers which are visible in the netmap that
	// have valid Tailnet Lock signatures signatures.
	VisiblePeers []tkaTrustedPeerV1

	// FilteredPeers describes peers which were removed from the netmap
	// (i.e. no connectivity) because they failed Tailnet Lock
	// checks.
	FilteredPeers []tkaPeerV1

	// StateID is a nonce associated with the Tailnet Lock authority,
	// generated upon enablement. This field is empty if Tailnet Lock
	// is disabled.
	StateID uint64 `json:"State,omitzero"`
}

// tkaPeerV1 is the JSON representation of an [ipnstate.TKAPeer], which describes
// a peer and its Tailnet Lock details.
type tkaPeerV1 struct {
	// Stable ID, i.e. [tailcfg.StableNodeID]
	ID string

	// DNS name
	DNSName string

	// Tailscale IP(s) assigned to this node
	TailscaleIPs []string

	// The node's public key
	NodeKey string
}

// tkaPeerV1 is the JSON representation of a trusted [ipnstate.TKAPeer], which
// has a node key signature.
type tkaTrustedPeerV1 struct {
	tkaPeerV1

	// The node's key signature
	NodeKeySignature *tkaNodeKeySignatureV1 `json:"NodeKeySignature,omitzero"`
}

func toTKAPeerV1(peer *ipnstate.TKAPeer) tkaPeerV1 {
	out := tkaPeerV1{
		DNSName: peer.Name,
		ID:      string(peer.StableID),
	}
	for _, ip := range peer.TailscaleIPs {
		out.TailscaleIPs = append(out.TailscaleIPs, ip.String())
	}
	out.NodeKey = peer.NodeKey.String()

	return out
}

// tkaNodeKeySignatureV1 is the JSON representation of a [tka.NodeKeySignature],
// which describes a signature that authorizes a specific node key.
type tkaNodeKeySignatureV1 struct {
	// SigKind identifies the variety of signature.
	SigKind string

	// PublicKey identifies the key.NodePublic which is being authorized.
	// SigCredential signatures do not use this field.
	PublicKey string `json:"PublicKey,omitzero"`

	// KeyID identifies which key in the tailnet key authority should
	// be used to verify this signature. Only set for SigDirect and
	// SigCredential signature kinds.
	KeyID string `json:"KeyID,omitzero"`

	// Signature is the packed (R, S) ed25519 signature over all other
	// fields of the structure.
	Signature string

	// Nested describes a NodeKeySignature which authorizes the node-key
	// used as Pubkey. Only used for SigRotation signatures.
	Nested *tkaNodeKeySignatureV1 `json:"Nested,omitzero"`

	// WrappingPubkey specifies the ed25519 public key which must be used
	// to sign a Signature which embeds this one.
	WrappingPublicKey string `json:"WrappingPublicKey,omitzero"`
}

func toTKANodeKeySignatureV1(sig *tka.NodeKeySignature) *tkaNodeKeySignatureV1 {
	out := tkaNodeKeySignatureV1{
		SigKind: sig.SigKind.String(),
	}
	if len(sig.Pubkey) > 0 {
		out.PublicKey = fmt.Sprintf("tlpub:%x", sig.Pubkey)
	}
	if len(sig.KeyID) > 0 {
		out.KeyID = fmt.Sprintf("tlpub:%x", sig.KeyID)
	}
	out.Signature = base64.URLEncoding.EncodeToString(sig.Signature)
	if sig.Nested != nil {
		out.Nested = toTKANodeKeySignatureV1(sig.Nested)
	}
	if len(sig.WrappingPubkey) > 0 {
		out.WrappingPublicKey = fmt.Sprintf("tlpub:%x", sig.WrappingPubkey)
	}
	return &out
}
