package libnetwork

import (
	"encoding/json"
	"fmt"
	"net"
	"net/netip"
	"slices"

	"github.com/moby/moby/v2/daemon/internal/netiputil"
	"github.com/moby/moby/v2/daemon/libnetwork/driverapi"
	"github.com/moby/moby/v2/daemon/libnetwork/types"
)

// EndpointInfo provides an interface to retrieve network resources bound to the endpoint.
type EndpointInfo interface {
	// Iface returns information about the interface which was assigned to
	// the endpoint by the driver. This can be used after the
	// endpoint has been created.
	Iface() *EndpointInterface

	// Gateway returns the IPv4 gateway assigned by the driver.
	// This will only return a valid value if a container has joined the endpoint.
	Gateway() net.IP

	// GatewayIPv6 returns the IPv6 gateway assigned by the driver.
	// This will only return a valid value if a container has joined the endpoint.
	GatewayIPv6() net.IP

	// StaticRoutes returns the list of static routes configured by the network
	// driver when the container joins a network
	StaticRoutes() []*types.StaticRoute

	// Sandbox returns the attached sandbox if there, nil otherwise.
	Sandbox() *Sandbox

	// LoadBalancer returns whether the endpoint is the load balancer endpoint for the network.
	LoadBalancer() bool
}

// EndpointInterface holds interface addresses bound to the endpoint.
type EndpointInterface struct {
	mac                net.HardwareAddr
	addr               *net.IPNet
	addrv6             *net.IPNet
	llAddrs            []*net.IPNet
	srcName            string
	dstPrefix          string
	dstName            string // dstName is the name of the interface in the container namespace. It takes precedence over dstPrefix.
	routes             []*net.IPNet
	v4PoolID           string
	v6PoolID           string
	netnsPath          string
	createdInContainer bool
}

// endpointInterface is an intermediate struct used to marshal/unmarshal
// am [EndpointInterface] to JSON.
//
// TODO(thaJeztah): use "omitzero" for all fields once we no longer have to consider downgrades to < v29.0; see https://github.com/moby/moby/pull/51223#discussion_r2445826610
type endpointInterface struct {
	MAC                string         `json:"mac"`
	Addr               netip.Prefix   `json:"addr"`
	AddrV6             netip.Prefix   `json:"addrv6"`
	LLAddrs            []netip.Prefix `json:"llAddrs"`
	Routes             []netip.Prefix `json:"routes"`
	SrcName            string         `json:"srcName"`
	DstPrefix          string         `json:"dstPrefix"`
	DstName            string         `json:"dstName"`
	V4PoolID           string         `json:"v4PoolID"`
	V6PoolID           string         `json:"v6PoolID"`
	CreatedInContainer bool           `json:"createdInContainer"`
}

func (epi *EndpointInterface) MarshalJSON() ([]byte, error) {
	mac := ""
	if epi.mac != nil {
		mac = epi.mac.String()
	}

	return json.Marshal(&endpointInterface{
		MAC:                mac,
		Addr:               toPrefix(epi.addr),
		AddrV6:             toPrefix(epi.addrv6),
		LLAddrs:            toPrefixes(epi.llAddrs),
		Routes:             toPrefixes(epi.routes),
		SrcName:            epi.srcName,
		DstPrefix:          epi.dstPrefix,
		DstName:            epi.dstName,
		V4PoolID:           epi.v4PoolID,
		V6PoolID:           epi.v6PoolID,
		CreatedInContainer: epi.createdInContainer,
	})
}

func (epi *EndpointInterface) UnmarshalJSON(b []byte) error {
	var epiTmp endpointInterface
	if err := json.Unmarshal(b, &epiTmp); err != nil {
		return err
	}

	var mac net.HardwareAddr
	if epiTmp.MAC != "" {
		hw, err := net.ParseMAC(epiTmp.MAC)
		if err != nil {
			return types.InternalErrorf("invalid mac %q: %v", epiTmp.MAC, err)
		}
		mac = hw
	}

	*epi = EndpointInterface{
		mac:                mac,
		addr:               netiputil.ToIPNet(epiTmp.Addr),
		addrv6:             netiputil.ToIPNet(epiTmp.AddrV6),
		llAddrs:            toIPNets(epiTmp.LLAddrs),
		routes:             toIPNets(epiTmp.Routes),
		srcName:            epiTmp.SrcName,
		dstPrefix:          epiTmp.DstPrefix,
		dstName:            epiTmp.DstName,
		v4PoolID:           epiTmp.V4PoolID,
		v6PoolID:           epiTmp.V6PoolID,
		createdInContainer: epiTmp.CreatedInContainer,
	}

	return nil
}

