package scrape

import (
	"errors"
	"net"
	"net/http"
	"net/url"
	"strings"
)

// FailureKind classifies a scrape failure. Per PROJECT.md §8.9, the three
// categories drive different operator responses:
//
//   - Network: retry with backoff; no operator notification until a high
//     threshold.
//   - Auth: high-priority notification — operator needs to refresh credentials.
//   - Structural: different notification — the tracker probably changed
//     their HTML layout.
//
// RateLimit is a sub-category of Network that the hygiene primitive surfaces
// separately so adapters can compute backoff differently if needed; it does
// NOT alarm the operator on its own.
type FailureKind string

const (
	FailureNone       FailureKind = ""
	FailureNetwork    FailureKind = "network"
	FailureAuth       FailureKind = "auth"
	FailureStructural FailureKind = "structural"
	FailureRateLimit  FailureKind = "rate_limit"
)

// Classify returns the failure kind for a (response, error, body) tuple.
// Any of the inputs may be zero-valued; pass what you have. The function is
// pure — it does not mutate inputs.
func Classify(resp *http.Response, err error, body []byte) FailureKind {
	if err != nil {
		return classifyError(err)
	}
	if resp == nil {
		return FailureNetwork
	}

	switch {
	case resp.StatusCode == http.StatusUnauthorized,
		resp.StatusCode == http.StatusForbidden:
		return FailureAuth
	case resp.StatusCode == http.StatusTooManyRequests:
		return FailureRateLimit
	case resp.StatusCode >= 500:
		return FailureNetwork
	case resp.StatusCode >= 200 && resp.StatusCode < 300:
		// 2xx with a body that looks like a tracker login page is an auth
		// failure dressed up as 200 (common pattern). Adapter authors who
		// know their tracker's login-page heuristics should re-classify.
		if looksLikeLoginPage(body) {
			return FailureAuth
		}
		return FailureNone
	default:
		// 3xx, 4xx other than 401/403/429 — treat as structural by default.
		return FailureStructural
	}
}

func classifyError(err error) FailureKind {
	// Standard library wrappers surface the underlying nature in a few ways.
	var netErr net.Error
	if errors.As(err, &netErr) {
		return FailureNetwork
	}
	var urlErr *url.Error
	if errors.As(err, &urlErr) {
		return FailureNetwork
	}
	// Default to network — adapters that know better can override after Do.
	return FailureNetwork
}

// looksLikeLoginPage applies a small set of heuristics that catch the
// "tracker returned 200 but with a login form because your session expired"
// case common on private trackers. Adapter authors can replace this with a
// tracker-specific check.
func looksLikeLoginPage(body []byte) bool {
	if len(body) == 0 {
		return false
	}
	lower := strings.ToLower(string(body))
	// Cheap signals; intentionally conservative.
	signals := []string{
		`<form action="/login`,
		`<form action="login`,
		`name="password"`,
		`"Please log in"`,
	}
	hits := 0
	for _, s := range signals {
		if strings.Contains(lower, strings.ToLower(s)) {
			hits++
		}
	}
	return hits >= 2
}

// EventLevel returns the structured-event severity to use when logging this
// classification. Auth and structural are operator-visible; network is
// debug-level unless persistent.
func (k FailureKind) EventLevel() string {
	switch k {
	case FailureAuth, FailureStructural:
		return "error"
	case FailureRateLimit:
		return "warn"
	case FailureNetwork:
		return "info"
	default:
		return "info"
	}
}
