package nodes

import (
	"context"
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"sync"
	"time"
)

const defaultNodeTTL = 60 * time.Second

// Manager keeps paired node metadata and basic routing helpers.
type Handler func(req Request) Response
type WireSender interface {
	Send(msg WireMessage) error
}

type Manager struct {
	mu        sync.RWMutex
	nodes     map[string]NodeInfo
	handlers  map[string]Handler
	senders   map[string]WireSender
	pending   map[string]chan WireMessage
	nextWire  uint64
	ttl       time.Duration
	auditPath string
	statePath string
	policy    DispatchPolicy
}

type DispatchPolicy struct {
	PreferLocal        bool
	PreferP2P          bool
	AllowRelayFallback bool
	ActionTags         map[string][]string
	AgentTags          map[string][]string
	AllowActions       map[string][]string
	DenyActions        map[string][]string
	AllowAgents        map[string][]string
	DenyAgents         map[string][]string
}

var defaultManager = NewManager()

var nodeActionCapabilityChecks = map[string]func(Capabilities) bool{
	"run":             func(c Capabilities) bool { return c.Run },
	"agent_task":      func(c Capabilities) bool { return c.Model },
	"camera_snap":     func(c Capabilities) bool { return c.Camera },
	"camera_clip":     func(c Capabilities) bool { return c.Camera },
	"screen_record":   func(c Capabilities) bool { return c.Screen },
	"screen_snapshot": func(c Capabilities) bool { return c.Screen },
	"location_get":    func(c Capabilities) bool { return c.Location },
	"canvas_snapshot": func(c Capabilities) bool { return c.Canvas },
	"canvas_action":   func(c Capabilities) bool { return c.Canvas },
}

var realtimePreferredActions = map[string]struct{}{
	"camera_snap":     {},
	"camera_clip":     {},
	"screen_record":   {},
	"screen_snapshot": {},
	"canvas_snapshot": {},
	"canvas_action":   {},
}

var wireMessageHandlers = map[string]func(*Manager, WireMessage) bool{
	"node_response": handleWireNodeResponse,
}

func DefaultManager() *Manager { return defaultManager }

func handleWireNodeResponse(m *Manager, msg WireMessage) bool {
	if strings.TrimSpace(msg.ID) == "" {
		return false
	}
	m.mu.Lock()
	ch := m.pending[msg.ID]
	if ch != nil {
		delete(m.pending, msg.ID)
	}
	m.mu.Unlock()
	if ch == nil {
		return false
	}
	select {
	case ch <- msg:
	default:
	}
	return true
}

func NewManager() *Manager {
	m := &Manager{
		nodes:    map[string]NodeInfo{},
		handlers: map[string]Handler{},
		senders:  map[string]WireSender{},
		pending:  map[string]chan WireMessage{},
		ttl:      defaultNodeTTL,
		policy: DispatchPolicy{
			PreferP2P:          true,
			AllowRelayFallback: true,
			ActionTags:         map[string][]string{},
			AgentTags:          map[string][]string{},
			AllowActions:       map[string][]string{},
			DenyActions:        map[string][]string{},
			AllowAgents:        map[string][]string{},
			DenyAgents:         map[string][]string{},
		},
	}
	go m.reaperLoop()
	return m
}

func (m *Manager) SetAuditPath(path string) {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.auditPath = strings.TrimSpace(path)
}

func (m *Manager) SetStatePath(path string) {
	m.mu.Lock()
	m.statePath = strings.TrimSpace(path)
	m.mu.Unlock()
	m.loadState()
}

func (m *Manager) SetDispatchPolicy(policy DispatchPolicy) {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.policy = normalizeDispatchPolicy(policy)
}

func (m *Manager) DispatchPolicy() DispatchPolicy {
	m.mu.RLock()
	defer m.mu.RUnlock()
	return cloneDispatchPolicy(m.policy)
}