// Copy returns a deep copy of [EndpointInterface]. If the receiver is nil,
// Copy returns nil.
func (epi *EndpointInterface) Copy() *EndpointInterface {
	if epi == nil {
		return nil
	}

	var routes []*net.IPNet
	for _, route := range epi.routes {
		routes = append(routes, types.GetIPNetCopy(route))
	}

	return &EndpointInterface{
		mac:                slices.Clone(epi.mac),
		addr:               types.GetIPNetCopy(epi.addr),
		addrv6:             types.GetIPNetCopy(epi.addrv6),
		llAddrs:            slices.Clone(epi.llAddrs),
		srcName:            epi.srcName,
		dstPrefix:          epi.dstPrefix,
		dstName:            epi.dstName,
		routes:             routes,
		v4PoolID:           epi.v4PoolID,
		v6PoolID:           epi.v6PoolID,
		netnsPath:          epi.netnsPath,
		createdInContainer: epi.createdInContainer,
	}
}

type endpointJoinInfo struct {
	gw                    net.IP
	gw6                   net.IP
	forceGw4              bool
	forceGw6              bool
	StaticRoutes          []*types.StaticRoute
	driverTableEntries    []*tableEntry
	disableGatewayService bool
}

type tableEntry struct {
	tableName string
	key       string
	value     []byte
}

// Info hydrates the endpoint and returns certain operational data belonging
// to this endpoint.
//
// TODO(thaJeztah): make sure that Endpoint is always fully hydrated, and remove the EndpointInfo interface, and use Endpoint directly.
func (ep *Endpoint) Info() EndpointInfo {
	if ep.sandboxID != "" {
		return ep
	}
	n, err := ep.getNetworkFromStore()
	if err != nil {
		return nil
	}

	ep, err = n.getEndpointFromStore(ep.ID())
	if err != nil {
		return nil
	}

	sb, ok := ep.getSandbox()
	if !ok {
		// endpoint hasn't joined any sandbox.
		// Just return the endpoint
		return ep
	}

	return sb.GetEndpoint(ep.ID())
}

// Iface returns information about the interface which was assigned to
// the endpoint by the driver. This can be used after the
// endpoint has been created.
func (ep *Endpoint) Iface() *EndpointInterface {
	ep.mu.Lock()
	defer ep.mu.Unlock()
	return ep.iface
}

// SetMacAddress allows the driver to set the mac address to the endpoint interface
// during the call to CreateEndpoint, if the mac address is not already set.
func (epi *EndpointInterface) SetMacAddress(mac net.HardwareAddr) error {
	if epi.mac != nil {
		return types.ForbiddenErrorf("endpoint interface MAC address present (%s). Cannot be modified with %s.", epi.mac, mac)
	}
	if mac == nil {
		return types.InvalidParameterErrorf("tried to set nil MAC address to endpoint interface")
	}
	epi.mac = slices.Clone(mac)
	return nil
}

func (epi *EndpointInterface) SetIPAddress(address *net.IPNet) error {
	if address.IP == nil {
		return types.InvalidParameterErrorf("tried to set nil IP address to endpoint interface")
	}
	if address.IP.To4() == nil {
		return setAddress(&epi.addrv6, address)
	}
	return setAddress(&epi.addr, address)
}

func setAddress(ifaceAddr **net.IPNet, address *net.IPNet) error {
	if *ifaceAddr != nil {
		return types.ForbiddenErrorf("endpoint interface IP present (%s). Cannot be modified with (%s).", *ifaceAddr, address)
	}
	*ifaceAddr = types.GetIPNetCopy(address)
	return nil
}

// MacAddress returns the MAC address assigned to the endpoint.
func (epi *EndpointInterface) MacAddress() net.HardwareAddr {
	return slices.Clone(epi.mac)
}

// Address returns the IPv4 address assigned to the endpoint.
func (epi *EndpointInterface) Address() *net.IPNet {
	return types.GetIPNetCopy(epi.addr)
}

func (epi *EndpointInterface) Addr() netip.Prefix {
	p, _ := netiputil.ToPrefix(epi.addr)
	return p
}

// AddressIPv6 returns the IPv6 address assigned to the endpoint.
func (epi *EndpointInterface) AddressIPv6() *net.IPNet {
	return types.GetIPNetCopy(epi.addrv6)
}

func (epi *EndpointInterface) AddrIPv6() netip.Prefix {
	p, _ := netiputil.ToPrefix(epi.addrv6)
	return p
}

// LinkLocalAddresses returns the list of link-local (IPv4/IPv6) addresses assigned to the endpoint.
func (epi *EndpointInterface) LinkLocalAddresses() []*net.IPNet {
	return epi.llAddrs
}

// SrcName returns the name of the interface w/in the container
func (epi *EndpointInterface) SrcName() string {
	return epi.srcName
}

