// Package bootstrap deals with the parsing and creation of bootstrap.json
// files. It also contains some helpers which rely on bootstrap data.
package bootstrap

import (
	"cmp"
	"encoding/json"
	"fmt"
	"isle/nebula"
	"isle/toolkit"
	"maps"
	"net/netip"
	"path/filepath"
	"slices"
	"strings"

	"dev.mediocregopher.com/mediocre-go-lib.git/mctx"
)

// StateDirPath returns the path within the user's state directory where the
// bootstrap file is stored.
func StateDirPath(dataDirPath string) string {
	return filepath.Join(dataDirPath, "bootstrap.json")
}

// CreationParams are general parameters used when creating a new network. These
// are available to all hosts within the network via their bootstrap files.
type CreationParams struct {
	ID     string
	Name   string
	Domain string
}

// NewCreationParams instantiates and returns a CreationParams.
func NewCreationParams(name, domain string) CreationParams {
	return CreationParams{
		ID:     toolkit.RandStr(32),
		Name:   name,
		Domain: domain,
	}
}

// Annotate implements the mctx.Annotator interface.
func (p CreationParams) Annotate(aa mctx.Annotations) {
	aa["networkID"] = p.ID
	aa["networkName"] = p.Name
	aa["networkDomain"] = p.Domain
}

// Matches returns true if the given string matches some aspect of the
// CreationParams.
func (p CreationParams) Matches(str string) bool {
	if strings.HasPrefix(p.ID, str) {
		return true
	}

	if strings.EqualFold(p.Name, str) {
		return true
	}

	if strings.EqualFold(p.Domain, str) {
		return true
	}

	return false
}

// Conflicts returns true if either CreationParams has some parameter which
// overlaps with that of the other.
func (p CreationParams) Conflicts(p2 CreationParams) bool {
	if p.ID == p2.ID {
		return true
	}

	if strings.EqualFold(p.Name, p2.Name) {
		return true
	}

	if strings.EqualFold(p.Domain, p2.Domain) {
		return true
	}

	return false
}

// Bootstrap contains all information which is needed by a host daemon to join a
// network on boot.
type Bootstrap struct {
	NetworkCreationParams CreationParams
	CAPublicCredentials   nebula.CAPublicCredentials

	PrivateCredentials nebula.HostPrivateCredentials
	HostAssigned       `json:"-"`
	SignedHostAssigned nebula.Signed[HostAssigned] // signed by CA

	Hosts map[nebula.HostName]Host
}

// New initializes and returns a new Bootstrap file for a new host.
func New(
	caCreds nebula.CACredentials,
	adminCreationParams CreationParams,
	existingHosts map[nebula.HostName]Host,
	name nebula.HostName,
	ip netip.Addr,
) (
	Bootstrap, error,
) {
	host, hostPrivCreds, err := NewHost(caCreds, name, ip)
	if err != nil {
		return Bootstrap{}, fmt.Errorf("creating host: %w", err)
	}

	signedAssigned, err := nebula.Sign(
		host.HostAssigned, caCreds.SigningPrivateKey,
	)
	if err != nil {
		return Bootstrap{}, fmt.Errorf("signing assigned fields: %w", err)
	}

	existingHosts = maps.Clone(existingHosts)
	existingHosts[name] = host

	return Bootstrap{
		NetworkCreationParams: adminCreationParams,
		CAPublicCredentials:   caCreds.Public,
		PrivateCredentials:    hostPrivCreds,
		HostAssigned:          host.HostAssigned,
		SignedHostAssigned:    signedAssigned,
		Hosts:                 existingHosts,
	}, nil
}

// UnmarshalJSON implements the json.Unmarshaler interface. It will
// automatically populate the HostAssigned field by unwrapping the
// SignedHostAssigned field.
func (b *Bootstrap) UnmarshalJSON(data []byte) error {
	type inner Bootstrap

	err := json.Unmarshal(data, (*inner)(b))
	if err != nil {
		return fmt.Errorf("json unmarshaling: %w", err)
	}

	// Generally this will be filled, but during unit tests we sometimes leave
	// it empty for convenience.
	if b.SignedHostAssigned != nil {
		b.HostAssigned, err = b.SignedHostAssigned.Unwrap(
			b.CAPublicCredentials.SigningKey,
		)
		if err != nil {
			return fmt.Errorf("unwrapping HostAssigned: %w", err)
		}
	}

	return nil
}

// ThisHost is a shortcut for b.Hosts[b.HostName], but will panic if the
// HostName isn't found in the Hosts map.
func (b Bootstrap) ThisHost() Host {
	host, ok := b.Hosts[b.Name]
	if !ok {
		panic(fmt.Sprintf("hostname %q not defined in bootstrap's hosts", b.Name))
	}

	return host
}

// HostsOrdered returns the Hosts as a slice in a deterministic order.
func (b Bootstrap) HostsOrdered() []Host {
	hosts := make([]Host, 0, len(b.Hosts))
	for _, host := range b.Hosts {
		hosts = append(hosts, host)
	}

	slices.SortFunc(hosts, func(a, b Host) int {
		return cmp.Compare(a.Name, b.Name)
	})

	return hosts
}