package service

import (
	"axolotl/models"
	"axolotl/store"
	"fmt"
	"maps"
	"regexp"
	"slices"
	"strings"
	"time"
)

type nodeServiceImpl struct {
	store  store.GraphStore
	userID string
}

var mentionRegex = regexp.MustCompile(`@([a-z0-9_]+)`)

// parseDueDate converts a user-supplied date string to *time.Time.
// Returns nil, nil for an empty string (clear/unset). Accepts "YYYY-MM-DD" or RFC3339.
func parseDueDate(s string) (*time.Time, error) {
	if s == "" {
		return nil, nil
	}
	for _, layout := range []string{"2006-01-02", time.RFC3339} {
		if t, err := time.Parse(layout, s); err == nil {
			ut := t.UTC()
			return &ut, nil
		}
	}
	return nil, fmt.Errorf("invalid due date %q: expected YYYY-MM-DD", s)
}

func mentions(t string) []string {
	seen := make(map[string]bool)
	for _, m := range mentionRegex.FindAllStringSubmatch(t, -1) {
		seen[m[1]] = true
	}
	return slices.Collect(maps.Keys(seen))
}

func (s *nodeServiceImpl) User() string { return s.userID }

// --- Permission model ---
//
// Four levels (inclusive: higher includes lower):
//   1 can_read      – visible in list/show
//   2 can_create_rel – can create non-permission relations between nodes
//   3 can_write     – can update/delete a node
//   4 has_ownership – sole owner; deletion cascades to owned nodes
//
// Permissions are transitive: if A has level L on B, and B has level M on C,
// then A has level min(L, M) on C. Computed by BFS from the user's own node.
// Users have self-ownership (has_ownership → self), so BFS starts at level 4.
//
// Rules for adding edge rels in Add/Update:
//   Non-perm rel A → B : need can_create_rel on A, can_read on B
//   Perm rel A --perm_P→ B : need perm_P on B (resource owner grants to any subject)
//   Ownership  A --has_ownership→ B : need has_ownership on B + can_create_rel on A
//     → also removes existing ownership rels pointing to B
//
// Field/tag changes and rel removals require can_write on the node.

const (
	permRead      = 1
	permCreateRel = 2
	permWrite     = 3
	permOwnership = 4
)

// isReferenceRel returns true for rels that point to "identity" nodes (users, namespaces).
// For these rels, the target only needs can_read (not can_create_rel), because users and
// namespaces are globally readable and any node can reference them.
func isReferenceRel(t models.RelType) bool {
	switch t {
	case models.RelAssignee, models.RelCreated, models.RelMentions:
		return true
	}
	return false
}

// permRelLevels maps permission rel types to their numeric level.
var permRelLevels = map[models.RelType]int{
	models.RelCanRead:      permRead,
	models.RelCanCreateRel: permCreateRel,
	models.RelCanWrite:     permWrite,
	models.RelHasOwnership: permOwnership,
}

type permContext struct {
	levels map[string]int
}

func (pc *permContext) level(nodeID string) int         { return pc.levels[nodeID] }
func (pc *permContext) canRead(nodeID string) bool      { return pc.levels[nodeID] >= permRead }
func (pc *permContext) canCreateRel(nodeID string) bool { return pc.levels[nodeID] >= permCreateRel }
func (pc *permContext) canWrite(nodeID string) bool     { return pc.levels[nodeID] >= permWrite }
func (pc *permContext) hasOwnership(nodeID string) bool { return pc.levels[nodeID] >= permOwnership }

