// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause

package tstun

import (
	"fmt"
	"sync"
	"time"

	"github.com/tailscale/wireguard-go/tun"
	"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
	"tailscale.com/types/logger"
)

// ifaceWatcher waits for an interface to be up.
type ifaceWatcher struct {
	logf logger.Logf
	luid winipcfg.LUID

	mu   sync.Mutex // guards following
	done bool
	sig  chan bool
}

// callback is the callback we register with Windows to call when IP interface changes.
func (iw *ifaceWatcher) callback(notificationType winipcfg.MibNotificationType, iface *winipcfg.MibIPInterfaceRow) {
	// Probably should check only when MibParameterNotification, but just in case included MibAddInstance also.
	if notificationType == winipcfg.MibParameterNotification || notificationType == winipcfg.MibAddInstance {
		// Out of paranoia, start a goroutine to finish our work, to return to Windows out of this callback.
		go iw.isUp()
	}
}

func (iw *ifaceWatcher) isUp() bool {
	iw.mu.Lock()
	defer iw.mu.Unlock()

	if iw.done {
		// We already know that it's up
		return true
	}

	if iw.getOperStatus() != winipcfg.IfOperStatusUp {
		return false
	}

	iw.done = true
	iw.sig <- true
	return true
}

func (iw *ifaceWatcher) getOperStatus() winipcfg.IfOperStatus {
	ifc, err := iw.luid.Interface()
	if err != nil {
		iw.logf("iw.luid.Interface error: %v", err)
		return 0
	}
	return ifc.OperStatus
}

func waitInterfaceUp(iface tun.Device, timeout time.Duration, logf logger.Logf) error {
	iw := &ifaceWatcher{
		luid: winipcfg.LUID(iface.(*tun.NativeTun).LUID()),
		logf: logger.WithPrefix(logf, "waitInterfaceUp: "),
	}

	// Just in case check the status first
	if iw.getOperStatus() == winipcfg.IfOperStatusUp {
		iw.logf("TUN interface already up; no need to wait")
		return nil
	}

	iw.sig = make(chan bool, 1)
	cb, err := winipcfg.RegisterInterfaceChangeCallback(iw.callback)
	if err != nil {
		iw.logf("RegisterInterfaceChangeCallback error: %v", err)
		return err
	}
	defer cb.Unregister()

	t0 := time.Now()
	expires := t0.Add(timeout)
	ticker := time.NewTicker(10 * time.Second)
	defer ticker.Stop()

	for {
		iw.logf("waiting for TUN interface to come up...")

		select {
		case <-iw.sig:
			iw.logf("TUN interface is up after %v", time.Since(t0))
			return nil
		case <-ticker.C:
		}

		if iw.isUp() {
			// Very unlikely to happen - either NotifyIpInterfaceChange doesn't work
			// or it came up in the same moment as tick. Indicate this in the log message.
			iw.logf("TUN interface is up after %v (on poll, without notification)", time.Since(t0))
			return nil
		}

		if expires.Before(time.Now()) {
			iw.logf("timeout waiting %v for TUN interface to come up", timeout)
			return fmt.Errorf("timeout waiting for TUN interface to come up")
		}
	}
}