func (m *Manager) Upsert(info NodeInfo) {
	m.mu.Lock()
	now := time.Now().UTC()
	old, existed := m.nodes[info.ID]
	info.LastSeenAt = now
	info.Online = true
	if existed {
		if info.RegisteredAt.IsZero() {
			info.RegisteredAt = old.RegisteredAt
		}
		if len(info.Tags) == 0 && len(old.Tags) > 0 {
			info.Tags = append([]string(nil), old.Tags...)
		}
		if strings.TrimSpace(info.Endpoint) == "" {
			info.Endpoint = old.Endpoint
		}
		if strings.TrimSpace(info.Token) == "" {
			info.Token = old.Token
		}
	} else if info.RegisteredAt.IsZero() {
		info.RegisteredAt = now
	}
	m.nodes[info.ID] = info
	m.mu.Unlock()
	m.saveState()
	m.appendAudit("upsert", info.ID, map[string]interface{}{"existed": existed, "endpoint": info.Endpoint, "version": info.Version})
}

func (m *Manager) MarkOffline(id string) {
	m.mu.Lock()
	changed := false
	if n, ok := m.nodes[id]; ok {
		n.Online = false
		m.nodes[id] = n
		changed = true
	}
	m.mu.Unlock()
	if changed {
		m.saveState()
	}
}

func (m *Manager) Get(id string) (NodeInfo, bool) {
	m.mu.RLock()
	defer m.mu.RUnlock()
	n, ok := m.nodes[id]
	return n, ok
}

func (m *Manager) List() []NodeInfo {
	m.mu.RLock()
	defer m.mu.RUnlock()
	out := make([]NodeInfo, 0, len(m.nodes))
	for _, n := range m.nodes {
		out = append(out, n)
	}
	sort.Slice(out, func(i, j int) bool { return out[i].LastSeenAt.After(out[j].LastSeenAt) })
	return out
}

func (m *Manager) Remove(id string) bool {
	id = strings.TrimSpace(id)
	if id == "" {
		return false
	}
	m.mu.Lock()
	_, exists := m.nodes[id]
	if exists {
		delete(m.nodes, id)
		delete(m.handlers, id)
	}
	m.mu.Unlock()
	if exists {
		m.saveState()
		m.appendAudit("delete", id, nil)
	}
	return exists
}

func (m *Manager) RegisterHandler(nodeID string, h Handler) {
	m.mu.Lock()
	defer m.mu.Unlock()
	if strings.TrimSpace(nodeID) == "" || h == nil {
		return
	}
	m.handlers[nodeID] = h
}

func (m *Manager) RegisterWireSender(nodeID string, sender WireSender) {
	nodeID = strings.TrimSpace(nodeID)
	if nodeID == "" {
		return
	}
	m.mu.Lock()
	defer m.mu.Unlock()
	if sender == nil {
		delete(m.senders, nodeID)
		return
	}
	m.senders[nodeID] = sender
}

func (m *Manager) HandleWireMessage(msg WireMessage) bool {
	if handler := wireMessageHandlers[strings.ToLower(strings.TrimSpace(msg.Type))]; handler != nil {
		return handler(m, msg)
	}
	return false
}

func (m *Manager) SendWireRequest(ctx context.Context, nodeID string, req Request) (Response, error) {
	nodeID = strings.TrimSpace(nodeID)
	if nodeID == "" {
		return Response{}, fmt.Errorf("node id required")
	}
	m.mu.Lock()
	sender := m.senders[nodeID]
	if sender == nil {
		m.mu.Unlock()
		return Response{}, fmt.Errorf("node %s websocket sender unavailable", nodeID)
	}
	m.nextWire++
	wireID := fmt.Sprintf("wire-%d", m.nextWire)
	ch := make(chan WireMessage, 1)
	m.pending[wireID] = ch
	m.mu.Unlock()

	msg := WireMessage{
		Type:    "node_request",
		ID:      wireID,
		To:      nodeID,
		Request: &req,
	}
	if err := sender.Send(msg); err != nil {
		m.mu.Lock()
		delete(m.pending, wireID)
		m.mu.Unlock()
		return Response{}, err
	}

	select {
	case <-ctx.Done():
		m.mu.Lock()
		delete(m.pending, wireID)
		m.mu.Unlock()
		return Response{}, ctx.Err()
	case incoming := <-ch:
		if incoming.Response == nil {
			return Response{}, fmt.Errorf("node %s returned empty response", nodeID)
		}
		return *incoming.Response, nil
	}
}

