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

package ipn

import (
	"errors"
	"fmt"
	"net/netip"

	"tailscale.com/tailcfg"
	"tailscale.com/types/opt"
	"tailscale.com/types/preftype"
)

// ConfigVAlpha is the config file format for the "alpha0" version.
type ConfigVAlpha struct {
	Version string   // "alpha0" for now
	Locked  opt.Bool `json:",omitempty"` // whether the config is locked from being changed by 'tailscale set'; it defaults to true

	ServerURL *string  `json:",omitempty"` // defaults to https://controlplane.tailscale.com
	AuthKey   *string  `json:",omitempty"` // as needed if NeedsLogin. either key or path to a file (if prefixed with "file:")
	Enabled   opt.Bool `json:",omitempty"` // wantRunning; empty string defaults to true

	OperatorUser *string `json:",omitempty"` // local user name who is allowed to operate tailscaled without being root or using sudo
	Hostname     *string `json:",omitempty"`

	AcceptDNS    opt.Bool `json:"acceptDNS,omitempty"`    // --accept-dns
	AcceptRoutes opt.Bool `json:"acceptRoutes,omitempty"` // --accept-routes defaults to true

	ExitNode                   *string  `json:"exitNode,omitempty"` // IP, StableID, or MagicDNS base name
	AllowLANWhileUsingExitNode opt.Bool `json:"allowLANWhileUsingExitNode,omitempty"`

	AdvertiseRoutes []netip.Prefix `json:",omitempty"`
	DisableSNAT     opt.Bool       `json:",omitempty"`

	AdvertiseServices []string `json:",omitempty"`

	AppConnector *AppConnectorPrefs `json:",omitempty"` // advertise app connector; defaults to false (if nil or explicitly set to false)

	NetfilterMode       *string  `json:",omitempty"` // "on", "off", "nodivert"
	NoStatefulFiltering opt.Bool `json:",omitempty"`

	PostureChecking opt.Bool         `json:",omitempty"`
	RunSSHServer    opt.Bool         `json:",omitempty"` // Tailscale SSH
	RunWebClient    opt.Bool         `json:",omitempty"`
	ShieldsUp       opt.Bool         `json:",omitempty"`
	AutoUpdate      *AutoUpdatePrefs `json:",omitempty"`
	ServeConfigTemp *ServeConfig     `json:",omitempty"` // TODO(bradfitz,maisem): make separate stable type for this

	// StaticEndpoints are additional, user-defined endpoints that this node
	// should advertise amongst its wireguard endpoints.
	StaticEndpoints []netip.AddrPort `json:",omitempty"`

	// TODO(bradfitz,maisem): future something like:
	// Profile map[string]*Config // keyed by alice@gmail.com, corp.com (TailnetSID)
}

func (c *ConfigVAlpha) ToPrefs() (MaskedPrefs, error) {
	var mp MaskedPrefs
	if c == nil {
		return mp, nil
	}

	mp.WantRunning = !c.Enabled.EqualBool(false)
	mp.WantRunningSet = mp.WantRunning || c.Enabled != ""
	if c.ServerURL != nil {
		mp.ControlURL = *c.ServerURL
		mp.ControlURLSet = true
	}
	if c.AuthKey != nil && *c.AuthKey != "" {
		mp.LoggedOut = false
		mp.LoggedOutSet = true
	}
	if c.OperatorUser != nil {
		mp.OperatorUser = *c.OperatorUser
		mp.OperatorUserSet = true
	}
	if c.Hostname != nil {
		mp.Hostname = *c.Hostname
		mp.HostnameSet = true
	}
	if c.AcceptDNS != "" {
		mp.CorpDNS = c.AcceptDNS.EqualBool(true)
		mp.CorpDNSSet = true
	}
	if c.AcceptRoutes != "" {
		mp.RouteAll = c.AcceptRoutes.EqualBool(true)
		mp.RouteAllSet = true
	}
	if c.ExitNode != nil {
		ip, err := netip.ParseAddr(*c.ExitNode)
		if err == nil {
			mp.ExitNodeIP = ip
			mp.ExitNodeIPSet = true
		} else {
			mp.ExitNodeID = tailcfg.StableNodeID(*c.ExitNode)
			mp.ExitNodeIDSet = true
		}
	}
	if c.AllowLANWhileUsingExitNode != "" {
		mp.ExitNodeAllowLANAccess = c.AllowLANWhileUsingExitNode.EqualBool(true)
		mp.ExitNodeAllowLANAccessSet = true
	}
	if c.AdvertiseRoutes != nil {
		var routeErrs []error
		for _, route := range c.AdvertiseRoutes {
			if route != route.Masked() {
				routeErrs = append(routeErrs, fmt.Errorf("route %s has non-address bits set; expected %s", route, route.Masked()))
			}
		}
		if err := errors.Join(routeErrs...); err != nil {
			return mp, err
		}
		mp.AdvertiseRoutes = c.AdvertiseRoutes
		mp.AdvertiseRoutesSet = true
	}
	if c.DisableSNAT != "" {
		mp.NoSNAT = c.DisableSNAT.EqualBool(true)
		mp.NoSNATSet = true
	}
	if c.NoStatefulFiltering != "" {
		mp.NoStatefulFiltering = c.NoStatefulFiltering
		mp.NoStatefulFilteringSet = true
	}

	if c.NetfilterMode != nil {
		m, err := preftype.ParseNetfilterMode(*c.NetfilterMode)
		if err != nil {
			return mp, err
		}
		mp.NetfilterMode = m
		mp.NetfilterModeSet = true
	}
	if c.PostureChecking != "" {
		mp.PostureChecking = c.PostureChecking.EqualBool(true)
		mp.PostureCheckingSet = true
	}
	if c.RunSSHServer != "" {
		mp.RunSSH = c.RunSSHServer.EqualBool(true)
		mp.RunSSHSet = true
	}
	if c.RunWebClient != "" {
		mp.RunWebClient = c.RunWebClient.EqualBool(true)
		mp.RunWebClientSet = true
	}
	if c.ShieldsUp != "" {
		mp.ShieldsUp = c.ShieldsUp.EqualBool(true)
		mp.ShieldsUpSet = true
	}
	if c.AutoUpdate != nil {
		mp.AutoUpdate = *c.AutoUpdate
		mp.AutoUpdateSet = AutoUpdatePrefsMask{ApplySet: true, CheckSet: true}
	}
	if c.AppConnector != nil {
		mp.AppConnector = *c.AppConnector
		mp.AppConnectorSet = true
	}
	// Configfile should be the source of truth for whether this node
	// advertises any services.  We need to ensure that each reload updates
	// currently advertised services as else the transition from 'some
	// services are advertised' to 'advertised services are empty/unset in
	// conffile' would have no effect (especially given that an empty
	// service slice would be omitted from the JSON config).
	mp.AdvertiseServicesSet = true
	if c.AdvertiseServices != nil {
		mp.AdvertiseServices = c.AdvertiseServices
	}
	return mp, nil
}
