package pii

import (
	"context"
	"crypto/rand"
	"encoding/hex"
	"net/http"
	"time"

	"github.com/labstack/echo/v4"
	"github.com/mudler/LocalAI/core/http/auth"
	"github.com/mudler/LocalAI/core/services/routing/contract"
	"github.com/mudler/xlog"
)

// Echo context keys this middleware reads from / writes to. The string
// values must match the constants in core/http/middleware/context_keys.go;
// kept in sync by hand because echoing constants across packages would
// drag the http/middleware package into pii's import graph and create
// a cycle (http/middleware will import this one).
const (
	ctxKeyCorrelationID = "routing.correlation_id"
	ctxKeyPIIEventID    = "routing.pii_event_id"
	// Must match the constants in core/http/middleware/request.go.
	// Echoing them across packages would create an import cycle
	// (http/middleware imports this package). Drift is caught by
	// integration tests against the chat route.
	ctxKeyParsedRequest = "LOCALAI_REQUEST"
	ctxKeyModelConfig   = "MODEL_CONFIG"
)

// ModelPIIConfig is the duck-typed view this middleware needs of the
// per-model PII configuration carried on the echo context.
// *config.ModelConfig satisfies it via PIIIsEnabled / PIIDetectors; the
// indirection keeps the pii package from importing core/config.
//
// PIIDetectors lists the token-classification models whose detections
// drive redaction for this (consuming) model. The detection policy lives
// on each named detector model — resolved via NERDetectorResolver — so
// this consuming view carries no per-entity actions of its own.
type ModelPIIConfig interface {
	PIIIsEnabled() bool
	PIIDetectors() []string
}

// NERDetectorResolver resolves a detector model name to a ready-to-use
// NERConfig — the detector plus the policy (min score, entity→action
// map, default action) read from that model's own pii_detection block.
// ok is false when the name can't supply a detector (unknown model, not
// a token_classify model, or load failure); the middleware fails closed
// in that case. Supplied by the application layer, which owns the model
// loader and the core/backend dependency, keeping the pii package free of
// both. A nil resolver (or the option being unset) disables the NER tier.
type NERDetectorResolver func(modelName string) (NERConfig, bool)

// Option configures optional RequestMiddleware behaviour. Threaded as
// variadic options so adding the NER tier doesn't break the existing
// four-argument call sites (routes and tests).
type Option func(*mwOptions)

type mwOptions struct {
	nerResolver    NERDetectorResolver
	policyResolver PolicyResolver
}

// PolicyResolver returns the effective (enabled, detectors) for the model
// carried on the request context, layering instance-wide PII defaults over the
// per-model config. Supplied by the application layer (which owns core/config),
// keeping this package decoupled from it — the middleware passes the raw
// context value through as `any`. When unset, the middleware falls back to the
// duck-typed ModelPIIConfig (explicit per-model config only, no global default).
type PolicyResolver func(modelCfg any) (enabled bool, detectors []string)

// WithPolicyResolver overrides how the middleware decides enablement and the
// detector list, so the instance-wide default detector / default-on usecases
// apply. Without it the middleware reads ModelPIIConfig off the context.
func WithPolicyResolver(r PolicyResolver) Option {
	return func(o *mwOptions) { o.policyResolver = r }
}

// WithNERResolver enables the NER tier. When a request's model lists
// pii.detectors, the middleware resolves each to a NERConfig and runs
// RedactNER (the union of all detectors' hits, merged). Without this
// option, or when a model lists no detectors, redaction is a no-op.
func WithNERResolver(r NERDetectorResolver) Option {
	return func(o *mwOptions) { o.nerResolver = r }
}

// ScannedText is one piece of user text from the request. Index is
// opaque to the middleware — the Adapter implementation uses it to
// put the redacted version back in the right place.
type ScannedText struct {
	Index int
	Text  string
}

// Adapter pulls scannable text out of a parsed request and writes
// redacted text back. Provided as a per-API-shape function rather
// than an interface on the request type so the schema package does
// not have to depend on pii. Each route registration passes the
// adapter that knows its request format.
//
// The middleware calls Scan once per request and Apply once with
// every span the redactor returned. updates are guaranteed to share
// indices the adapter previously returned from Scan; the adapter
// must not assume input order matches scan order.
type Adapter struct {
	Scan  func(parsed any) []ScannedText
	Apply func(parsed any, updates []ScannedText)
}

