package service

import (
	"fmt"

	edgenode "iop/apps/edge/internal/node"
	"iop/packages/go/config"
	iop "iop/proto/gen/iop"
)

// NodeSnapshot is a surface-neutral view of a registered node, suitable for
// CLI, HTTP, or other transports. Connected reports whether the node is
// currently attached to the live registry; configured records that have never
// connected (or whose connection has dropped) still appear with Connected=false
// so the snapshot source of truth is the configured NodeStore, not the live
// registry.
// Label is the short display label (node0, node1). LifecycleState carries the
// registry entry lifecycle when present; for configured-only records it is empty.
type NodeSnapshot struct {
	NodeID            string
	Alias             string
	Label             string
	LifecycleState    string
	Connected         bool
	Config            *iop.NodeConfigPayload
	ProviderSnapshots []*iop.ProviderSnapshot
}

// ListNodeSnapshots returns surface-neutral node descriptors derived from the
// current registry contents. For nodes backed by a queue manager, the entire
// snapshot is built inside a single queue-held critical section so that
// runtime config, queue state, and candidate pressure are observed atomically
// under the same lock order (queue → service) that SetRuntimeConfig follows.
// Nodes without a queue fall back to the catalog-only path under s.mu.
func (s *Service) ListNodeSnapshots() []NodeSnapshot {
	if s.queue == nil {
		return s.buildSnapshotNoQueue()
	}
	// Queue-backed: acquire the queue lock for the entire snapshot build so
	// the runtime config, resource state, and candidate pressure are captured
	// as a single consistent old-or-new tuple (no torn mixed state).
	s.queue.mu.Lock()
	defer s.queue.mu.Unlock()
	return s.buildSnapshotLocked()
}

// buildSnapshotNoQueue builds node snapshots using the configured NodeStore
// catalog as the identity source, joined against the current Registry to
// determine live connectivity. Nodes that are configured but not currently
// connected still appear with Connected=false so the snapshot never silently
// drops offline records. When no node store is configured the builder falls
// back to the live registry so callers without a store still receive snapshots.
func (s *Service) buildSnapshotNoQueue() []NodeSnapshot {
	store, _, _ := s.runtimeConfigSnapshot()
	if store == nil {
		return s.buildSnapshotFromRegistry()
	}
	records := store.All()
	out := make([]NodeSnapshot, 0, len(records))
	for _, rec := range records {
		snap := s.emptySnapshotForConfigured(rec)
		if r, ok := store.FindByID(rec.ID); ok {
			if payload, err := edgenode.BuildConfigPayload(r); err == nil {
				snap.Config = payload
			}
			if len(r.Providers) > 0 {
				snap.ProviderSnapshots = staticProviderCatalogSnapshots(r, snap.Connected)
			}
		}
		out = append(out, snap)
	}
	return out
}

// buildSnapshotLocked builds node snapshots under the assumption that the
// caller already holds s.queue.mu. ListNodeSnapshots uses this variant to keep
// runtime config and queue state inside one queue-held critical section. The
// identity source is the configured NodeStore catalog joined with current
// Registry connectivity; the ordering is deterministic across calls. When no
// node store is configured the builder falls back to the live registry so
// callers without a store still receive snapshots.
func (s *Service) buildSnapshotLocked() []NodeSnapshot {
	store, _, _ := s.runtimeConfigSnapshot()
	if store == nil {
		return s.buildSnapshotFromRegistry()
	}
	records := store.All()
	out := make([]NodeSnapshot, 0, len(records))
	for _, rec := range records {
		snap := s.emptySnapshotForConfigured(rec)
		if r, ok := store.FindByID(rec.ID); ok {
			if payload, err := edgenode.BuildConfigPayload(r); err == nil {
				snap.Config = payload
			}
			snap.ProviderSnapshots = s.queue.getSnapshotForNodeLocked(rec.ID, r, snap.Connected)
		}
		out = append(out, snap)
	}
	return out
}

// buildSnapshotFromRegistry is the legacy catalog path used when no node store
// is configured. It preserves snapshot output for callers that build a Service
// without SetNodeStore.
func (s *Service) buildSnapshotFromRegistry() []NodeSnapshot {
	entries := s.registry.AllReady()
	out := make([]NodeSnapshot, 0, len(entries))
	for _, entry := range entries {
		out = append(out, s.emptySnapshot(entry))
	}
	return out
}

// emptySnapshotForConfigured builds a NodeSnapshot from a configured NodeRecord.
// If registry has a live entry for the same NodeID it is merged in so Alias,
// Label and LifecycleState reflect the current connection; otherwise
// the snapshot is labeled from the configured ID/index and Connected is false.
func (s *Service) emptySnapshotForConfigured(rec *edgenode.NodeRecord) NodeSnapshot {
	snap := NodeSnapshot{
		NodeID:    rec.ID,
		Alias:     rec.Alias,
		Connected: false,
	}
	// GetReady, not Get: a pending accepted connection has not signalled dispatch
	// readiness, so it is reported offline until its node applies config and
	// installs its handler. This keeps "connected" aligned with "dispatchable".
	if entry, ok := s.registry.GetReady(rec.ID); ok {
		snap.Connected = true
		snap.Label = entry.DisplayLabel()
		snap.LifecycleState = entry.LifecycleState
	} else {
		snap.Label = configuredLabel(rec)
	}
	return snap
}

