package node

import (
	"fmt"
	"sort"
	"strings"
	"sync"

	"github.com/google/uuid"
	"iop/packages/go/config"
)

// NodeRecord is the pre-registered node definition stored in edge.
// Workspaces is compiled immutably from config at LoadFromConfig time and
// carried through the store. Runtime mutation of workspace definitions is
// restart-required; the store returns deep copies so callers cannot affect
// the stored catalog.
type NodeRecord struct {
	ID         string
	Alias      string
	Token      string
	Index      int
	Adapters   config.AdaptersConf
	Providers  []config.NodeProviderConf
	Runtime    config.RuntimeConf
	Workspaces []config.WorkspaceDefinition
}

// NodeStore holds pre-registered node definitions, keyed by token.
type NodeStore struct {
	mu      sync.RWMutex
	byToken map[string]*NodeRecord
	byID    map[string]*NodeRecord
}

func NewNodeStore() *NodeStore {
	return &NodeStore{
		byToken: make(map[string]*NodeRecord),
		byID:    make(map[string]*NodeRecord),
	}
}

func (s *NodeStore) Add(rec *NodeRecord) {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.byToken[rec.Token] = rec
	s.byID[rec.ID] = rec
}

func (s *NodeStore) FindByToken(token string) (*NodeRecord, bool) {
	s.mu.RLock()
	defer s.mu.RUnlock()
	r, ok := s.byToken[token]
	return r, ok
}

func (s *NodeStore) FindByID(id string) (*NodeRecord, bool) {
	s.mu.RLock()
	defer s.mu.RUnlock()
	r, ok := s.byID[id]
	return r, ok
}

// All returns all configured NodeRecords in deterministic, ascending Index order.
// Callers (e.g. status snapshot builder) rely on this ordering so the surface
// exposed to CLI/HTTP/Control Plane is reproducible across calls.
func (s *NodeStore) All() []*NodeRecord {
	s.mu.RLock()
	defer s.mu.RUnlock()
	out := make([]*NodeRecord, 0, len(s.byID))
	for _, r := range s.byID {
		out = append(out, r)
	}
	sort.Slice(out, func(i, j int) bool {
		return out[i].Index < out[j].Index
	})
	return out
}

// ResolveWorkspace returns the NodeRecord and deep-copied WorkspaceDefinition
// for the given ref, or an error if no matching workspace is found. The
// returned WorkspaceDefinition is a deep copy so callers cannot mutate the
// store's immutable catalog. Exactly one workspace across all nodes must
// match the ref; this is enforced at load time.
func (s *NodeStore) ResolveWorkspace(ref string) (*NodeRecord, config.WorkspaceDefinition, error) {
	s.mu.RLock()
	defer s.mu.RUnlock()
	for _, rec := range s.byID {
		for _, ws := range rec.Workspaces {
			if ws.Ref == ref {
				return cloneWorkspaceOwner(rec), cloneWorkspaceDefinition(ws), nil
			}
		}
	}
	return nil, config.WorkspaceDefinition{}, fmt.Errorf("workspace ref %q not found in any node", ref)
}

// LoadFromConfig seeds the store from EdgeConfig.Nodes.
func LoadFromConfig(defs []config.NodeDefinition) (*NodeStore, error) {
	s := NewNodeStore()
	seenToken := make(map[string]bool)
	seenAlias := make(map[string]bool)
	seenID := make(map[string]bool)
	seenWorkspaceRef := make(map[string]struct{})
	for i, d := range defs {
		if d.Token == "" {
			return nil, fmt.Errorf("node[%d] alias=%q: token must not be empty", i, d.Alias)
		}
		if seenToken[d.Token] {
			return nil, fmt.Errorf("node[%d] alias=%q: duplicate token", i, d.Alias)
		}
		if seenAlias[d.Alias] {
			return nil, fmt.Errorf("node[%d] alias=%q: duplicate alias", i, d.Alias)
		}
		if d.ID != "" && seenID[d.ID] {
			return nil, fmt.Errorf("node[%d] alias=%q: duplicate id %q", i, d.Alias, d.ID)
		}
		seenToken[d.Token] = true
		seenAlias[d.Alias] = true
		nodeID := d.ID
		if nodeID == "" {
			nodeID = uuid.NewString()
		} else {
			seenID[d.ID] = true
		}
		adapters := d.Adapters
		if err := config.NormalizeAdapters(&adapters); err != nil {
			return nil, fmt.Errorf("node[%d] alias=%q: adapters: %w", i, d.Alias, err)
		}
		workspaces, err := cloneWorkspaceCatalog(d.Workspaces, seenWorkspaceRef, i)
		if err != nil {
			return nil, err
		}
		s.Add(&NodeRecord{
			ID:         nodeID,
			Alias:      d.Alias,
			Token:      d.Token,
			Index:      i,
			Adapters:   adapters,
			Providers:  d.Providers,
			Runtime:    d.Runtime,
			Workspaces: workspaces,
		})
	}
	return s, nil
}

func cloneWorkspaceOwner(rec *NodeRecord) *NodeRecord {
	owner := *rec
	owner.Workspaces = cloneWorkspaceCatalogUnchecked(rec.Workspaces)
	return &owner
}

func cloneWorkspaceCatalog(defs []config.WorkspaceDefinition, seenRefs map[string]struct{}, nodeIndex int) ([]config.WorkspaceDefinition, error) {
	if len(defs) == 0 {
		return nil, nil
	}

	workspaces := make([]config.WorkspaceDefinition, len(defs))
	for workspaceIndex, ws := range defs {
		ws.Ref = strings.TrimSpace(ws.Ref)
		if ws.Ref == "" {
			return nil, fmt.Errorf("node[%d].workspaces[%d]: ref must not be empty after trim", nodeIndex, workspaceIndex)
		}
		if _, duplicate := seenRefs[ws.Ref]; duplicate {
			return nil, fmt.Errorf("node[%d].workspaces[%d]: duplicate workspace ref %q", nodeIndex, workspaceIndex, ws.Ref)
		}
		seenRefs[ws.Ref] = struct{}{}
		workspaces[workspaceIndex] = cloneWorkspaceDefinition(ws)
	}
	return workspaces, nil
}

func cloneWorkspaceCatalogUnchecked(defs []config.WorkspaceDefinition) []config.WorkspaceDefinition {
	if len(defs) == 0 {
		return nil
	}
	workspaces := make([]config.WorkspaceDefinition, len(defs))
	for i, ws := range defs {
		workspaces[i] = cloneWorkspaceDefinition(ws)
	}
	return workspaces
}

func cloneWorkspaceDefinition(ws config.WorkspaceDefinition) config.WorkspaceDefinition {
	cp := config.WorkspaceDefinition{
		Ref:                 ws.Ref,
		Platform:            ws.Platform,
		Root:                ws.Root,
		MaxReadBytes:        ws.MaxReadBytes,
		MaxWriteBytes:       ws.MaxWriteBytes,
		MaxOutputBytes:      ws.MaxOutputBytes,
		MaxCommandTimeoutMS: ws.MaxCommandTimeoutMS,
	}
	cp.Operations = append([]config.WorkspaceOperation(nil), ws.Operations...)
	cp.EnvironmentAllowlist = append([]string(nil), ws.EnvironmentAllowlist...)
	cp.Commands = make([]config.WorkspaceCommandDefinition, len(ws.Commands))
	for i, command := range ws.Commands {
		cp.Commands[i] = config.WorkspaceCommandDefinition{
			ID:         command.ID,
			Executable: command.Executable,
			Args:       append([]string(nil), command.Args...),
		}
	}
	return cp
}