// getPermContext builds a permContext by BFS from the current user's node,
// following permission rels and taking the minimum level along each path.
// User and namespace nodes are made globally readable after the BFS.
// If the user node doesn't exist yet, returns an empty permContext (no access);
// Add operations still work because unresolved targets skip the permission check.
func (s *nodeServiceImpl) getPermContext() (*permContext, error) {
	userNodeID, err := s.resolveIDByNameAndType(s.store, s.userID, "user")
	if err != nil {
		return nil, err
	}
	pc := &permContext{levels: make(map[string]int)}
	if userNodeID == "" {
		return pc, nil // user not bootstrapped yet; Add will auto-create user node
	}

	type entry struct {
		nodeID string
		level  int
	}
	// Start at the user's own node at ownership level (users have self-ownership).
	queue := []entry{{userNodeID, permOwnership}}
	for len(queue) > 0 {
		curr := queue[0]
		queue = queue[1:]
		if pc.levels[curr.nodeID] >= curr.level {
			continue // already reached at a higher or equal level
		}
		pc.levels[curr.nodeID] = curr.level
		node, err := s.store.GetNode(curr.nodeID)
		if err != nil {
			continue // node may have been deleted; skip
		}
		for relType, pLevel := range permRelLevels {
			for _, tgt := range node.Relations[string(relType)] {
				eff := min(curr.level, pLevel)
				if eff > pc.levels[tgt] {
					queue = append(queue, entry{tgt, eff})
				}
			}
		}
	}

	// User and namespace nodes are globally readable (they represent identities,
	// and anyone can reference or assign to them).
	for _, nodeType := range []string{"user", "namespace"} {
		nodes, _ := s.store.FindNodes([]*models.Rel{{Type: models.RelType("_type::" + nodeType), Target: ""}})
		for _, n := range nodes {
			if pc.levels[n.ID] < permRead {
				pc.levels[n.ID] = permRead
			}
		}
	}

	return pc, nil
}

// --- Validation ---

var (
	validTypes    = map[string]bool{"issue": true, "note": true, "user": true, "namespace": true}
	validStatuses = map[string]bool{"open": true, "done": true}
	validPrios    = map[string]bool{"high": true, "medium": true, "low": true}
)

// validateRels checks that any _ -prefixed rel names are known system properties
// and that their values are valid. Users may not define custom _ -prefixed rels.
func validateRels(rels []RelInput) error {
	for _, r := range rels {
		name := string(r.Type)
		if !strings.HasPrefix(name, "_") {
			continue
		}
		if v, ok := strings.CutPrefix(name, "_type::"); ok {
			if !validTypes[v] {
				return fmt.Errorf("invalid type %q: must be one of issue, note, user, namespace", v)
			}
		} else if v, ok := strings.CutPrefix(name, "_status::"); ok {
			if !validStatuses[v] {
				return fmt.Errorf("invalid status %q: must be one of open, done", v)
			}
		} else if v, ok := strings.CutPrefix(name, "_prio::"); ok {
			if !validPrios[v] {
				return fmt.Errorf("invalid priority %q: must be one of high, medium, low", v)
			}
		} else {
			return fmt.Errorf("invalid relation %q: custom _ prefix not allowed", name)
		}
	}
	return nil
}

// --- Query ---

func (s *nodeServiceImpl) GetByID(id string) (*models.Node, error) {
	n, err := s.store.GetNode(id)
	if err != nil {
		return nil, err
	}
	pc, err := s.getPermContext()
	if err != nil {
		return nil, err
	}
	if !pc.canRead(id) {
		return nil, fmt.Errorf("permission denied: no read access to node %s", id)
	}
	return n, nil
}

func (s *nodeServiceImpl) List(filter ListFilter) ([]*models.Node, error) {
	// Resolve namespace filter to owned node IDs.
	var nsOwnedIDs map[string]bool
	if filter.Namespace != "" {
		nsID, _ := s.resolveIDByNameAndType(s.store, filter.Namespace, "namespace")
		if nsID == "" {
			if exists, _ := s.store.NodeExists(filter.Namespace); exists {
				nsID = filter.Namespace
			}
		}
		if nsID == "" {
			return nil, nil // namespace not found
		}
		nsNode, err := s.store.GetNode(nsID)
		if err != nil {
			return nil, nil
		}
		nsOwnedIDs = make(map[string]bool)
		for _, ownedID := range nsNode.Relations[string(models.RelHasOwnership)] {
			nsOwnedIDs[ownedID] = true
		}
	}

	var storeFilters []*models.Rel
	for _, ri := range filter.Rels {
		if ri.Target == "" {
			storeFilters = append(storeFilters, &models.Rel{Type: ri.Type, Target: ""})
		} else {
			id, ok := s.lookupRelTarget(ri.Type, ri.Target)
			if !ok {
				return nil, nil
			}
			storeFilters = append(storeFilters, &models.Rel{Type: ri.Type, Target: id})
		}
	}
	nodes, err := s.store.FindNodes(storeFilters)
	if err != nil {
		return nil, err
	}
	pc, err := s.getPermContext()
	if err != nil {
		return nil, err
	}
	var result []*models.Node
	for _, n := range nodes {
		if !pc.canRead(n.ID) {
			continue
		}
		if nsOwnedIDs != nil && !nsOwnedIDs[n.ID] {
			continue
		}
		result = append(result, n)
	}

	if filter.HasDueDate || filter.DueWithin != nil {
		now := time.Now().UTC().Truncate(24 * time.Hour)
		filtered := result[:0]
		for _, n := range result {
			if n.DueDate == nil {
				continue
			}
			if filter.DueWithin != nil {
				due := n.DueDate.UTC().Truncate(24 * time.Hour)
				cutoff := now.AddDate(0, 0, *filter.DueWithin)
				if due.After(cutoff) {
					continue
				}
			}
			filtered = append(filtered, n)
		}
		result = filtered
	}

	return result, nil
}

