// Package oracle holds the concrete external-ground-truth oracles that satisfy
// verify.Oracle: out-of-band callback, differential-response, and timing. They
// live here rather than in internal/swarm/verify so the core gate stays free of
// network and tool dependencies — verify defines the contract, oracle supplies
// the probes.
package oracle

import (
	"context"
	"strings"
	"time"

	"github.com/Armur-Ai/Pentest-Swarm-AI/internal/swarm/blackboard"
	"github.com/Armur-Ai/Pentest-Swarm-AI/internal/swarm/verify"
)

// OOBSession is a live out-of-band listener (interactsh / Collaborator style).
// The whole point of OOB is that confirmation comes from infrastructure WE
// control, not from the target's response — so a positive is ground truth that
// no model and no WAF can fake.
type OOBSession interface {
	// Payload reserves a fresh, unique OOB URL to embed in an attack, plus a
	// correlation token used later to query for interactions on exactly that
	// URL. Reserving BEFORE delivery is mandatory: the listener must be armed
	// before the payload goes out, or a fast callback is missed.
	Payload(ctx context.Context) (url, token string, err error)
	// Observed blocks up to window for any interaction (DNS/HTTP/SMTP/LDAP) on
	// the URL behind token, and returns whether one arrived plus a short proof
	// receipt (protocol + source address + timestamp).
	Observed(ctx context.Context, token string, window time.Duration) (hit bool, proof string, err error)
}

// Delivery embeds oobURL into an attack against the candidate and sends it, so
// that a vulnerable target makes an outbound request to the OOB URL. It is
// supplied by the exploit layer (which knows the injection point and payload
// shape for each class); the oracle stays agnostic to how the payload is built.
// A nil Delivery means "the exploit agent already sent OOB payloads this
// campaign" — the oracle then only listens for a correlating callback.
type Delivery func(ctx context.Context, c verify.Candidate, oobURL string) error

// OOB is an out-of-band interaction oracle. It proves blind vulnerability
// classes — blind SQLi, blind/OOB SSRF, blind RCE, XXE, OOB exfiltration —
// where the only ground-truth signal is the target calling back to a host we
// control.
//
// Honest asymmetry: a callback PROVES the vuln, but silence does NOT disprove
// it (a WAF, egress filtering, or a slow callback all look like silence). So
// OOB returns Proven or Inconclusive, never Disproven.
type OOB struct {
	session OOBSession
	deliver Delivery // may be nil (listen-only correlation)
	window  time.Duration
	applies func(verify.Candidate) bool
}

// Option configures an OOB oracle.
type Option func(*OOB)

// WithWindow sets how long to listen for a callback after delivery.
func WithWindow(d time.Duration) Option {
	return func(o *OOB) {
		if d > 0 {
			o.window = d
		}
	}
}

// WithApplies overrides which candidates this oracle will probe.
func WithApplies(fn func(verify.Candidate) bool) Option {
	return func(o *OOB) {
		if fn != nil {
			o.applies = fn
		}
	}
}

// NewOOB builds an OOB oracle over a live session. deliver may be nil for
// listen-only correlation (the exploit agent planted the OOB payloads itself).
func NewOOB(session OOBSession, deliver Delivery, opts ...Option) *OOB {
	o := &OOB{
		session: session,
		deliver: deliver,
		window:  15 * time.Second,
		applies: defaultOOBApplies,
	}
	for _, opt := range opts {
		opt(o)
	}
	return o
}

// Name implements verify.Oracle.
func (o *OOB) Name() string { return "oob-callback" }

// Applies implements verify.Oracle.
func (o *OOB) Applies(c verify.Candidate) bool {
	if o.session == nil {
		return false
	}
	return o.applies(c)
}

// Probe arms the listener, delivers the OOB payload (if a Delivery was given),
// and waits for a callback. A callback is Proven ground truth; silence is
// Inconclusive, never Disproven.
func (o *OOB) Probe(ctx context.Context, c verify.Candidate) (verify.OracleVerdict, float64, string, error) {
	url, token, err := o.session.Payload(ctx)
	if err != nil {
		return verify.Inconclusive, 0, "", err
	}

	if o.deliver != nil {
		// Delivery failure is an infrastructure problem, not a verdict — report
		// it so the corroborator treats it as Inconclusive.
		if err := o.deliver(ctx, c, url); err != nil {
			return verify.Inconclusive, 0, "", err
		}
	}

	hit, proof, err := o.session.Observed(ctx, token, o.window)
	if err != nil {
		return verify.Inconclusive, 0, "", err
	}
	if hit {
		// A real out-of-band interaction is the hardest evidence there is.
		return verify.Proven, 1.0, proof, nil
	}
	return verify.Inconclusive, 0, "", nil
}

// oobKeywords are detail substrings that mark a finding as OOB-confirmable even
// when its coarse FindingType doesn't say so (there is no dedicated SSRF type).
var oobKeywords = []string{
	"ssrf", "blind", "oob", "out-of-band", "out of band",
	"xxe", "rce", "remote code", "command injection", "cmd injection",
	"exfiltrat", "dns", "callback",
}

// defaultOOBApplies matches the classes a callback can confirm: blind SQLi by
// type, plus any finding whose detail names an OOB-confirmable class.
func defaultOOBApplies(c verify.Candidate) bool {
	if c.Type == blackboard.TypePotentialSQLI {
		return true
	}
	detail := strings.ToLower(c.Detail)
	for _, kw := range oobKeywords {
		if strings.Contains(detail, kw) {
			return true
		}
	}
	return false
}
