package poke

import (
	"context"
	"log"
	"time"

	"gitea.dooplex.hu/admin/felhom-hub/internal/store"
)

// peerStore is the WG-peer lookup seam (satisfied by *store.Store; tests fake it).
type peerStore interface {
	GetWGPeerForHost(hostID string) (*store.WGPeer, error)
	ListWGPeers() ([]store.WGPeer, error)
}

// pokeSender is the SSH send seam (satisfied by *Client; tests fake it).
type pokeSender interface {
	Poke(ctx context.Context, boxWGIP string) error
}

// Notifier turns an operator-intent event (a host id, or "all hosts") into fire-and-forget pokes.
// Every public method returns immediately and does the SSH work on a detached goroutine, so a poke
// NEVER blocks or fails the hub save that triggered it — a lost poke is harmless (the 15-min cycle
// reconciles). nil *Notifier is a safe no-op (poke disabled / not configured).
type Notifier struct {
	store   peerStore
	client  pokeSender
	timeout time.Duration
	logger  *log.Logger
}

// NewNotifier builds a notifier. timeout bounds one poke (default 10s).
func NewNotifier(st peerStore, client pokeSender, logger *log.Logger) *Notifier {
	if logger == nil {
		logger = log.Default()
	}
	return &Notifier{store: st, client: client, timeout: 10 * time.Second, logger: logger}
}

// PokeHost fires a poke to one host's box (async, fire-and-forget). Safe on a nil receiver.
func (n *Notifier) PokeHost(hostID string) {
	if n == nil {
		return
	}
	go func() {
		if err := n.pokeHost(context.Background(), hostID); err != nil {
			n.logger.Printf("[INFO] poke: host %s not nudged (%v) — the 15-min cycle still reconciles", hostID, err)
		}
	}()
}

// PokeAllHosts fires a poke to EVERY host with a WG peer (async, fire-and-forget). Used by a
// fleet-wide agent-plane change (e.g. a MinAgent floor). Safe on a nil receiver.
func (n *Notifier) PokeAllHosts() {
	if n == nil {
		return
	}
	go func() {
		peers, err := n.store.ListWGPeers()
		if err != nil {
			n.logger.Printf("[INFO] poke: fleet nudge skipped (peer list: %v) — the 15-min cycle still reconciles", err)
			return
		}
		nudged := 0
		for _, p := range peers {
			if p.HostID == "" || p.AssignedIP == "" {
				continue
			}
			ctx, cancel := context.WithTimeout(context.Background(), n.timeout)
			if err := n.client.Poke(ctx, p.AssignedIP); err != nil {
				n.logger.Printf("[INFO] poke: host %s (%s) not nudged (%v)", p.HostID, p.AssignedIP, err)
			} else {
				nudged++
			}
			cancel()
		}
		n.logger.Printf("[INFO] poke: fleet nudge sent to %d/%d WG peers", nudged, len(peers))
	}()
}

// pokeHost is the synchronous body (test seam). Resolves the host's WG /32 and sends one poke.
func (n *Notifier) pokeHost(ctx context.Context, hostID string) error {
	peer, err := n.store.GetWGPeerForHost(hostID)
	if err != nil {
		return err
	}
	if peer == nil || peer.AssignedIP == "" {
		return errNoPeer
	}
	tctx, cancel := context.WithTimeout(ctx, n.timeout)
	defer cancel()
	return n.client.Poke(tctx, peer.AssignedIP)
}

type pokeErr string

func (e pokeErr) Error() string { return string(e) }

const errNoPeer = pokeErr("host has no WG peer")