// --- Lifecycle ---

func (s *nodeServiceImpl) Add(input AddInput) (*models.Node, error) {
	// Build tag set from tag rels (Target == ""), applying property-replacement semantics.
	tmp := models.NewNode()
	for _, r := range input.Rels {
		if r.Target == "" {
			tmp.AddTag(string(r.Type))
		}
	}

	// Apply defaults.
	if tmp.GetProperty("type") == "" {
		tmp.AddTag("_type::issue")
	}
	if tmp.GetProperty("type") == "issue" && tmp.GetProperty("status") == "" {
		tmp.AddTag("_status::open")
	}

	// Validate all rels (including the resolved default tags).
	tagRels := make([]RelInput, len(tmp.Tags))
	for i, t := range tmp.Tags {
		tagRels[i] = RelInput{Type: models.RelType(t)}
	}
	if err := validateRels(append(tagRels, input.Rels...)); err != nil {
		return nil, err
	}

	// Permission checks for edge rels.
	pc, err := s.getPermContext()
	if err != nil {
		return nil, err
	}
	for _, ri := range input.Rels {
		if ri.Target == "" {
			continue // tag rel, no target to check
		}
		targetID, found := s.lookupRelTarget(ri.Type, ri.Target)
		if !found {
			continue // will be auto-created; skip check
		}
		permLevel, isPerm := permRelLevels[ri.Type]
		switch {
		case ri.Type == models.RelHasOwnership:
			if !pc.hasOwnership(targetID) {
				return nil, fmt.Errorf("permission denied: no ownership of %q to transfer", ri.Target)
			}
		case isPerm:
			if pc.level(targetID) < permLevel {
				return nil, fmt.Errorf("permission denied: cannot grant %s on %q", ri.Type, ri.Target)
			}
		default:
			// Non-perm rel: source is the new node (creator gets ownership = can_create_rel).
			// Target: reference rels (assignee/mentions/in_namespace) need can_read; others need can_create_rel.
			if isReferenceRel(ri.Type) {
				if !pc.canRead(targetID) {
					return nil, fmt.Errorf("permission denied: no read access to %q", ri.Target)
				}
			} else {
				if !pc.canCreateRel(targetID) {
					return nil, fmt.Errorf("permission denied: no create_rel access to %q", ri.Target)
				}
			}
		}
	}

	dueDate, err := parseDueDate(input.DueDate)
	if err != nil {
		return nil, err
	}

	id, err := s.store.GenerateID()
	if err != nil {
		return nil, err
	}

	err = s.store.Transaction(func(st store.GraphStore) error {
		now := time.Now().UTC().Format(time.RFC3339)
		if err := st.AddNode(id, input.Title, input.Content, dueDate, now, now); err != nil {
			return err
		}

		// Store tag rels.
		for _, t := range tmp.Tags {
			if err := st.AddRel(id, t, ""); err != nil {
				return err
			}
		}

		// Mentions.
		for _, m := range mentions(input.Title + " " + input.Content) {
			userID, err := s.resolveUserRef(st, m)
			if err != nil {
				return err
			}
			if err := st.AddRel(id, string(models.RelMentions), userID); err != nil {
				return err
			}
		}

		// Edge rels.
		hasCreated := false
		for _, ri := range input.Rels {
			if ri.Target == "" {
				continue // already stored as tag
			}
			if ri.Type == models.RelCreated {
				hasCreated = true
			}
			resolved, err := s.resolveRelTarget(st, ri)
			if err != nil {
				return err
			}
			if ri.Type == models.RelHasOwnership {
				// Ownership transfer: remove existing owner of the target.
				existingOwners, _ := st.FindNodes([]*models.Rel{{Type: models.RelHasOwnership, Target: resolved}})
				for _, owner := range existingOwners {
					st.RemoveRel(owner.ID, string(models.RelHasOwnership), resolved) //nolint:errcheck
				}
			}
			if err := st.AddRel(id, string(ri.Type), resolved); err != nil {
				return err
			}
		}

		// Default created.
		if !hasCreated {
			userID, err := s.resolveUserRef(st, s.userID)
			if err != nil {
				return err
			}
			if err := st.AddRel(id, string(models.RelCreated), userID); err != nil {
				return err
			}
		}

		// Grant ownership of the new node.
		// Namespace nodes are owned by their creator (user node).
		// All other nodes are owned by the namespace they belong to — the user
		// retains transitive ownership through the namespace's own ownership chain
		// (e.g. user→has_ownership→default-ns→has_ownership→node).
		creatorID, err := s.resolveUserRef(st, s.userID)
		if err != nil {
			return err
		}
		ownerID := creatorID
		if tmp.GetProperty("type") != "namespace" {
			nsRef := input.Namespace
			if nsRef == "" {
				nsRef = s.userID
			}
			nsID, err := s.resolveNamespaceRef(st, nsRef)
			if err != nil {
				return err
			}
			ownerID = nsID
		}
		if err := st.AddRel(ownerID, string(models.RelHasOwnership), id); err != nil {
			return err
		}

		return nil
	})
	if err != nil {
		return nil, err
	}
	return s.store.GetNode(id)
}