func (m *Manager) Invoke(req Request) (Response, bool) {
	m.mu.RLock()
	h, ok := m.handlers[req.Node]
	m.mu.RUnlock()
	if !ok {
		return Response{}, false
	}
	resp := h(req)
	if strings.TrimSpace(resp.Node) == "" {
		resp.Node = req.Node
	}
	if strings.TrimSpace(resp.Action) == "" {
		resp.Action = req.Action
	}
	return resp, true
}

func (m *Manager) SupportsAction(nodeID, action string) bool {
	n, ok := m.Get(nodeID)
	if !ok || !n.Online {
		return false
	}
	return nodeSupportsRequest(n, Request{Action: action})
}

func (m *Manager) SupportsRequest(nodeID string, req Request) bool {
	n, ok := m.Get(nodeID)
	if !ok || !n.Online {
		return false
	}
	return nodeSupportsRequest(n, req)
}

func nodeSupportsRequest(n NodeInfo, req Request) bool {
	action := strings.ToLower(strings.TrimSpace(req.Action))
	if len(n.Actions) > 0 {
		allowed := false
		for _, a := range n.Actions {
			if strings.ToLower(strings.TrimSpace(a)) == action {
				allowed = true
				break
			}
		}
		if !allowed {
			return false
		}
	}
	if check := nodeActionCapabilityChecks[action]; check != nil {
		return check(n.Capabilities)
	}
	return n.Capabilities.Invoke
}

func (m *Manager) PickFor(action string) (NodeInfo, bool) {
	return m.PickRequest(Request{Action: action}, "auto")
}

func (m *Manager) PickRequest(req Request, mode string) (NodeInfo, bool) {
	m.mu.RLock()
	defer m.mu.RUnlock()
	bestScore := -1
	bestNode := NodeInfo{}
	policy := normalizeDispatchPolicy(m.policy)
	for _, n := range m.nodes {
		score, ok := scoreNodeCandidate(n, req, mode, m.senders[strings.TrimSpace(n.ID)] != nil, policy)
		if !ok {
			continue
		}
		if score > bestScore || (score == bestScore && bestNode.ID != "" && n.LastSeenAt.After(bestNode.LastSeenAt)) {
			bestScore = score
			bestNode = n
		}
	}
	if bestScore < 0 || strings.TrimSpace(bestNode.ID) == "" {
		return NodeInfo{}, false
	}
	return bestNode, true
}

func scoreNodeCandidate(n NodeInfo, req Request, mode string, hasWireSender bool, policy DispatchPolicy) (int, bool) {
	if !n.Online {
		return 0, false
	}
	if !nodeSupportsRequest(n, req) {
		return 0, false
	}
	if !matchesDispatchPolicy(n, req, policy) {
		return 0, false
	}

	mode = strings.ToLower(strings.TrimSpace(mode))
	if mode == "p2p" && !hasWireSender {
		return 0, false
	}
	if !policy.AllowRelayFallback && strings.TrimSpace(n.ID) != "local" && !hasWireSender {
		return 0, false
	}

	score := 100
	if hasWireSender {
		score += 30
	}
	if policy.PreferP2P {
		if hasWireSender {
			score += 35
		} else {
			score -= 10
		}
	}
	if policy.PreferLocal && strings.EqualFold(strings.TrimSpace(n.ID), "local") {
		score += 60
	}
	if prefersRealtimeTransport(req.Action) && hasWireSender {
		score += 40
	}
	if mode == "relay" && hasWireSender {
		score -= 10
	}
	if mode == "p2p" && hasWireSender {
		score += 80
	}
	if strings.EqualFold(strings.TrimSpace(req.Action), "agent_task") {
		remoteAgentID := requestedRemoteAgentID(req.Args)
		switch {
		case remoteAgentID == "", remoteAgentID == "main":
			score += 20
		case nodeHasAgent(n, remoteAgentID):
			score += 80
		default:
			return 0, false
		}
	}
	if !n.LastSeenAt.IsZero() {
		ageSeconds := int(time.Since(n.LastSeenAt).Seconds())
		if ageSeconds < 0 {
			ageSeconds = 0
		}
		if ageSeconds < 60 {
			score += 20
		} else if ageSeconds < 300 {
			score += 5
		}
	}
	return score, true
}

