package node

import (
	"context"
	"time"

	runtime "iop/packages/go/execution"
)

// healthProbeCeiling is the independent upper bound on a single exact-target
// health probe. The probe never inherits the stalled execution request's
// context, deadline, or cancellation; it always roots a fresh deadline from
// the background so a canceling parent cannot cut the evidence short. It is a
// var rather than a const so deterministic tests can lower it without scheduler
// sleeps; production always observes the five-second ceiling.
var healthProbeCeiling = 5 * time.Second

// probeFunc is the injectable hook over ProviderProber.ProbeProvider. Tests
// inject deterministic providers and observe the bounded context; production
// resolves the adapter's ProviderProber implementation through ResolveProbeFunc.
type probeFunc func(ctx context.Context, target string) (runtime.ProviderProbeResult, error)

// ResolveProbeFunc returns a probe hook bound to the adapter's ProviderProber
// implementation, or nil when the adapter does not support active probing. A
// nil hook makes ProbeHealth fail closed to HealthUnknown without invoking any
// provider endpoint.
func ResolveProbeFunc(adapter runtime.Provider) probeFunc {
	prober, ok := adapter.(runtime.ProviderProber)
	if !ok {
		return nil
	}
	return prober.ProbeProvider
}

// HealthProbeEvidence is the fail-closed evidence returned by ProbeHealth. It
// carries only stable, coordinator-owned values: it never copies arbitrary
// provider metadata, resets progress, changes the attempt fence, or authorizes
// retry. Terminal assembly may consume Health as evidence only.
type HealthProbeEvidence struct {
	Health runtime.ProviderHealth
	Status runtime.ProviderStatus
	Detail string
}

// ProbeHealth performs a single bounded exact-target health probe of the named
// adapter and target, independent of any stalled execution request. It roots
// its own deadline from the background, runs the hook concurrently so a probe
// that ignores context cancellation cannot hold the coordinator past the
// independent ceiling, re-checks that deadline/cancellation after the probe
// returns, validates that the probe confirmed the exact adapter and target
// identity, and returns only the fail-closed normalized evidence.
//
// adapterName and instanceKey identify the stalled execution's required
// provider (instanceKey may be empty for single-instance adapters); target is
// the exact target that stalled. probe is the injectable ProviderProber hook,
// or nil when the adapter does not support probing. ProbeHealth never calls
// observer progress/reset, never changes the attempt fence, and never
// authorizes retry.
func ProbeHealth(adapterName, instanceKey, target string, probe probeFunc) HealthProbeEvidence {
	probeCtx, cancel := context.WithTimeout(context.Background(), healthProbeCeiling)
	defer cancel()

	outcome := runtime.ProbeOutcome{
		ExpectedAdapter:  adapterName,
		ExpectedInstance: instanceKey,
		ExpectedTarget:   target,
	}
	if probe == nil {
		outcome.Err = runtime.ErrProbeUnsupported
		return finalizeHealthProbe(outcome)
	}

	return finalizeHealthProbe(runProbe(probeCtx, target, probe, outcome))
}

// probeCallResult is the typed result the probe goroutine reports to the
// coordinator. It lets the coordinator select a completed probe against its
// independent deadline without holding return time hostage to a hook that
// ignores context cancellation.
type probeCallResult struct {
	result runtime.ProviderProbeResult
	err    error
}

// runProbe invokes the probe hook on a background goroutine and selects its
// result against the independent probe context. The result channel is buffered
// to size one so a late-finishing hook can complete and send after the
// coordinator has already returned, without blocking. On the deadline branch
// the context error is surfaced as health_unknown through the normalizer; on
// the result branch the post-result context recheck is preserved so a
// simultaneously expired deadline still wins fail-closed. runProbe never calls
// observer progress/reset, never changes the attempt fence, and never
// authorizes retry.
func runProbe(probeCtx context.Context, target string, probe probeFunc, outcome runtime.ProbeOutcome) runtime.ProbeOutcome {
	resultCh := make(chan probeCallResult, 1)
	go func() {
		res, err := probe(probeCtx, target)
		resultCh <- probeCallResult{result: res, err: err}
	}()

	select {
	case call := <-resultCh:
		err := call.err
		// Re-check the independent deadline/cancellation even when the probe
		// returns nil error: a probe that ignored its bound context must still
		// be treated as inconclusive rather than allowed to manufacture a
		// definitive result. A result racing a simultaneous deadline expiry
		// therefore stays fail-closed.
		if err == nil && probeCtx.Err() != nil {
			err = probeCtx.Err()
		}
		outcome.AdapterName = call.result.AdapterName
		outcome.InstanceKey = call.result.InstanceKey
		outcome.Target = call.result.Target
		outcome.Status = call.result.Status
		outcome.Err = err
	case <-probeCtx.Done():
		outcome.Err = probeCtx.Err()
	}
	return outcome
}

// finalizeHealthProbe normalizes the probe outcome and packages the stable
// evidence. It is the single path that feeds the typed outcome normalizer.
func finalizeHealthProbe(outcome runtime.ProbeOutcome) HealthProbeEvidence {
	classification := runtime.ClassifyProbeOutcome(outcome)
	return HealthProbeEvidence{
		Health: runtime.HealthFromClassification(classification),
		Status: runtime.NormalizeProviderStatus(outcome.Status),
		Detail: healthProbeDetail(outcome, classification),
	}
}

// healthProbeDetail returns a short, coordinator-owned reason string for the
// evidence. It never copies arbitrary provider metadata; only the probe's own
// error message (when present) is surfaced for diagnostics.
func healthProbeDetail(outcome runtime.ProbeOutcome, classification runtime.LivenessClassification) string {
	switch classification {
	case runtime.LivenessAvailable:
		return "exact target available"
	case runtime.LivenessUnavailable:
		return "exact target unavailable"
	case runtime.LivenessTimeout:
		return "probe timed out"
	case runtime.LivenessUnsupported:
		return "adapter does not support probing"
	case runtime.LivenessIdentityMismatch:
		return "probe identity did not match request"
	case runtime.LivenessUnknown:
		return "probe returned unknown status"
	default:
		if outcome.Err != nil {
			return outcome.Err.Error()
		}
		return "probe inconclusive"
	}
}