func (s *nodeServiceImpl) Update(id string, input UpdateInput) (*models.Node, error) {
	// Validate rels before doing any I/O.
	if err := validateRels(input.AddRels); err != nil {
		return nil, err
	}

	// --- Permission checks ---
	pc, err := s.getPermContext()
	if err != nil {
		return nil, err
	}

	// Field/tag changes, rel removals, and namespace change require can_write on the node.
	needsWrite := input.Title != nil || input.Content != nil || input.DueDate != nil || input.Namespace != nil
	for _, ri := range input.AddRels {
		if ri.Target == "" {
			needsWrite = true
			break
		}
	}
	if len(input.RemoveRels) > 0 {
		needsWrite = true
	}
	if needsWrite && !pc.canWrite(id) {
		return nil, fmt.Errorf("permission denied: no write access to node %s", id)
	}

	// Check each edge rel being added.
	for _, ri := range input.AddRels {
		if ri.Target == "" {
			continue // tag — handled above
		}
		permLevel, isPerm := permRelLevels[ri.Type]
		targetID, found := s.lookupRelTarget(ri.Type, ri.Target)
		switch {
		case ri.Type == models.RelHasOwnership:
			if !found {
				return nil, fmt.Errorf("ownership target %q not found", ri.Target)
			}
			if !pc.hasOwnership(targetID) {
				return nil, fmt.Errorf("permission denied: no ownership of %q to transfer", ri.Target)
			}
			if !pc.canCreateRel(id) {
				return nil, fmt.Errorf("permission denied: no create_rel access to node %s", id)
			}
		case isPerm:
			// Perm rel: need perm_P on target; no check on source.
			if found && pc.level(targetID) < permLevel {
				return nil, fmt.Errorf("permission denied: insufficient permission on %q to grant %s", ri.Target, ri.Type)
			}
		default:
			// Non-perm rel: need can_create_rel on source.
			// Target: reference rels (assignee/mentions/in_namespace) need can_read; others need can_create_rel.
			if !pc.canCreateRel(id) {
				return nil, fmt.Errorf("permission denied: no create_rel access to node %s", id)
			}
			if found {
				if isReferenceRel(ri.Type) {
					if !pc.canRead(targetID) {
						return nil, fmt.Errorf("permission denied: no read access to %s target %q", ri.Type, ri.Target)
					}
				} else {
					if !pc.canCreateRel(targetID) {
						return nil, fmt.Errorf("permission denied: no create_rel access to %s target %q", ri.Type, ri.Target)
					}
				}
			}
		}
	}

	// Enforce blocking constraint before allowing status=done.
	for _, r := range input.AddRels {
		if r.Target == "" && string(r.Type) == "_status::done" {
			if err := s.checkBlockers(id); err != nil {
				return nil, err
			}
			break
		}
	}

	err = s.store.Transaction(func(st store.GraphStore) error {
		current, err := st.GetNode(id)
		if err != nil {
			return err
		}

		title, content := current.Title, current.Content
		var dueDate *time.Time
		if current.DueDate != nil {
			t := current.DueDate.Time
			dueDate = &t
		}
		if input.Title != nil {
			title = *input.Title
		}
		if input.Content != nil {
			content = *input.Content
		}
		if input.DueDate != nil {
			parsed, err := parseDueDate(*input.DueDate)
			if err != nil {
				return err
			}
			dueDate = parsed
		}

		now := time.Now().UTC().Format(time.RFC3339)
		if err := st.UpdateNode(id, title, content, dueDate, now); err != nil {
			return err
		}

		// Compute new tag set using the model's AddTag/RemoveTag to preserve
		// property-prefix replacement semantics.
		tmp := models.NewNode()
		for _, t := range current.Tags {
			tmp.AddTag(t)
		}
		for _, r := range input.AddRels {
			if r.Target == "" {
				tmp.AddTag(string(r.Type))
			}
		}
		for _, r := range input.RemoveRels {
			if r.Target == "" {
				tmp.RemoveTag(string(r.Type))
			}
		}

		currentTags, newTags := current.Tags, tmp.Tags
		for _, t := range currentTags {
			if !slices.Contains(newTags, t) {
				if err := st.RemoveRel(id, t, ""); err != nil {
					return err
				}
			}
		}
		for _, t := range newTags {
			if !slices.Contains(currentTags, t) {
				if err := st.AddRel(id, t, ""); err != nil {
					return err
				}
			}
		}

		// Sync mention edges when title or content changed.
		if input.Title != nil || input.Content != nil {
			if err := s.syncMentions(st, id, current, title, content); err != nil {
				return err
			}
		}

		currentRels := current.Relations
		for _, ri := range input.AddRels {
			if ri.Target == "" {
				continue // already handled as tag
			}
			resolved, err := s.resolveRelTarget(st, ri)
			if err != nil {
				return err
			}
			// Single-value relations replace the previous target.
			if ri.Type == models.RelAssignee {
				for _, oldTgt := range currentRels[string(ri.Type)] {
					if err := st.RemoveRel(id, string(ri.Type), oldTgt); err != nil {
						return err
					}
				}
			}
			// Ownership transfer: enforce single-owner constraint.
			if ri.Type == models.RelHasOwnership {
				existingOwners, _ := st.FindNodes([]*models.Rel{{Type: models.RelHasOwnership, Target: resolved}})
				for _, owner := range existingOwners {
					st.RemoveRel(owner.ID, string(models.RelHasOwnership), resolved) //nolint:errcheck
				}
			}
			if err := st.AddRel(id, string(ri.Type), resolved); err != nil {
				return err
			}
		}

		for _, ri := range input.RemoveRels {
			if ri.Target == "" {
				continue // already handled as tag
			}
			resolved, err := s.resolveRelTarget(st, ri)
			if err != nil {
				return err
			}
			if err := st.RemoveRel(id, string(ri.Type), resolved); err != nil {
				return err
			}
		}

		// Namespace change: transfer ownership from the current namespace to the new one.
		if input.Namespace != nil {
			newNsID, err := s.resolveNamespaceRef(st, *input.Namespace)
			if err != nil {
				return err
			}
			// Remove ownership from any current namespace owner.
			currentOwners, _ := st.FindNodes([]*models.Rel{{Type: models.RelHasOwnership, Target: id}})
			for _, owner := range currentOwners {
				if owner.GetProperty("type") == "namespace" {
					st.RemoveRel(owner.ID, string(models.RelHasOwnership), id) //nolint:errcheck
				}
			}
			if err := st.AddRel(newNsID, string(models.RelHasOwnership), id); err != nil {
				return err
			}
		}

		return nil
	})
	if err != nil {
		return nil, err
	}
	return s.store.GetNode(id)
}