// RequestMiddleware applies the regex PII tier to incoming chat
// requests. If the parsed request is not a MessageScanner (e.g.,
// non-chat endpoints registered against the same group later), the
// middleware passes through.
//
//   - On match with action=block: the request is rejected with 400 and
//     a PIIEvent is recorded. The matched value is never echoed back
//     to the client.
//   - On match with action=mask: the redacted text replaces the
//     original on the parsed request. PIIEvents are recorded.
//   - On match with action=allow: the original text is left intact; a
//     PIIEvent is still recorded so the detection is auditable.
//
// recorder is the Recorder on which to record events; nil disables
// recording (the redaction still happens). fallbackUser supplies the
// no-auth identity. The middleware writes ctxKeyPIIEventID on the echo
// context so the usage middleware can later cross-reference the event
// with the UsageRecord.
func RequestMiddleware(redactor *Redactor, store EventStore, adapter Adapter, fallbackUser *auth.User, opts ...Option) echo.MiddlewareFunc {
	var o mwOptions
	for _, opt := range opts {
		opt(&o)
	}
	return func(next echo.HandlerFunc) echo.HandlerFunc {
		return func(c echo.Context) error {
			if redactor == nil || adapter.Scan == nil {
				return next(c)
			}

			// Per-model gating: redaction is opt-in per model. The policy
			// resolver (when wired) layers instance-wide defaults over the
			// per-model config; otherwise we read the per-model config
			// directly. A missing config (non-chat routes, or middleware
			// wired before SetModelAndConfig) or a not-enabled result passes
			// through.
			rawCfg := c.Get(ctxKeyModelConfig)
			var enabled bool
			var detectors []string
			if o.policyResolver != nil {
				enabled, detectors = o.policyResolver(rawCfg)
			} else if cfg, ok := rawCfg.(ModelPIIConfig); ok {
				enabled, detectors = cfg.PIIIsEnabled(), cfg.PIIDetectors()
			}
			if !enabled {
				return next(c)
			}

			parsed := c.Get(ctxKeyParsedRequest)
			if parsed == nil {
				return next(c)
			}

			// A PII-enabled model with no detectors (or no resolver wired)
			// has nothing to scan with — pass through.
			if len(detectors) == 0 || o.nerResolver == nil {
				return next(c)
			}

			user := auth.GetUser(c)
			if user == nil {
				user = fallbackUser
			}
			userID := ""
			if user != nil {
				userID = user.ID
			}
			correlationID, _ := c.Get(ctxKeyCorrelationID).(string)

			// Resolve each named detector to its NERConfig (detector +
			// the policy from that model's own pii_detection block). A
			// configured detector that can't be resolved fails closed:
			// serving the request without the semantic check the operator
			// asked for is exactly the leak this tier exists to prevent.
			cfgs := make([]NERConfig, 0, len(detectors))
			for _, name := range detectors {
				nc, ok := o.nerResolver(name)
				if !ok {
					xlog.Error("pii: configured detector model could not be resolved; blocking request (fail-closed)", "detector", name)
					return blockNERUnavailable(c, store, correlationID, userID)
				}
				cfgs = append(cfgs, nc)
			}

			texts := adapter.Scan(parsed)
			updates := make([]ScannedText, 0, len(texts))
			var blocked bool
			var firstEventID string

			// Scan the request as ONE document (messages joined) so the NER
			// tier keeps conversational context — whether "4421" is a PIN is
			// decided by the question in the previous message. The spans come
			// back per message with local offsets for in-place rewriting.
			segTexts := make([]string, len(texts))
			for i, st := range texts {
				segTexts[i] = st.Text
			}
			// Fail closed: a detector outage at request time must NOT
			// silently serve the request. The NER tier was explicitly
			// configured for this model, so the semantic check is part
			// of the contract.
			segResults, nerErr := RedactNERSegments(c.Request().Context(), segTexts, cfgs)
			if nerErr != nil {
				xlog.Error("pii: NER detector failed; blocking request (fail-closed)", "error", nerErr)
				return blockNERUnavailable(c, store, correlationID, userID)
			}

			for i, res := range segResults {
				st := texts[i]
				if len(res.Spans) == 0 {
					continue
				}

				// Persist one event per detected span. The action recorded
				// is the one that actually fired (carried on the span after
				// the overlap merge), so the events log reflects what
				// happened to the request.
				for _, span := range res.Spans {
					ev := PIIEvent{
						ID:            newEventID(),
						Origin:        OriginMiddleware,
						CorrelationID: correlationID,
						UserID:        userID,
						Direction:     DirectionIn,
						PatternID:     span.Pattern,
						ByteOffset:    span.Start,
						Length:        span.End - span.Start,
						HashPrefix:    span.HashPrefix,
						Action:        span.Action,
						Score:         span.Score,
						CreatedAt:     time.Now().UTC(),
					}
					if firstEventID == "" {
						firstEventID = ev.ID
					}
					if store != nil {
						if err := store.Record(context.Background(), ev); err != nil {
							xlog.Error("pii: failed to record event", "error", err, "pattern", span.Pattern)
						}
					}
					// Contract: every span must produce an event.
					contract.Invariant(
						"pii.event_per_span",
						span.Pattern != "" && ev.PatternID != "",
						"correlation", correlationID, "pattern", span.Pattern,
					)
				}

				if res.Blocked {
					blocked = true
				}
				updates = append(updates, ScannedText{Index: st.Index, Text: res.Redacted})
			}

			if blocked {
				return c.JSON(http.StatusBadRequest, map[string]any{
					"error": map[string]string{
						"message": "request blocked by content policy (sensitive data detected)",
						"type":    "pii_blocked",
					},
					"correlation_id": correlationID,
					"pii_event_id":   firstEventID,
				})
			}

			if len(updates) > 0 && adapter.Apply != nil {
				adapter.Apply(parsed, updates)
			}
			if firstEventID != "" {
				c.Set(ctxKeyPIIEventID, firstEventID)
			}
			return next(c)
		}
	}
}