// SetNames method assigns the srcName, dstName, and dstPrefix for the
// interface. If both dstName and dstPrefix are set, dstName takes precedence.
func (epi *EndpointInterface) SetNames(srcName, dstPrefix, dstName string) error {
	epi.srcName = srcName
	epi.dstPrefix = dstPrefix
	epi.dstName = dstName
	return nil
}

// NetnsPath returns the path of the network namespace, if there is one. Else "".
func (epi *EndpointInterface) NetnsPath() string {
	return epi.netnsPath
}

// SetCreatedInContainer can be called by the driver to indicate that it's
// created the network interface in the container's network namespace (so,
// it doesn't need to be moved there).
func (epi *EndpointInterface) SetCreatedInContainer(cic bool) {
	epi.createdInContainer = cic
}

func (ep *Endpoint) InterfaceName() driverapi.InterfaceNameInfo {
	ep.mu.Lock()
	defer ep.mu.Unlock()
	return ep.iface
}

// AddStaticRoute adds a route to the sandbox.
// It may be used in addition to or instead of a default gateway (as above).
func (ep *Endpoint) AddStaticRoute(destination *net.IPNet, routeType types.RouteType, nextHop net.IP) error {
	ep.mu.Lock()
	defer ep.mu.Unlock()
	if routeType == types.NEXTHOP {
		// If the route specifies a next-hop, then it's loosely routed (i.e. not bound to a particular interface).
		ep.joinInfo.StaticRoutes = append(ep.joinInfo.StaticRoutes, &types.StaticRoute{
			Destination: destination,
			RouteType:   routeType,
			NextHop:     nextHop,
		})
	} else {
		// If the route doesn't specify a next-hop, it must be a connected route, bound to an interface.
		ep.iface.routes = append(ep.iface.routes, destination)
	}
	return nil
}

// AddTableEntry adds a table entry to the gossip layer
// passing the table name, key and an opaque value.
func (ep *Endpoint) AddTableEntry(tableName, key string, value []byte) error {
	ep.mu.Lock()
	defer ep.mu.Unlock()

	ep.joinInfo.driverTableEntries = append(ep.joinInfo.driverTableEntries, &tableEntry{
		tableName: tableName,
		key:       key,
		value:     value,
	})

	return nil
}

// Sandbox returns the attached sandbox if there, nil otherwise.
func (ep *Endpoint) Sandbox() *Sandbox {
	cnt, ok := ep.getSandbox()
	if !ok {
		return nil
	}
	return cnt
}

// LoadBalancer returns whether the endpoint is the load balancer endpoint for the network.
func (ep *Endpoint) LoadBalancer() bool {
	ep.mu.Lock()
	defer ep.mu.Unlock()
	return ep.loadBalancer
}

// StaticRoutes returns the list of static routes configured by the network
// driver when the container joins a network
func (ep *Endpoint) StaticRoutes() []*types.StaticRoute {
	ep.mu.Lock()
	defer ep.mu.Unlock()

	if ep.joinInfo == nil {
		return nil
	}

	return ep.joinInfo.StaticRoutes
}

// Gateway returns the IPv4 gateway assigned by the driver.
// This will only return a valid value if a container has joined the endpoint.
func (ep *Endpoint) Gateway() net.IP {
	ep.mu.Lock()
	defer ep.mu.Unlock()

	if ep.joinInfo == nil {
		return net.IP{}
	}

	return slices.Clone(ep.joinInfo.gw)
}

// GatewayIPv6 returns the IPv6 gateway assigned by the driver.
// This will only return a valid value if a container has joined the endpoint.
func (ep *Endpoint) GatewayIPv6() net.IP {
	ep.mu.Lock()
	defer ep.mu.Unlock()

	if ep.joinInfo == nil {
		return net.IP{}
	}

	return slices.Clone(ep.joinInfo.gw6)
}

// SetGateway sets the default IPv4 gateway when a container joins the endpoint.
func (ep *Endpoint) SetGateway(gw net.IP) error {
	ep.mu.Lock()
	defer ep.mu.Unlock()

	ep.joinInfo.gw = slices.Clone(gw)
	return nil
}

// SetGatewayIPv6 sets the default IPv6 gateway when a container joins the endpoint.
func (ep *Endpoint) SetGatewayIPv6(gw6 net.IP) error {
	ep.mu.Lock()
	defer ep.mu.Unlock()

	ep.joinInfo.gw6 = slices.Clone(gw6)
	return nil
}

func (ep *Endpoint) ForceGw4() {
	ep.mu.Lock()
	defer ep.mu.Unlock()
	ep.joinInfo.forceGw4 = true
}

func (ep *Endpoint) ForceGw6() {
	ep.mu.Lock()
	defer ep.mu.Unlock()
	ep.joinInfo.forceGw6 = true
}