func (s *nodeServiceImpl) Delete(id string) error {
	pc, err := s.getPermContext()
	if err != nil {
		return err
	}
	if !pc.canWrite(id) {
		return fmt.Errorf("permission denied: no write access to node %s", id)
	}
	return s.store.Transaction(func(st store.GraphStore) error {
		return s.cascadeDelete(st, id, make(map[string]bool))
	})
}

// cascadeDelete deletes id and all nodes it owns (recursively).
// visited prevents infinite loops from ownership cycles.
func (s *nodeServiceImpl) cascadeDelete(st store.GraphStore, id string, visited map[string]bool) error {
	if visited[id] {
		return nil
	}
	visited[id] = true
	node, err := st.GetNode(id)
	if err != nil {
		return err
	}
	// Capture owned node IDs before deleting (DeleteNode cascades the rels).
	ownedIDs := make([]string, len(node.Relations[string(models.RelHasOwnership)]))
	copy(ownedIDs, node.Relations[string(models.RelHasOwnership)])
	if err := st.DeleteNode(id); err != nil {
		return err
	}
	for _, ownedID := range ownedIDs {
		if ownedID == id {
			continue // skip self-ownership
		}
		s.cascadeDelete(st, ownedID, visited) //nolint:errcheck — node may already be gone
	}
	return nil
}