// configuredLabel builds a display label from a configured NodeRecord when no
// live registry entry is present. Uses index if available, otherwise alias,
// otherwise node ID.
func configuredLabel(rec *edgenode.NodeRecord) string {
	if rec.Index >= 0 {
		return fmt.Sprintf("node%d", rec.Index)
	}
	if rec.Alias != "" {
		return rec.Alias
	}
	return rec.ID
}

// emptySnapshot returns a NodeSnapshot populated from the registry entry. Used
// for registry-only (non-catalog) lookups such as ResolveNodeSnapshot.
func (s *Service) emptySnapshot(entry *edgenode.NodeEntry) NodeSnapshot {
	return NodeSnapshot{
		NodeID:         entry.NodeID,
		Alias:          entry.Alias,
		Label:          entry.DisplayLabel(),
		LifecycleState: entry.LifecycleState,
		Connected:      true,
	}
}

// NodeEntrySnapshot converts a registry entry into the surface-neutral DTO.
func NodeEntrySnapshot(entry *edgenode.NodeEntry) NodeSnapshot {
	if entry == nil {
		return NodeSnapshot{}
	}
	return NodeSnapshot{
		NodeID:         entry.NodeID,
		Alias:          entry.Alias,
		Label:          entry.DisplayLabel(),
		LifecycleState: entry.LifecycleState,
		Connected:      true,
	}
}

// staticProviderCatalogSnapshots builds catalog-based ProviderSnapshots for
// nodes whose service has no queue. The catalog entry (id/adapter/type/category/models)
// is always preserved so operators can see configured providers. When connected
// is false, effective status/health/capacity/longContext counters are dropped
// to unavailable/offline/0 so the snapshot reflects disconnect without removing
// the configured provider from the list. disabled providers keep status/health=disabled
// regardless of connectivity.
func staticProviderCatalogSnapshots(rec *edgenode.NodeRecord, connected bool) []*iop.ProviderSnapshot {
	snaps := make([]*iop.ProviderSnapshot, 0, len(rec.Providers))
	for _, prov := range rec.Providers {
		if prov.ID == "" {
			continue
		}
		servedModels := make([]string, len(prov.Models))
		copy(servedModels, prov.Models)
		lifecycleCaps := make([]string, len(prov.LifecycleCapabilities))
		copy(lifecycleCaps, prov.LifecycleCapabilities)

		if !config.ProviderEnabled(prov) {
			snaps = append(snaps, &iop.ProviderSnapshot{
				Adapter:               prov.Adapter,
				Status:                "disabled",
				Capacity:              0,
				Id:                    prov.ID,
				Type:                  prov.Type,
				Category:              string(prov.Category),
				ServedModels:          servedModels,
				Health:                "disabled",
				LifecycleCapabilities: lifecycleCaps,
				LongContextCapacity:   0,
				LongInFlight:          0,
				LongQueued:            0,
			})
			continue
		}

		snaps = append(snaps, &iop.ProviderSnapshot{
			Adapter:               prov.Adapter,
			Status:                effectiveStatus(connected),
			Health:                effectiveHealth(connected, prov.Health),
			Capacity:              int32(effectiveCount(connected, prov.Capacity)),
			Id:                    prov.ID,
			Type:                  prov.Type,
			Category:              string(prov.Category),
			ServedModels:          servedModels,
			LifecycleCapabilities: lifecycleCaps,
			LongContextCapacity:   int32(effectiveCount(connected, prov.LongContextCapacity)),
			LongInFlight:          0,
			LongQueued:            0,
		})
	}
	return snaps
}

func (s *Service) ResolveNode(ref string) (*edgenode.NodeEntry, error) {
	return s.registry.Resolve(ref)
}

// ResolveDispatchReady resolves ref exactly like ResolveNode but only ever returns a
// dispatch-ready entry.
func (s *Service) ResolveDispatchReady(ref string) (*edgenode.NodeEntry, error) {
	return s.registry.ResolveReady(ref)
}

// ResolveNodeSnapshot returns a surface-neutral DTO for the resolved node.
func (s *Service) ResolveNodeSnapshot(ref string) (NodeSnapshot, error) {
	entry, err := s.ResolveNode(ref)
	if err != nil {
		return NodeSnapshot{}, err
	}
	return NodeEntrySnapshot(entry), nil
}

func (s *Service) GetCapabilities() []*iop.EdgeCapabilitySummary {
	var entries []*edgenode.NodeEntry
	if s.registry != nil {
		entries = s.registry.AllReady()
	}
	if len(entries) == 0 {
		return nil
	}
	return []*iop.EdgeCapabilitySummary{
		{
			Kind:      "run",
			Available: true,
			Status:    "ready",
			Summary:   "Provider execution node available",
		},
	}
}

// HealthStatusView is the Edge-owned status summary used by control commands
// such as health.check. It is derived from the live node registry and
// capability snapshot rather than a fixed simulation delay.
type HealthStatusView struct {
	Nodes        []NodeSnapshot
	Capabilities []*iop.EdgeCapabilitySummary
}

// HealthStatus returns a status summary derived from the current node registry
// and capability snapshot. It is the real Edge-owned boundary that backs the
// health.check control command.
func (s *Service) HealthStatus() HealthStatusView {
	return HealthStatusView{
		Nodes:        s.ListNodeSnapshots(),
		Capabilities: s.GetCapabilities(),
	}
}