// hasGatewayOrDefaultRoute returns true if ep has a gateway, or a route to '0.0.0.0'/'::'.
func (ep *Endpoint) hasGatewayOrDefaultRoute() (v4, v6 bool) {
	ep.mu.Lock()
	defer ep.mu.Unlock()

	if ep.joinInfo != nil {
		v4 = len(ep.joinInfo.gw) > 0 || ep.joinInfo.forceGw4
		v6 = len(ep.joinInfo.gw6) > 0 || ep.joinInfo.forceGw6
		if !v4 || !v6 {
			for _, route := range ep.joinInfo.StaticRoutes {
				if route.Destination.IP.IsUnspecified() && net.IP(route.Destination.Mask).IsUnspecified() {
					if route.Destination.IP.To4() == nil {
						v6 = true
					} else {
						v4 = true
					}
				}
			}
		}
	}
	if ep.iface != nil && (!v4 || !v6) {
		for _, route := range ep.iface.routes {
			if route.IP.IsUnspecified() && net.IP(route.Mask).IsUnspecified() {
				if route.IP.To4() == nil {
					v6 = true
				} else {
					v4 = true
				}
			}
		}
	}
	return v4, v6
}

func (ep *Endpoint) retrieveFromStore() (*Endpoint, error) {
	n, err := ep.getNetworkFromStore()
	if err != nil {
		return nil, fmt.Errorf("could not find network in store to get latest endpoint %s: %v", ep.Name(), err)
	}
	return n.getEndpointFromStore(ep.ID())
}

// DisableGatewayService tells libnetwork not to provide Default GW for the container
func (ep *Endpoint) DisableGatewayService() {
	ep.mu.Lock()
	defer ep.mu.Unlock()

	ep.joinInfo.disableGatewayService = true
}

func (epj *endpointJoinInfo) MarshalJSON() ([]byte, error) {
	epMap := make(map[string]any)
	if epj.gw != nil {
		epMap["gw"] = epj.gw.String()
	}
	if epj.gw6 != nil {
		epMap["gw6"] = epj.gw6.String()
	}
	epMap["disableGatewayService"] = epj.disableGatewayService
	epMap["StaticRoutes"] = epj.StaticRoutes
	return json.Marshal(epMap)
}

func (epj *endpointJoinInfo) UnmarshalJSON(b []byte) error {
	var (
		err   error
		epMap map[string]any
	)
	if err = json.Unmarshal(b, &epMap); err != nil {
		return err
	}
	if v, ok := epMap["gw"]; ok {
		epj.gw = net.ParseIP(v.(string))
	}
	if v, ok := epMap["gw6"]; ok {
		epj.gw6 = net.ParseIP(v.(string))
	}
	epj.disableGatewayService = epMap["disableGatewayService"].(bool)

	var tStaticRoute []types.StaticRoute
	if v, ok := epMap["StaticRoutes"]; ok {
		// TODO(cpuguy83): Linter caught that we aren't checking errors here
		// I don't know why we aren't other than potentially the data is not always expected to be right?
		// This is why I'm not adding the error check.
		//
		// In any case for posterity please if you figure this out document it or check the error
		tb, _ := json.Marshal(v)              //nolint:errchkjson // FIXME: handle json (Un)Marshal errors (see above)
		_ = json.Unmarshal(tb, &tStaticRoute) //nolint:errcheck
	}
	var StaticRoutes []*types.StaticRoute
	for _, r := range tStaticRoute {
		StaticRoutes = append(StaticRoutes, &r)
	}
	epj.StaticRoutes = StaticRoutes

	return nil
}

func (epj *endpointJoinInfo) Copy() *endpointJoinInfo {
	if epj == nil {
		return nil
	}

	return &endpointJoinInfo{
		gw:                    slices.Clone(epj.gw),
		gw6:                   slices.Clone(epj.gw6),
		StaticRoutes:          slices.Clone(epj.StaticRoutes),
		driverTableEntries:    slices.Clone(epj.driverTableEntries),
		disableGatewayService: epj.disableGatewayService,
	}
}

func toPrefix(n *net.IPNet) netip.Prefix {
	p, _ := netiputil.ToPrefix(n)
	return p
}

func toIPNets(ps []netip.Prefix) []*net.IPNet {
	out := make([]*net.IPNet, 0, len(ps))
	for _, p := range ps {
		if n := netiputil.ToIPNet(p); n != nil {
			out = append(out, n)
		}
	}
	return out
}

func toPrefixes(nets []*net.IPNet) []netip.Prefix {
	out := make([]netip.Prefix, 0, len(nets))
	for _, n := range nets {
		if prefix, ok := netiputil.ToPrefix(n); ok {
			out = append(out, prefix)
		}
	}
	return out
}
