package verify

import (
	"fmt"
	"strings"
)

// ProbeClass names the kind of external probe a finding supports.
type ProbeClass string

const (
	// ProbeOOB — embed a callback URL in a payload and watch for an
	// out-of-band interaction (blind SSRF/SQLi/RCE/XXE/exfil).
	ProbeOOB ProbeClass = "oob"
	// ProbeDifferential — send an attack and a negative-control request and
	// compare their responses (authz, data-returning injection).
	ProbeDifferential ProbeClass = "differential"
	// ProbeTiming — inject a known delay and measure response latency (blind
	// SQLi, race windows).
	ProbeTiming ProbeClass = "timing"
)

// Default substitution markers in a ProbeSpec. They use § so they never collide
// with normal URL/body characters a payload might contain.
const (
	DefaultMarker = "§PAYLOAD§" // injection point in BaseRequest
	OOBMarker     = "§OOB§"     // callback URL slot in OOBTemplate
	DelayMarker   = "§DELAY§"   // delay-seconds slot in DelayTemplate
)

// ProbeSpec is a self-contained recipe for RE-PROVING a finding against the
// live target — the parameterised context a single concrete request can't carry
// on its own. It is produced by whoever has the injection context (a tool
// adapter or the exploit agent) and consumed by the live oracles. A finding
// with no spec is simply not oracle-probed (it falls through to the model
// verifier), so the recipe is an opt-in upgrade, never a requirement.
type ProbeSpec struct {
	Class ProbeClass `json:"class"`

	// BaseRequest is a raw HTTP/1.1 request (as in Reproduction.HTTPRequest).
	// The injection point is marked by Marker.
	BaseRequest string `json:"base_request"`
	// Marker is the injection token inside BaseRequest. Empty → DefaultMarker.
	Marker string `json:"marker,omitempty"`

	// Differential: the value substituted at Marker for the attack and for the
	// negative control. Indicator is an optional success substring.
	AttackPayload  string `json:"attack_payload,omitempty"`
	ControlPayload string `json:"control_payload,omitempty"`
	Indicator      string `json:"indicator,omitempty"`

	// OOB: the payload substituted at Marker, itself containing OOBMarker where
	// the reserved callback URL is injected.
	OOBTemplate string `json:"oob_template,omitempty"`

	// Timing: the payload substituted at Marker, containing DelayMarker where
	// the delay (in seconds) is injected. A 0-second delay yields a baseline.
	DelayTemplate string `json:"delay_template,omitempty"`
}

// MarkerToken returns the effective injection marker (BaseRequest token).
func (s *ProbeSpec) MarkerToken() string {
	if s == nil || s.Marker == "" {
		return DefaultMarker
	}
	return s.Marker
}

// Valid reports whether the spec is well-formed for its class: it has a base
// request with the injection marker, and the per-class fields it needs.
func (s *ProbeSpec) Valid() bool {
	if s == nil || s.BaseRequest == "" {
		return false
	}
	if !strings.Contains(s.BaseRequest, s.MarkerToken()) {
		return false
	}
	switch s.Class {
	case ProbeOOB:
		return strings.Contains(s.OOBTemplate, OOBMarker)
	case ProbeDifferential:
		return s.AttackPayload != "" || s.ControlPayload != ""
	case ProbeTiming:
		return strings.Contains(s.DelayTemplate, DelayMarker)
	default:
		return false
	}
}

// Inject substitutes payload at the marker in BaseRequest.
func (s *ProbeSpec) Inject(payload string) string {
	return strings.ReplaceAll(s.BaseRequest, s.MarkerToken(), payload)
}

// CanProbe reports whether the candidate carries a valid spec for class.
func (c Candidate) CanProbe(class ProbeClass) bool {
	return c.Spec != nil && c.Spec.Class == class && c.Spec.Valid()
}

// NewOOBSpec builds an out-of-band probe recipe. baseRequest must contain
// marker (the injection point); oobTemplate is the payload placed there and
// must contain OOBMarker where the reserved callback URL is injected. An empty
// marker defaults to DefaultMarker. Returns an error if the result is invalid,
// so a producer can't attach a spec that would silently never fire.
func NewOOBSpec(baseRequest, marker, oobTemplate string) (*ProbeSpec, error) {
	s := &ProbeSpec{Class: ProbeOOB, BaseRequest: baseRequest, Marker: marker, OOBTemplate: oobTemplate}
	return validated(s)
}

// NewDifferentialSpec builds a differential probe recipe: the attack and
// negative-control payloads substituted at the marker, plus an optional success
// indicator substring.
func NewDifferentialSpec(baseRequest, marker, attackPayload, controlPayload, indicator string) (*ProbeSpec, error) {
	s := &ProbeSpec{
		Class: ProbeDifferential, BaseRequest: baseRequest, Marker: marker,
		AttackPayload: attackPayload, ControlPayload: controlPayload, Indicator: indicator,
	}
	return validated(s)
}

// NewTimingSpec builds a timing probe recipe: delayTemplate is the payload
// placed at the marker and must contain DelayMarker where the delay (seconds)
// is injected (e.g. "' AND SLEEP(§DELAY§)-- -").
func NewTimingSpec(baseRequest, marker, delayTemplate string) (*ProbeSpec, error) {
	s := &ProbeSpec{Class: ProbeTiming, BaseRequest: baseRequest, Marker: marker, DelayTemplate: delayTemplate}
	return validated(s)
}

func validated(s *ProbeSpec) (*ProbeSpec, error) {
	if !s.Valid() {
		return nil, fmt.Errorf("invalid %s ProbeSpec: base request must contain the %q marker and the class fields must be set", s.Class, s.MarkerToken())
	}
	return s, nil
}
