package service

import (
	"context"
	"sync"

	"github.com/mhsanaei/3x-ui/v3/internal/database"
	"github.com/mhsanaei/3x-ui/v3/internal/database/model"
	"github.com/mhsanaei/3x-ui/v3/internal/web/runtime"
)

// LocalDescendants returns this panel's read-only summaries of the nodes it
// directly manages, so a parent panel can surface them as transitive sub-nodes
// (#4983). Only nodes with a known GUID are included — a stable identity is
// required to attribute them one hop up. Not recursive: each panel reports its
// own direct nodes, and a master walks one level via each direct node's
// endpoint, which covers the Node1 -> Node2 -> Node3 case.
func (s *NodeService) LocalDescendants() ([]model.NodeSummary, error) {
	selfGuid, _ := (&SettingService{}).GetPanelGuid()
	db := database.GetDB()
	var nodes []*model.Node
	if err := db.Model(model.Node{}).Order("id asc").Find(&nodes).Error; err != nil {
		return nil, err
	}
	out := make([]model.NodeSummary, 0, len(nodes))
	for _, n := range nodes {
		if n.Guid == "" {
			continue
		}
		out = append(out, model.NodeSummary{
			Guid:          n.Guid,
			ParentGuid:    selfGuid,
			Name:          n.Name,
			Address:       n.Address,
			Scheme:        n.Scheme,
			Port:          n.Port,
			Status:        n.Status,
			LastHeartbeat: n.LastHeartbeat,
			LatencyMs:     n.LatencyMs,
			PanelVersion:  n.PanelVersion,
			XrayVersion:   n.XrayVersion,
			XrayState:     n.XrayState,
			XrayError:     n.XrayError,
		})
	}
	return out, nil
}

var (
	nodeDescendantsMu    sync.RWMutex
	nodeDescendantsCache = map[int][]model.NodeSummary{}
)

// RefreshDescendants pulls a direct node's published sub-node summaries and
// caches them keyed by node id. Best-effort: a fetch error keeps the last good
// set (the node may be briefly unreachable). Called from the heartbeat job.
func (s *NodeService) RefreshDescendants(ctx context.Context, n *model.Node) {
	if n == nil {
		return
	}
	mgr := runtime.GetManager()
	if mgr == nil {
		return
	}
	rt, err := mgr.RemoteFor(n)
	if err != nil {
		return
	}
	summaries, err := rt.GetDescendants(ctx)
	if err != nil {
		return
	}
	nodeDescendantsMu.Lock()
	if len(summaries) == 0 {
		delete(nodeDescendantsCache, n.Id)
	} else {
		nodeDescendantsCache[n.Id] = summaries
	}
	nodeDescendantsMu.Unlock()
}

// ClearDescendants drops a node's cached sub-node summaries (its probe failed).
func (s *NodeService) ClearDescendants(nodeID int) {
	nodeDescendantsMu.Lock()
	delete(nodeDescendantsCache, nodeID)
	nodeDescendantsMu.Unlock()
}

func cachedDescendants() []model.NodeSummary {
	nodeDescendantsMu.RLock()
	defer nodeDescendantsMu.RUnlock()
	out := make([]model.NodeSummary, 0)
	for _, list := range nodeDescendantsCache {
		out = append(out, list...)
	}
	return out
}

// GetNodeTree returns the direct nodes plus any transitive sub-nodes learned
// from them, with per-GUID counts so each node shows only the inbounds/online
// it physically hosts (#4983). Direct nodes carry the master's own GUID as
// ParentGuid; a transitive node carries its parent node's GUID. Transitive
// nodes are read-only projections (Id == 0). Used by the Nodes page and the
// heartbeat broadcast — never for probing/syncing, which stay on GetAll.
func (s *NodeService) GetNodeTree() ([]*model.Node, error) {
	nodes, err := s.GetAll()
	if err != nil {
		return nodes, err
	}
	selfGuid, _ := (&SettingService{}).GetPanelGuid()
	directGuids := make(map[string]struct{}, len(nodes))
	for _, n := range nodes {
		n.ParentGuid = selfGuid
		if n.Guid != "" {
			directGuids[n.Guid] = struct{}{}
		}
	}

	seen := make(map[string]struct{})
	var transitive []*model.Node
	for _, sum := range cachedDescendants() {
		if sum.Guid == "" {
			continue
		}
		if _, ok := directGuids[sum.Guid]; ok {
			continue // already shown as a direct node
		}
		if _, ok := seen[sum.Guid]; ok {
			continue
		}
		seen[sum.Guid] = struct{}{}
		transitive = append(transitive, &model.Node{
			Guid:          sum.Guid,
			ParentGuid:    sum.ParentGuid,
			Name:          sum.Name,
			Address:       sum.Address,
			Scheme:        sum.Scheme,
			Port:          sum.Port,
			Status:        sum.Status,
			LastHeartbeat: sum.LastHeartbeat,
			LatencyMs:     sum.LatencyMs,
			PanelVersion:  sum.PanelVersion,
			XrayVersion:   sum.XrayVersion,
			XrayState:     sum.XrayState,
			XrayError:     sum.XrayError,
			Transitive:    true,
		})
	}
	if len(transitive) == 0 {
		return nodes, nil
	}

	all := make([]*model.Node, 0, len(nodes)+len(transitive))
	all = append(all, nodes...)
	all = append(all, transitive...)
	s.recountByGuid(all, selfGuid)
	return all, nil
}

