package pmtud

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

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

var (
	ErrICMPOkTCPFail   = errors.New("PMTUD succeeded with ICMP but failed with TCP")
	ErrICMPFailTCPFail = errors.New("PMTUD failed with both ICMP and 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, fw tcp.Firewall, 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
		}
	}

	minMTU := constants.MinIPv4MTU
	if tcpAddrs[0].Addr().Is6() {
		minMTU = constants.MinIPv6MTU
	}
	if icmpSuccess {
		const mtuMargin = 150
		minMTU = max(maxPossibleMTU-mtuMargin, minMTU)
	}
	mtu, err = tcp.PathMTUDiscover(ctx, tcpAddrs, minMTU, maxPossibleMTU, tryTimeout, fw, logger)
	if err != nil {
		if errors.Is(err, iptables.ErrMarkMatchModuleMissing) {
			logger.Debugf("aborting TCP path MTU discovery: %s", err)
			if icmpSuccess {
				return maxPossibleMTU, nil // only rely on ICMP PMTUD results
			}
		}
		if icmpSuccess {
			return 0, fmt.Errorf("%w - discarding ICMP obtained MTU %d",
				ErrICMPOkTCPFail, maxPossibleMTU)
		}
		return 0, fmt.Errorf("%w", ErrICMPFailTCPFail)
	}
	logger.Debugf("TCP path MTU discovery found maximum valid MTU %d", mtu)
	return mtu, nil
}