func normalizeDispatchPolicy(policy DispatchPolicy) DispatchPolicy {
	normalized := DispatchPolicy{
		PreferLocal:        policy.PreferLocal,
		PreferP2P:          policy.PreferP2P,
		AllowRelayFallback: policy.AllowRelayFallback,
		ActionTags:         map[string][]string{},
		AgentTags:          map[string][]string{},
		AllowActions:       map[string][]string{},
		DenyActions:        map[string][]string{},
		AllowAgents:        map[string][]string{},
		DenyAgents:         map[string][]string{},
	}
	for key, tags := range policy.ActionTags {
		trimmed := normalizeStringList(tags)
		if len(trimmed) > 0 {
			normalized.ActionTags[strings.ToLower(strings.TrimSpace(key))] = trimmed
		}
	}
	for key, tags := range policy.AgentTags {
		trimmed := normalizeStringList(tags)
		if len(trimmed) > 0 {
			normalized.AgentTags[strings.ToLower(strings.TrimSpace(key))] = trimmed
		}
	}
	for key, tags := range policy.AllowActions {
		trimmed := normalizeStringList(tags)
		if len(trimmed) > 0 {
			normalized.AllowActions[strings.ToLower(strings.TrimSpace(key))] = trimmed
		}
	}
	for key, tags := range policy.DenyActions {
		trimmed := normalizeStringList(tags)
		if len(trimmed) > 0 {
			normalized.DenyActions[strings.ToLower(strings.TrimSpace(key))] = trimmed
		}
	}
	for key, tags := range policy.AllowAgents {
		trimmed := normalizeStringList(tags)
		if len(trimmed) > 0 {
			normalized.AllowAgents[strings.ToLower(strings.TrimSpace(key))] = trimmed
		}
	}
	for key, tags := range policy.DenyAgents {
		trimmed := normalizeStringList(tags)
		if len(trimmed) > 0 {
			normalized.DenyAgents[strings.ToLower(strings.TrimSpace(key))] = trimmed
		}
	}
	return normalized
}

func cloneDispatchPolicy(policy DispatchPolicy) DispatchPolicy {
	return normalizeDispatchPolicy(policy)
}

func normalizeStringList(values []string) []string {
	if len(values) == 0 {
		return nil
	}
	seen := map[string]struct{}{}
	out := make([]string, 0, len(values))
	for _, raw := range values {
		trimmed := strings.ToLower(strings.TrimSpace(raw))
		if trimmed == "" {
			continue
		}
		if _, ok := seen[trimmed]; ok {
			continue
		}
		seen[trimmed] = struct{}{}
		out = append(out, trimmed)
	}
	return out
}

func matchesDispatchPolicy(n NodeInfo, req Request, policy DispatchPolicy) bool {
	if !isNodePermittedByPolicy(n, req, policy) {
		return false
	}
	if tags := policy.ActionTags[strings.ToLower(strings.TrimSpace(req.Action))]; len(tags) > 0 && !nodeMatchesAnyTag(n, tags) {
		return false
	}
	remoteAgentID := requestedRemoteAgentID(req.Args)
	if remoteAgentID != "" {
		if tags := policy.AgentTags[remoteAgentID]; len(tags) > 0 && !nodeMatchesAnyTag(n, tags) {
			return false
		}
	}
	return true
}

func isNodePermittedByPolicy(n NodeInfo, req Request, policy DispatchPolicy) bool {
	nodeID := strings.ToLower(strings.TrimSpace(n.ID))
	action := strings.ToLower(strings.TrimSpace(req.Action))
	remoteAgentID := requestedRemoteAgentID(req.Args)
	if remoteAgentID == "" && action == "agent_task" {
		remoteAgentID = "main"
	}
	if deny := policy.DenyActions[nodeID]; len(deny) > 0 && containsNormalized(deny, action) {
		return false
	}
	if allow := policy.AllowActions[nodeID]; len(allow) > 0 && !containsNormalized(allow, action) {
		return false
	}
	if remoteAgentID != "" {
		if deny := policy.DenyAgents[nodeID]; len(deny) > 0 && containsNormalized(deny, remoteAgentID) {
			return false
		}
		if allow := policy.AllowAgents[nodeID]; len(allow) > 0 && !containsNormalized(allow, remoteAgentID) {
			return false
		}
	}
	return true
}