func (s *NodeService) GetNodeTreeView() ([]*NodeView, error) {
	nodes, err := s.GetNodeTree()
	if err != nil {
		return nil, err
	}
	return toNodeViews(nodes), nil
}

// recountByGuid recomputes InboundCount/OnlineCount/DepletedCount for every node
// in the tree, keyed by the GUID that physically hosts each inbound, so a direct
// node shows only its own inbounds and each transitive node shows its own
// (#4983). In a flat topology the per-GUID and per-node-id counts coincide, so
// this only changes behaviour once a transitive node exists.
func (s *NodeService) recountByGuid(nodes []*model.Node, selfGuid string) {
	db := database.GetDB()
	type ibRow struct {
		Id             int
		NodeID         *int   `gorm:"column:node_id"`
		OriginNodeGuid string `gorm:"column:origin_node_guid"`
	}
	var ibRows []ibRow
	if err := db.Table("inbounds").Select("id, node_id, origin_node_guid").Scan(&ibRows).Error; err != nil {
		return
	}
	ambiguous := ambiguousNodeGuids(nodes, selfGuid)
	effByInbound := make(map[int]string, len(ibRows))
	inboundCountByGuid := make(map[string]int)
	for _, r := range ibRows {
		guid := r.OriginNodeGuid
		if guid == "" {
			if r.NodeID != nil {
				guid = synthNodeGuid(*r.NodeID)
			} else {
				guid = selfGuid
			}
		} else if r.NodeID != nil {
			// Origin still holds an ambiguous GUID (cloned server / master-shared,
			// not yet re-attributed): bucket under the hosting node's unique id so
			// the clones don't merge.
			if _, bad := ambiguous[guid]; bad {
				guid = synthNodeGuid(*r.NodeID)
			}
		}
		effByInbound[r.Id] = guid
		inboundCountByGuid[guid]++
	}

	// Classify by EMAIL (not the stale client_traffics.inbound_id) and bucket
	// each client under its inbound's effective attribution GUID, deduping a
	// client attached to several inbounds under the same GUID.
	depletedByGuid := make(map[string]int)
	disabledByGuid := make(map[string]int)
	activeByGuid := make(map[string]int)
	if statuses, err := s.nodeClientStatuses(); err == nil {
		seen := make(map[string]map[int]struct{})
		for _, st := range statuses {
			guid, ok := effByInbound[st.InboundID]
			if !ok {
				continue
			}
			clientsSeen := seen[guid]
			if clientsSeen == nil {
				clientsSeen = make(map[int]struct{})
				seen[guid] = clientsSeen
			}
			if _, dup := clientsSeen[st.ClientID]; dup {
				continue
			}
			clientsSeen[st.ClientID] = struct{}{}
			switch {
			case st.Depleted:
				depletedByGuid[guid]++
			case st.Disabled:
				disabledByGuid[guid]++
			default:
				activeByGuid[guid]++
			}
		}
	}

	onlineByGuid := s.onlineEmailsByGuid()
	for _, n := range nodes {
		guid := effectiveNodeGuid(n, ambiguous)
		n.InboundCount = inboundCountByGuid[guid]
		n.OnlineCount = len(onlineByGuid[guid])
		n.DepletedCount = depletedByGuid[guid]
		n.DisabledCount = disabledByGuid[guid]
		n.ActiveCount = activeByGuid[guid]
	}
}
