package plugin

import (
	"fmt"
	"sort"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/seaweedfs/seaweedfs/weed/pb/plugin_pb"
)

// maxObservations caps the store. Observations are a convenience for display,
// so a cluster with more objects than this loses the oldest rather than growing
// admin's memory without limit.
const maxObservations = 10000

// observationKey identifies one observed object across workers.
func observationKey(objectID []string) string {
	return strings.Join(objectID, "\x1f")
}

// ObservationStore keeps the last thing a worker said about each object.
//
// It is deliberately not authoritative: a worker reports what it saw when it
// last looked, and admin serves that back with its timestamp so a reader can
// judge how stale it is. Nothing schedules work from it.
type ObservationStore struct {
	mu      sync.RWMutex
	entries map[string]*Observation
}

// Observation is one object as a worker last reported it.
type Observation struct {
	ObjectID   []string `json:"object_id"`
	ObjectKind string   `json:"object_kind"`
	Format     string   `json:"format"`
	// Attributes are flattened on the way in: they exist to be displayed and
	// served as JSON, and the typed form is the worker's business.
	Attributes map[string]interface{} `json:"attributes"`
	JobType    string                 `json:"job_type"`
	WorkerID   string                 `json:"worker_id"`
	ObservedAt time.Time              `json:"observed_at"`
}

func NewObservationStore() *ObservationStore {
	return &ObservationStore{entries: make(map[string]*Observation)}
}

// Record stores what one worker reported, replacing whatever was there for the
// same object. A later observation from a different worker still wins: the
// newest look at an object is the useful one.
func (s *ObservationStore) Record(workerID string, report *plugin_pb.WorkerObservations) {
	if report == nil {
		return
	}
	s.mu.Lock()
	defer s.mu.Unlock()

	for _, observed := range report.Observations {
		if observed == nil || len(observed.ObjectId) == 0 {
			continue
		}
		at := time.Now()
		if observed.ObservedAt != nil {
			at = observed.ObservedAt.AsTime()
		}
		s.entries[observationKey(observed.ObjectId)] = &Observation{
			ObjectID:   observed.ObjectId,
			ObjectKind: observed.ObjectKind,
			Format:     observed.Format,
			Attributes: flattenAttributes(observed.Attributes),
			JobType:    report.JobType,
			WorkerID:   workerID,
			ObservedAt: at,
		}
	}
	s.evictOldest()
}

// evictOldest trims the store back under the cap. Called with the lock held.
func (s *ObservationStore) evictOldest() {
	if len(s.entries) <= maxObservations {
		return
	}
	keys := make([]string, 0, len(s.entries))
	for key := range s.entries {
		keys = append(keys, key)
	}
	sort.Slice(keys, func(i, j int) bool {
		return s.entries[keys[i]].ObservedAt.Before(s.entries[keys[j]].ObservedAt)
	})
	for _, key := range keys[:len(s.entries)-maxObservations] {
		delete(s.entries, key)
	}
}

// flattenAttributes unwraps the ConfigValue envelope that configValueMapToPlain
// preserves. An observation exists to be read, and {"int64_value":"16"} is not
// a number anyone wants to render; a protojson int64 also arrives as a string,
// so it is converted back.
func flattenAttributes(values map[string]*plugin_pb.ConfigValue) map[string]interface{} {
	plain := configValueMapToPlain(values)
	if plain == nil {
		return nil
	}
	flat := make(map[string]interface{}, len(plain))
	for name, value := range plain {
		wrapper, ok := value.(map[string]interface{})
		if !ok || len(wrapper) != 1 {
			flat[name] = value
			continue
		}
		for kind, inner := range wrapper {
			if kind == "int64_value" {
				if text, ok := inner.(string); ok {
					if parsed, err := strconv.ParseInt(text, 10, 64); err == nil {
						flat[name] = parsed
						continue
					}
				}
			}
			flat[name] = inner
		}
	}
	return flat
}

// Get returns the last observation of one object, if any.
func (s *ObservationStore) Get(objectID []string) (*Observation, bool) {
	s.mu.RLock()
	defer s.mu.RUnlock()
	observed, ok := s.entries[observationKey(objectID)]
	return observed, ok
}

// GetFormat returns the last observation of one object only when it describes
// the format asked for. A table can be dropped and remade in another format at
// the same path, and the stale observation would then describe something that
// no longer exists.
func (s *ObservationStore) GetFormat(objectID []string, format string) (*Observation, bool) {
	observed, ok := s.Get(objectID)
	if !ok || !strings.EqualFold(observed.Format, format) {
		return nil, false
	}
	return observed, true
}

// List returns every observation, newest first.
func (s *ObservationStore) List() []*Observation {
	s.mu.RLock()
	defer s.mu.RUnlock()
	all := make([]*Observation, 0, len(s.entries))
	for _, observed := range s.entries {
		all = append(all, observed)
	}
	sort.Slice(all, func(i, j int) bool {
		return all[i].ObservedAt.After(all[j].ObservedAt)
	})
	return all
}

// AttributeString renders one attribute for display, or "" when it is absent.
func (o *Observation) AttributeString(name string) string {
	if o == nil {
		return ""
	}
	value, ok := o.Attributes[name]
	if !ok || value == nil {
		return ""
	}
	return fmt.Sprintf("%v", value)
}

// Observations exposes the store so admin handlers can serve it.
func (r *Plugin) Observations() *ObservationStore {
	return r.observations
}