// --- User management ---

func (s *nodeServiceImpl) AddUser(name string) (*models.Node, error) {
	var id string
	err := s.store.Transaction(func(st store.GraphStore) error {
		var err error
		id, err = s.ensureUser(st, name)
		return err
	})
	if err != nil {
		return nil, err
	}
	return s.store.GetNode(id)
}

func (s *nodeServiceImpl) ListUsers() ([]*models.Node, error) {
	return s.store.FindNodes([]*models.Rel{{Type: "_type::user", Target: ""}})
}

// --- Internal helpers ---

func (s *nodeServiceImpl) checkBlockers(id string) error {
	blockers, err := s.store.FindNodes([]*models.Rel{{Type: models.RelBlocks, Target: id}})
	if err != nil {
		return err
	}
	var blocking []string
	for _, b := range blockers {
		if b.GetProperty("status") == "open" {
			blocking = append(blocking, b.ID)
		}
	}
	if len(blocking) > 0 {
		return fmt.Errorf("cannot close: blocked by %v", blocking)
	}
	return nil
}

func (s *nodeServiceImpl) syncMentions(st store.GraphStore, id string, current *models.Node, newTitle, newContent string) error {
	existingMentionIDs := make(map[string]bool)
	for _, uid := range current.Relations[string(models.RelMentions)] {
		existingMentionIDs[uid] = true
	}
	mentionedUserIDs := make(map[string]bool)
	for _, m := range mentions(newTitle + " " + newContent) {
		userID, err := s.resolveUserRef(st, m)
		if err != nil {
			return err
		}
		mentionedUserIDs[userID] = true
		if !existingMentionIDs[userID] {
			if err := st.AddRel(id, string(models.RelMentions), userID); err != nil {
				return err
			}
		}
	}
	for uid := range existingMentionIDs {
		if !mentionedUserIDs[uid] {
			if err := st.RemoveRel(id, string(models.RelMentions), uid); err != nil {
				return err
			}
		}
	}
	return nil
}

// resolveRelTarget resolves a RelInput target to a node ID, auto-creating users
// and namespaces as needed. Use only inside a transaction.
func (s *nodeServiceImpl) resolveRelTarget(st store.GraphStore, ri RelInput) (string, error) {
	switch ri.Type {
	case models.RelAssignee, models.RelCreated, models.RelMentions:
		return s.resolveUserRef(st, ri.Target)
	default:
		// Permission rels and all other edge rels expect raw node IDs.
		return ri.Target, nil
	}
}

// lookupRelTarget resolves a filter target to a node ID without creating anything.
// Returns ("", false) when the target doesn't exist.
func (s *nodeServiceImpl) lookupRelTarget(relType models.RelType, target string) (string, bool) {
	if exists, _ := s.store.NodeExists(target); exists {
		return target, true
	}
	var nodeType string
	switch relType {
	case models.RelAssignee, models.RelCreated, models.RelMentions:
		nodeType = "user"
	default:
		// Permission rels and other edge rels use raw node IDs.
		return "", false
	}
	id, err := s.resolveIDByNameAndType(s.store, target, nodeType)
	if err != nil || id == "" {
		return "", false
	}
	return id, true
}

