package pmtud

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

	"github.com/qdm12/gluetun/internal/pmtud/constants"
	"github.com/qdm12/gluetun/internal/pmtud/icmp"
	"github.com/qdm12/gluetun/internal/pmtud/tcp"
)

// PathMTUDiscover discovers the maximum MTU using both ICMP and TCP.
// Multiple ICMP addresses and TCP addresses can be specified for redundancy.
// ICMP PMTUD is run first. If successful, the range of possible MTU values to
// check for TCP PMTUD is reduced to [maxMTU-150, maxMTU] where maxMTU is the
// maximum MTU found with ICMP PMTUD. Otherwise, TCP PMTUD is run with the
// whole range of possible MTU values up to the physical link MTU to check.
// If the physicalLinkMTU is zero, it defaults to 1500 which is the ethernet standard MTU.
// If the pingTimeout is zero, it defaults to 1 second.
// If the logger is nil, a no-op logger is used.
// It returns [ErrMTUNotFound] if the MTU could not be determined.
func PathMTUDiscover(ctx context.Context, icmpAddrs []netip.Addr, tcpAddrs []netip.AddrPort,
	physicalLinkMTU uint32, tryTimeout time.Duration, logger Logger) (
	mtu uint32, err error,
) {
	if physicalLinkMTU == 0 {
		const ethernetStandardMTU = 1500
		physicalLinkMTU = ethernetStandardMTU
	}
	if tryTimeout == 0 {
		tryTimeout = time.Second
	}
	if logger == nil {
		logger = &noopLogger{}
	}

	// Try finding the MTU using ICMP
	maxPossibleMTU := physicalLinkMTU
	icmpSuccess := false
	for _, icmpIP := range icmpAddrs {
		mtu, err := icmp.PathMTUDiscover(ctx, icmpIP, physicalLinkMTU,
			tryTimeout, logger)
		switch {
		case err == nil:
			logger.Debugf("ICMP path MTU discovery against %s found maximum valid MTU %d", icmpIP, mtu)
			icmpSuccess = true
			maxPossibleMTU = mtu
		case errors.Is(err, icmp.ErrNotPermitted), errors.Is(err, icmp.ErrMTUNotFound):
			logger.Debugf("ICMP path MTU discovery failed: %s", err)
		default:
			return 0, fmt.Errorf("ICMP path MTU discovery: %w", err)
		}
		if icmpSuccess {
			break
		}
	}

	for _, addrPort := range tcpAddrs {
		minMTU := constants.MinIPv4MTU
		if addrPort.Addr().Is6() {
			minMTU = constants.MinIPv6MTU
		}
		if icmpSuccess {
			const mtuMargin = 150
			minMTU = max(maxPossibleMTU-mtuMargin, minMTU)
		}
		mtu, err = tcp.PathMTUDiscover(ctx, addrPort, minMTU, maxPossibleMTU, logger)
		if err != nil {
			logger.Debugf("TCP path MTU discovery to %s failed: %s", addrPort, err)
			continue
		}
		logger.Debugf("TCP path MTU discovery to %s found maximum valid MTU %d", addrPort, mtu)
		return mtu, nil
	}
	return 0, fmt.Errorf("TCP path MTU discovery: last error: %w", err)
}
