// Package analytics ships product telemetry events to an external analytics
// backend (PostHog). Events feed the acquisition → activation → expansion
// funnel — see docs/analytics.md for the event contract.
//
// Design:
//   - Capture is non-blocking. Request handlers must never wait on analytics
//     network I/O, so we enqueue into a bounded channel and a background
//     worker flushes to PostHog in batches.
//   - When the queue is full events are dropped (and counted). A broken
//     analytics backend must never degrade the product.
//   - When POSTHOG_API_KEY is empty the package runs a no-op client, which
//     keeps local dev and self-hosted instances friction-free.
package analytics

import (
	"log/slog"
	"os"
	"strings"
	"time"
)

// Event is a single analytics capture. Fields mirror PostHog's /capture/ shape
// but are framework-agnostic so alternate backends can plug in later.
type Event struct {
	// Name of the event (e.g. "signup", "workspace_created").
	Name string

	// DistinctID identifies the person this event belongs to. For logged-in
	// users this is user.id; for anonymous events it should be the anon_id
	// that was previously used on the frontend so identity merging works.
	DistinctID string

	// WorkspaceID scopes the event to a workspace. Required when the event is
	// about a workspace-level action (workspace_created, issue_executed, ...).
	// Empty is allowed for pre-workspace events (signup).
	WorkspaceID string

	// Properties is the free-form bag of event attributes. Only serialisable
	// values (string, number, bool, nested maps/slices of the same) should
	// go here. Never put raw PII like full emails here — use email_domain.
	Properties map[string]any

	// SetOnce properties attach to the person record and are only written the
	// first time they appear. Use this for acquisition attribution
	// (initial_utm_source, etc.) so later events don't overwrite the origin.
	SetOnce map[string]any

	// Set properties attach to the person record and overwrite on every write.
	// Use this for mutable cohort signals (role, use_case, platform_preference)
	// that users can legitimately change during onboarding.
	Set map[string]any

	// Timestamp is optional; when zero the client fills in time.Now().
	Timestamp time.Time
}

// Client is the narrow surface the rest of the codebase depends on. Handlers
// call Capture and move on; the implementation is responsible for buffering,
// batching, and shipping.
type Client interface {
	Capture(e Event)
	// Close drains pending events. Call once during graceful shutdown.
	Close()
}

// NewFromEnv returns a Client configured from environment variables:
//
//   - POSTHOG_API_KEY: project API key. Empty → no-op client.
//   - POSTHOG_HOST:    API host (default https://us.i.posthog.com).
//   - ANALYTICS_ENVIRONMENT: production/staging/dev. Defaults from APP_ENV.
//   - ANALYTICS_DISABLED: set to "true"/"1" to force a no-op client even
//     when POSTHOG_API_KEY is set (useful for CI and self-hosted opt-out).
func NewFromEnv() Client {
	if isDisabled() {
		slog.Info("analytics disabled via ANALYTICS_DISABLED")
		return NoopClient{}
	}
	key := os.Getenv("POSTHOG_API_KEY")
	if key == "" {
		slog.Info("analytics: POSTHOG_API_KEY not set, using noop client")
		return NoopClient{}
	}
	host := os.Getenv("POSTHOG_HOST")
	if host == "" {
		host = "https://us.i.posthog.com"
	}
	slog.Info("analytics: posthog client enabled", "host", host)
	return NewPostHogClient(PostHogConfig{
		APIKey:      key,
		Host:        host,
		Environment: EnvironmentFromEnv(),
	})
}

func isDisabled() bool {
	v := os.Getenv("ANALYTICS_DISABLED")
	return v == "true" || v == "1"
}

func EnvironmentFromEnv() string {
	if v := normalizeEnvironment(os.Getenv("ANALYTICS_ENVIRONMENT")); v != "" {
		return v
	}
	if v := normalizeEnvironment(os.Getenv("APP_ENV")); v != "" {
		return v
	}
	return "dev"
}

func normalizeEnvironment(v string) string {
	switch strings.ToLower(strings.TrimSpace(v)) {
	case "production", "prod":
		return "production"
	case "staging", "stage":
		return "staging"
	case "development", "dev", "test", "local":
		return "dev"
	default:
		return ""
	}
}

// NoopClient silently drops all events. Used in tests, in local dev when
// POSTHOG_API_KEY is unset, and in self-hosted instances that opt out.
type NoopClient struct{}

func (NoopClient) Capture(Event) {}
func (NoopClient) Close()        {}
