package networkmap

import (
	"context"
	"encoding/base64"
	"fmt"

	"github.com/netbirdio/netbird/shared/management/proto"
	"github.com/netbirdio/netbird/shared/management/types"
)

// EnvelopeResult is what the client engine consumes after receiving a
// component-format NetworkMap. Both fields are populated:
//
//   - NetworkMap is the *proto.NetworkMap shape the engine reads today via
//     update.GetNetworkMap() — built from the envelope's components by
//     running Calculate() locally + converting back through the shared
//     proto helpers + merging the optional ProxyPatch.
//   - Components is the *types.NetworkMapComponents the engine retains so
//     future incremental delta updates have a base to apply changes
//     against. The client keeps it under its sync lock.
type EnvelopeResult struct {
	NetworkMap *proto.NetworkMap
	Components *types.NetworkMapComponents
}

// EnvelopeToNetworkMap is the full client-side pipeline: decode the
// component envelope back to a typed NetworkMapComponents, run Calculate()
// locally to produce the typed NetworkMap, convert it to the wire form the
// engine consumes, and fold in any ProxyPatch the server attached.
//
// localPeerKey is the receiving peer's WG pub key (used to derive
// includeIPv6 / useSourcePrefixes from the receiving peer's own record in
// the components struct, mirroring legacy ToSyncResponse behaviour).
//
// dnsName is the account's DNS domain ("netbird.cloud" etc.); used when
// rebuilding the per-peer FQDNs that proto.RemotePeerConfig carries.
func EnvelopeToNetworkMap(ctx context.Context, env *proto.NetworkMapEnvelope, localPeerKey, dnsName string) (*EnvelopeResult, error) {
	components, err := DecodeEnvelope(env)
	if err != nil {
		return nil, fmt.Errorf("decode envelope: %w", err)
	}

	// Find the receiving peer in the decoded components by WG key.
	// c.Peers is keyed by canonical base64 of the raw 32-byte pub key
	// (decoder re-encodes the bytes off the wire). The caller may pass a
	// non-canonical encoding (some persisted production keys carry
	// non-zero trailing padding bits that survived a legacy import), so
	// round-trip through raw bytes once to canonicalize before lookup.
	canonicalKey := canonicalizeWgKey(localPeerKey)
	localPeer := components.Peers[canonicalKey]
	if localPeer == nil {
		return nil, fmt.Errorf("receiving peer (wg_key prefix %q) not found among %d decoded peers — components have no PeerID, Calculate would return empty", trimKey(localPeerKey), len(components.Peers))
	}
	components.PeerID = canonicalKey

	includeIPv6 := localPeer.SupportsIPv6 && localPeer.IPv6.IsValid()
	useSourcePrefixes := localPeer.SupportsSourcePrefixes

	typedNM := components.Calculate(ctx)

	full := env.GetFull()
	dnsFwdPort := int64(0)
	if full != nil {
		dnsFwdPort = full.DnsForwarderPort
	}

	protoNM := &proto.NetworkMap{
		Serial: typedNM.Network.CurrentSerial(),
	}
	if full != nil {
		protoNM.PeerConfig = full.PeerConfig
	}
	protoNM.Routes = ToProtocolRoutes(typedNM.Routes)
	protoNM.DNSConfig = ToProtocolDNSConfig(typedNM.DNSConfig, nil, dnsFwdPort)

	remotePeers := AppendRemotePeerConfig(nil, typedNM.Peers, dnsName, includeIPv6)
	protoNM.RemotePeers = remotePeers
	protoNM.RemotePeersIsEmpty = len(remotePeers) == 0

	protoNM.OfflinePeers = AppendRemotePeerConfig(nil, typedNM.OfflinePeers, dnsName, includeIPv6)

	firewallRules := ToProtocolFirewallRules(typedNM.FirewallRules, includeIPv6, useSourcePrefixes)
	protoNM.FirewallRules = firewallRules
	protoNM.FirewallRulesIsEmpty = len(firewallRules) == 0

	routesFirewallRules := ToProtocolRoutesFirewallRules(typedNM.RoutesFirewallRules)
	protoNM.RoutesFirewallRules = routesFirewallRules
	protoNM.RoutesFirewallRulesIsEmpty = len(routesFirewallRules) == 0

	if typedNM.AuthorizedUsers != nil {
		hashedUsers, machineUsers := BuildAuthorizedUsersProto(ctx, typedNM.AuthorizedUsers)
		userIDClaim := ""
		if full != nil {
			userIDClaim = full.UserIdClaim
		}
		protoNM.SshAuth = &proto.SSHAuth{
			AuthorizedUsers: hashedUsers,
			MachineUsers:    machineUsers,
			UserIDClaim:     userIDClaim,
		}
	}

	if typedNM.ForwardingRules != nil {
		forwardingRules := make([]*proto.ForwardingRule, 0, len(typedNM.ForwardingRules))
		for _, rule := range typedNM.ForwardingRules {
			forwardingRules = append(forwardingRules, rule.ToProto())
		}
		protoNM.ForwardingRules = forwardingRules
	}

	// Merge the proxy patch the server attached. Mirrors the legacy
	// NetworkMap.Merge step that the server runs after Calculate().
	if full != nil && full.ProxyPatch != nil {
		mergeProxyPatch(protoNM, full.ProxyPatch)
	}

	return &EnvelopeResult{
		NetworkMap: protoNM,
		Components: components,
	}, nil
}