// nerUnavailablePattern is the sentinel PatternID recorded on the
// fail-closed audit event when a model's configured NER tier cannot
// run. It is not a real regex pattern — it marks a request blocked
// because the encoder/NER check was unavailable (model unresolved or
// backend error), so the events log distinguishes it from a content
// block (which carries a real pattern ID).
const nerUnavailablePattern = "__ner_unavailable__"

// blockNERUnavailable records a fail-closed audit event and returns the
// response used when a model has an NER tier configured but it could
// not run. Failing closed is deliberate for a PII filter: if the
// semantic check the operator asked for cannot execute, refusing the
// request is safer than serving it with only the cheap regex tier. The
// 503 (vs the 400 used for a content block) tells clients and operators
// this was a dependency outage, not sensitive data in the request.
func blockNERUnavailable(c echo.Context, store EventStore, correlationID, userID string) error {
	ev := PIIEvent{
		ID:            newEventID(),
		Kind:          KindPII,
		Origin:        OriginMiddleware,
		CorrelationID: correlationID,
		UserID:        userID,
		Direction:     DirectionIn,
		PatternID:     nerUnavailablePattern,
		Action:        ActionBlock,
		CreatedAt:     time.Now().UTC(),
	}
	if store != nil {
		if err := store.Record(context.Background(), ev); err != nil {
			xlog.Error("pii: failed to record NER-unavailable event", "error", err)
		}
	}
	c.Set(ctxKeyPIIEventID, ev.ID)
	return c.JSON(http.StatusServiceUnavailable, map[string]any{
		"error": map[string]string{
			"message": "request blocked: PII NER check is configured but unavailable",
			"type":    "pii_ner_unavailable",
		},
		"correlation_id": correlationID,
		"pii_event_id":   ev.ID,
	})
}

// validAction converts a raw YAML action string to the typed Action,
// returning "" for anything that isn't a known action.
func validAction(raw string) Action {
	switch Action(raw) {
	case ActionMask, ActionBlock, ActionAllow:
		return Action(raw)
	default:
		return ""
	}
}

// validActionOr is validAction with a fallback for empty/invalid input.
func validActionOr(raw string, fallback Action) Action {
	if a := validAction(raw); a != "" {
		return a
	}
	return fallback
}

// validActions converts a raw entity-group->action map to typed
// Actions, dropping (and logging) unknown actions so a model YAML typo
// is ignored rather than taking the request down — mirroring how the
// per-pattern overrides are validated above.
func validActions(raw map[string]string) map[string]Action {
	if len(raw) == 0 {
		return nil
	}
	out := make(map[string]Action, len(raw))
	for group, action := range raw {
		if a := validAction(action); a != "" {
			out[group] = a
		} else {
			xlog.Warn("pii: ignoring unknown NER entity action", "group", group, "action", action)
		}
	}
	return out
}

func newEventID() string {
	var b [12]byte
	_, _ = rand.Read(b[:])
	return "pii_" + hex.EncodeToString(b[:])
}

// NewEventID mints a fresh random event id in the package's standard shape.
// Exported so callers outside this package (the analyze/redact API handlers)
// record events with ids indistinguishable from the in-band middleware's.
func NewEventID() string { return newEventID() }
