package appsec

import (
	"strconv"
	"time"

	"github.com/crowdsecurity/crowdsec/pkg/appsec/challenge"
	"github.com/crowdsecurity/crowdsec/pkg/pipeline"
)

const (
	// ModuleName is the acquisition module name carried on events produced by
	// the appsec datasource.
	ModuleName = "appsec"

	// SourceWAF is the `source` field set on events generated by a WAF rule match.
	SourceWAF = "crowdsec-appsec"
	// SourceChallenge is the `source` field set on events generated by the
	// challenge lifecycle. It is intentionally distinct from SourceWAF so that
	// challenge scenarios can filter on it without colliding with WAF scenarios.
	SourceChallenge = "crowdsec-appsec-challenge"
)

// ChallengeReason identifies a challenge lifecycle moment. Its string value is
// set as the `challenge_event` field on events produced by
// ChallengeEventFromRequest.
type ChallengeReason string

const (
	ChallengeReasonRequested ChallengeReason = "requested" // challenge page served to the client
	ChallengeReasonSubmitted ChallengeReason = "submitted" // a challenge validation attempt was received
	ChallengeReasonFailed    ChallengeReason = "failed"    // submission failed crypto/PoW validation
	ChallengeReasonRejected  ChallengeReason = "rejected"  // submission decoded fine but an on_challenge_submit hook called RejectSubmission
	ChallengeReasonSolved    ChallengeReason = "solved"    // validation passed, cookie issued
)

// baseEventFromRequest builds the common pipeline.Event skeleton shared by WAF
// and challenge events: the request metadata in Parsed and the acquisition Line.
func baseEventFromRequest(r *ParsedRequest, labels map[string]string, txUuid, source string) pipeline.Event {
	evt := pipeline.MakeEvent(false, pipeline.LOG, true)
	evt.Stage = "s00-raw"
	evt.Parsed = map[string]string{
		"source_ip":           r.ClientIP,
		"target_host":         r.Host,
		"target_uri":          r.URI,
		"method":              r.Method,
		"req_uuid":            txUuid,
		"source":              source,
		"remediation_cmpt_ip": r.RemoteAddrNormalized,
	}
	evt.Line = pipeline.Line{
		Time:    time.Now(),
		Labels:  labels,
		Process: true,
		Module:  ModuleName,
		Src:     ModuleName,
		Raw:     "dummy-appsec-data", // we discard empty Line.Raw items :)
	}
	evt.Appsec = pipeline.AppsecEvent{}

	return evt
}

func EventFromRequest(r *ParsedRequest, labels map[string]string, txUuid string) (pipeline.Event, error) {
	return baseEventFromRequest(r, labels, txUuid, SourceWAF), nil
}

// ChallengeEventInfo describes a single challenge lifecycle moment to be turned
// into a pipeline.Event by ChallengeEventFromRequest.
type ChallengeEventInfo struct {
	Reason      ChallengeReason            // requested | submitted | failed | rejected | solved
	FailReason  string                     // set on failed (raw error message) or rejected (operator-supplied reason)
	FailErr     error                      // set on failed; carries the sentinel-wrapped error so metrics can classify via errors.Is.
	Difficulty  int                        // target PoW difficulty for this moment
	Fingerprint *challenge.FingerprintData // nil when none available (e.g. requested w/o cookie)
}

// ChallengeEventFromRequest builds a LOG event for a challenge lifecycle moment.
// It carries SourceChallenge so it never collides with WAF scenarios, plus the
// reason, difficulty and (when available) fingerprint scalars for filtering.
func ChallengeEventFromRequest(r *ParsedRequest, labels map[string]string, txUuid string, info ChallengeEventInfo) pipeline.Event {
	evt := baseEventFromRequest(r, labels, txUuid, SourceChallenge)

	if r.HTTPRequest != nil {
		evt.Parsed["user_agent"] = r.HTTPRequest.UserAgent()
	}

	evt.Parsed["challenge_event"] = string(info.Reason)
	evt.Parsed["challenge_difficulty"] = strconv.Itoa(info.Difficulty)
	if info.FailReason != "" {
		evt.Parsed["challenge_fail_reason"] = info.FailReason
	}

	if fp := info.Fingerprint; fp != nil {
		// Expose the full fingerprint (all nested signals, bot-detection details,
		// etc.) so scenarios and alert-context can traverse it as
		// evt.Unmarshaled.fingerprint.Signals... Stored as a pointer so expr can
		// also call the *FingerprintData helper methods (Platform(), BotSignals(),
		// IsBot(), ...). It stays out of LAPI/CAPI unless explicitly mapped into Meta.
		evt.Unmarshaled["fingerprint"] = fp
		// Flat shortcuts for cheap scenario filtering.
		evt.Parsed["fsid"] = fp.FSID
		evt.Parsed["fingerprint_bot"] = strconv.FormatBool(fp.FastBotDetection.Bool())
		evt.Parsed["fingerprint_allowlisted"] = strconv.FormatBool(fp.Allowlisted)
		if fp.AllowlistReason != "" {
			evt.Parsed["fingerprint_allowlist_reason"] = fp.AllowlistReason
		}
		evt.Parsed["platform"] = fp.Signals.Device.Platform
	}

	return evt
}