// mergeProxyPatch folds a ProxyPatch's pre-expanded fragments into the
// proto.NetworkMap that Calculate() produced. Mirrors types.NetworkMap.Merge
// — same six collections, deduplicated where the legacy merge dedupes.
func mergeProxyPatch(nm *proto.NetworkMap, patch *proto.ProxyPatch) {
	nm.RemotePeers = appendUniquePeers(nm.RemotePeers, patch.Peers)
	nm.OfflinePeers = appendUniquePeers(nm.OfflinePeers, patch.OfflinePeers)
	nm.FirewallRules = append(nm.FirewallRules, patch.FirewallRules...)
	nm.Routes = append(nm.Routes, patch.Routes...)
	nm.RoutesFirewallRules = append(nm.RoutesFirewallRules, patch.RouteFirewallRules...)
	nm.ForwardingRules = append(nm.ForwardingRules, patch.ForwardingRules...)
	if len(nm.RemotePeers) > 0 {
		nm.RemotePeersIsEmpty = false
	}
	if len(nm.FirewallRules) > 0 {
		nm.FirewallRulesIsEmpty = false
	}
	if len(nm.RoutesFirewallRules) > 0 {
		nm.RoutesFirewallRulesIsEmpty = false
	}
}

// appendUniquePeers dedupes by WgPubKey — mirrors legacy
// mergeUniquePeersByID's intent (legacy keyed off Peer.ID; in proto form the
// closest stable identifier is WgPubKey).
func appendUniquePeers(dst, extra []*proto.RemotePeerConfig) []*proto.RemotePeerConfig {
	if len(extra) == 0 {
		return dst
	}
	seen := make(map[string]struct{}, len(dst))
	for _, p := range dst {
		if p == nil {
			continue
		}
		seen[p.WgPubKey] = struct{}{}
	}
	for _, p := range extra {
		if p == nil {
			continue
		}
		if _, ok := seen[p.WgPubKey]; ok {
			continue
		}
		seen[p.WgPubKey] = struct{}{}
		dst = append(dst, p)
	}
	return dst
}

func trimKey(s string) string {
	if len(s) > 12 {
		return s[:12]
	}
	return s
}

// canonicalizeWgKey normalises a base64-encoded WireGuard public key so it
// matches the canonical encoding emitted by the envelope decoder. Returns
// the input unchanged when it does not decode to 32 raw bytes (caller will
// hit a miss in the peer map and surface the error).
func canonicalizeWgKey(s string) string {
	raw, err := base64.StdEncoding.DecodeString(s)
	if err != nil || len(raw) != 32 {
		return s
	}
	return base64.StdEncoding.EncodeToString(raw)
}