// resolveIDByNameAndType finds a node by title and _type property without creating it.
func (s *nodeServiceImpl) resolveIDByNameAndType(st store.GraphStore, title, nodeType string) (string, error) {
	nodes, err := st.FindNodes([]*models.Rel{{Type: models.RelType("_type::" + nodeType), Target: ""}})
	if err != nil {
		return "", err
	}
	for _, n := range nodes {
		if n.Title == title {
			return n.ID, nil
		}
	}
	return "", nil
}

func (s *nodeServiceImpl) resolveUserRef(st store.GraphStore, ref string) (string, error) {
	if exists, _ := st.NodeExists(ref); exists {
		return ref, nil
	}
	return s.ensureUser(st, ref)
}

const globalNamespace = "_global"

func (s *nodeServiceImpl) ensureGlobalNamespace(st store.GraphStore) (string, error) {
	nsID, err := s.resolveIDByNameAndType(st, globalNamespace, "namespace")
	if err != nil {
		return "", err
	}
	if nsID != "" {
		return nsID, nil
	}
	id, err := st.GenerateID()
	if err != nil {
		return "", err
	}
	now := time.Now().UTC().Format(time.RFC3339)
	if err := st.AddNode(id, globalNamespace, "", nil, now, now); err != nil {
		return "", err
	}
	if err := st.AddRel(id, "_type::namespace", ""); err != nil {
		return "", err
	}
	// Self-owned so no single user controls it.
	if err := st.AddRel(id, string(models.RelHasOwnership), id); err != nil {
		return "", err
	}
	return id, nil
}

func (s *nodeServiceImpl) ensureUser(st store.GraphStore, username string) (string, error) {
	userID, err := s.resolveIDByNameAndType(st, username, "user")
	if err != nil {
		return "", err
	}
	if userID != "" {
		return userID, nil
	}
	id, err := st.GenerateID()
	if err != nil {
		return "", err
	}
	now := time.Now().UTC().Format(time.RFC3339)
	if err := st.AddNode(id, username, "", nil, now, now); err != nil {
		return "", err
	}
	if err := st.AddRel(id, "_type::user", ""); err != nil {
		return "", err
	}
	// Users have self-ownership by default.
	if err := st.AddRel(id, string(models.RelHasOwnership), id); err != nil {
		return "", err
	}
	// Every user gets can_create_rel access to the global namespace (inclusive
	// of can_read), providing a shared space where all users can see and interact with
	// nodes that are published there. User nodes themselves are already
	// globally readable via the post-BFS identity override in getPermContext.
	globalNsID, err := s.ensureGlobalNamespace(st)
	if err != nil {
		return "", err
	}
	if err := st.AddRel(id, string(models.RelCanCreateRel), globalNsID); err != nil {
		return "", err
	}
	return id, nil
}

func (s *nodeServiceImpl) resolveNamespaceRef(st store.GraphStore, ref string) (string, error) {
	if exists, _ := st.NodeExists(ref); exists {
		return ref, nil
	}
	return s.ensureNamespace(st, ref)
}

func (s *nodeServiceImpl) ensureNamespace(st store.GraphStore, name string) (string, error) {
	nsID, err := s.resolveIDByNameAndType(st, name, "namespace")
	if err != nil {
		return "", err
	}
	if nsID != "" {
		return nsID, nil
	}
	id, err := st.GenerateID()
	if err != nil {
		return "", err
	}
	now := time.Now().UTC().Format(time.RFC3339)
	if err := st.AddNode(id, name, "", nil, now, now); err != nil {
		return "", err
	}
	if err := st.AddRel(id, "_type::namespace", ""); err != nil {
		return "", err
	}
	userID, err := s.resolveUserRef(st, s.userID)
	if err != nil {
		return "", err
	}
	if err := st.AddRel(id, string(models.RelCreated), userID); err != nil {
		return "", err
	}
	// Creator owns the namespace.
	if err := st.AddRel(userID, string(models.RelHasOwnership), id); err != nil {
		return "", err
	}
	return id, nil
}