func containsNormalized(items []string, target string) bool {
	target = strings.ToLower(strings.TrimSpace(target))
	for _, item := range items {
		if strings.ToLower(strings.TrimSpace(item)) == target {
			return true
		}
	}
	return false
}

func nodeMatchesAnyTag(n NodeInfo, tags []string) bool {
	if len(tags) == 0 {
		return true
	}
	nodeTags := normalizeStringList(n.Tags)
	if len(nodeTags) == 0 {
		return false
	}
	seen := map[string]struct{}{}
	for _, tag := range nodeTags {
		seen[tag] = struct{}{}
	}
	for _, tag := range tags {
		if _, ok := seen[strings.ToLower(strings.TrimSpace(tag))]; ok {
			return true
		}
	}
	return false
}

func requestedRemoteAgentID(args map[string]interface{}) string {
	if len(args) == 0 {
		return ""
	}
	value, ok := args["remote_agent_id"]
	if !ok || value == nil {
		return ""
	}
	return strings.ToLower(strings.TrimSpace(fmt.Sprint(value)))
}

func nodeHasAgent(n NodeInfo, agentID string) bool {
	agentID = strings.ToLower(strings.TrimSpace(agentID))
	if agentID == "" {
		return false
	}
	for _, agent := range n.Agents {
		if strings.ToLower(strings.TrimSpace(agent.ID)) == agentID {
			return true
		}
	}
	return false
}

func prefersRealtimeTransport(action string) bool {
	_, ok := realtimePreferredActions[strings.ToLower(strings.TrimSpace(action))]
	return ok
}

func (m *Manager) reaperLoop() {
	t := time.NewTicker(15 * time.Second)
	defer t.Stop()
	for range t.C {
		cutoff := time.Now().UTC().Add(-m.ttl)
		m.mu.Lock()
		offlined := make([]string, 0)
		for id, n := range m.nodes {
			if n.Online && !n.LastSeenAt.IsZero() && n.LastSeenAt.Before(cutoff) {
				n.Online = false
				m.nodes[id] = n
				offlined = append(offlined, id)
			}
		}
		m.mu.Unlock()
		if len(offlined) > 0 {
			m.saveState()
		}
		for _, id := range offlined {
			m.appendAudit("offline_ttl", id, nil)
		}
	}
}

func (m *Manager) appendAudit(event, nodeID string, data map[string]interface{}) {
	m.mu.RLock()
	path := m.auditPath
	m.mu.RUnlock()
	if strings.TrimSpace(path) == "" {
		return
	}
	_ = os.MkdirAll(filepath.Dir(path), 0755)
	row := map[string]interface{}{"time": time.Now().UTC().Format(time.RFC3339), "event": event, "node": nodeID}
	for k, v := range data {
		row[k] = v
	}
	b, _ := json.Marshal(row)
	f, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
	if err != nil {
		return
	}
	defer f.Close()
	_, _ = f.Write(append(b, '\n'))
}

func (m *Manager) saveState() {
	m.mu.RLock()
	path := m.statePath
	items := make([]NodeInfo, 0, len(m.nodes))
	for _, n := range m.nodes {
		items = append(items, n)
	}
	m.mu.RUnlock()
	if strings.TrimSpace(path) == "" {
		return
	}
	_ = os.MkdirAll(filepath.Dir(path), 0755)
	b, err := json.MarshalIndent(items, "", "  ")
	if err != nil {
		return
	}
	_ = os.WriteFile(path, b, 0644)
}

func (m *Manager) loadState() {
	m.mu.RLock()
	path := m.statePath
	m.mu.RUnlock()
	if strings.TrimSpace(path) == "" {
		return
	}
	b, err := os.ReadFile(path)
	if err != nil {
		return
	}
	var items []NodeInfo
	if err := json.Unmarshal(b, &items); err != nil {
		return
	}
	m.mu.Lock()
	for _, n := range items {
		if strings.TrimSpace(n.ID) == "" {
			continue
		}
		m.nodes[n.ID] = n
	}
	m.mu.Unlock()
}
