package transport

import (
	toki "git.toki-labs.com/toki/proto-socket/go"
	"go.uber.org/zap"

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

// registerRunEventListener forwards node RunEvents to the current run-event
// handler after alias enrichment.
func (s *Server) registerRunEventListener(client *toki.TcpClient) {
	toki.AddListenerTyped[*iop.RunEvent](&client.Communicator, func(e *iop.RunEvent) {
		s.logger.Debug("run event received",
			zap.String("run_id", e.GetRunId()),
			zap.String("type", e.GetType()),
		)
		owner, ok := s.registry.CurrentOwnerForClient(client)
		s.enrichRunEvent(e)
		s.handlerMu.RLock()
		lifecycle := s.onRunLifecycle
		handler := s.onRunEvent
		s.handlerMu.RUnlock()
		// Correctness first: the lifecycle hook settles run accounting
		// synchronously with authoritative reception identity, then the event
		// goes out for observation. Publishing first would make a dropped fanout
		// lose the terminal signal. Stale or unregistered clients are dropped
		// before correctness callbacks.
		if ok && lifecycle != nil {
			lifecycle(owner.NodeID, owner.ConnectionGeneration, e)
		} else if !ok && lifecycle != nil {
			s.logger.Warn("stale or unregistered client run event dropped before lifecycle handler",
				zap.String("run_id", e.GetRunId()),
				zap.String("payload_node_id", e.GetNodeId()),
			)
		}
		if handler != nil {
			handler(e)
		}
	})
}

// registerTunnelFrameListener routes raw provider tunnel frames to the current
// tunnel handler, dropping them when none is registered or when the receiving client
// is stale/unregistered so they never reach correctness processing or the run event bus.
func (s *Server) registerTunnelFrameListener(client *toki.TcpClient) {
	toki.AddListenerTyped[*iop.ProviderTunnelFrame](&client.Communicator, func(f *iop.ProviderTunnelFrame) {
		owner, ok := s.registry.CurrentOwnerForClient(client)
		if !ok {
			s.logger.Warn("stale or unregistered provider tunnel frame dropped",
				zap.String("run_id", f.GetRunId()),
				zap.String("tunnel_id", f.GetTunnelId()),
			)
			return
		}
		s.handlerMu.RLock()
		handler := s.onTunnelFrame
		s.handlerMu.RUnlock()
		if handler == nil {
			s.logger.Warn("provider tunnel frame dropped: no tunnel handler",
				zap.String("run_id", f.GetRunId()),
				zap.String("tunnel_id", f.GetTunnelId()),
			)
			return
		}
		handler(owner.NodeID, owner.ConnectionGeneration, f)
	})
}

// registerNodeRequestListener installs the token-based RegisterRequest handler.
func (s *Server) registerNodeRequestListener(client *toki.TcpClient) {
	toki.AddRequestListenerTyped[*iop.RegisterRequest, *iop.RegisterResponse](
		&client.Communicator,
		func(req *iop.RegisterRequest) (*iop.RegisterResponse, error) {
			return s.handleRegisterRequest(client, req)
		},
	)
}

// handleRegisterRequest validates the node token, binds the registered-node
// listeners, and registers the node. The event/registry ordering is preserved:
// the node-event and disconnect listeners are bound before RegisterIfAbsent, and
// the connected event is emitted only after a successful registration.
func (s *Server) handleRegisterRequest(client *toki.TcpClient, req *iop.RegisterRequest) (*iop.RegisterResponse, error) {
	nodeStore := s.nodeStoreSnapshot()
	if nodeStore == nil {
		return &iop.RegisterResponse{Accepted: false, Reason: "edge node store unavailable"}, nil
	}
	rec, ok := nodeStore.FindByToken(req.GetToken())
	if !ok {
		s.logger.Warn("unknown token", zap.String("token_prefix", safePrefix(req.GetToken())))
		s.emitNodeEvent(events.NewEdgeNodeEvent(
			events.SourceEdge,
			events.TypeNodeRegistrationFailed,
			"",
			"",
			events.ReasonUnknownToken,
			map[string]string{
				events.MetadataFailureReason: events.ReasonUnknownToken,
				events.MetadataTokenPrefix:   safePrefix(req.GetToken()),
			},
		))
		return &iop.RegisterResponse{Accepted: false, Reason: "unknown token"}, nil
	}
	if s.requirePeerID {
		conn, found := s.peerConnection(client)
		identity, identityErr := auth.PeerWorkloadIdentity(conn)
		if !found || identityErr != nil || identity.Role != "node" || identity.Name != rec.ID {
			s.logger.Warn("node registration rejected: authenticated identity mismatch",
				zap.String("node_id", rec.ID),
			)
			return &iop.RegisterResponse{Accepted: false, Reason: "authenticated node identity mismatch"}, nil
		}
	}

	cfg, err := edgenode.BuildConfigPayload(rec)
	if err != nil {
		s.logger.Error("build config payload failed",
			zap.String("node_id", rec.ID),
			zap.Error(err),
		)
		return &iop.RegisterResponse{Accepted: false, Reason: "internal config error"}, nil
	}

	entry := &edgenode.NodeEntry{
		NodeID:                       rec.ID,
		Alias:                        rec.Alias,
		LifecycleState:               edgenode.LifecycleConnected,
		Client:                       client,
		Index:                        rec.Index,
		HasIndex:                     true,
		CredentialRecipientKeyID:     req.GetCredentialRecipientKeyId(),
		CredentialRecipientPublicKey: append([]byte(nil), req.GetCredentialRecipientPublicKey()...),
	}
	s.bindNodeEventListener(client, rec)
	s.bindDisconnectListener(client, rec)
	if !s.registry.RegisterIfAbsent(entry) {
		return s.rejectDuplicateRegistration(rec), nil
	}
	// Accepted registration claims ownership and delivers config only. It does NOT
	// open dispatch eligibility, pump stranded waiters, or emit the connected
	// event yet: the node has not applied this config or installed its message
	// handler, so any run/tunnel request sent now would be dropped on the node
	// side. The node applies the config from this response, installs its handler,
	// and then sends NodeReadyRequest — the authoritative connect lifecycle and
	// the connected event run there, exactly once, once dispatch is safe.
	s.logger.Info("node registration accepted, awaiting dispatch-ready",
		zap.String("node_id", rec.ID),
		zap.String("alias", rec.Alias),
		zap.Uint64("connection_generation", entry.ConnectionGeneration),
	)

	return &iop.RegisterResponse{
		Accepted: true,
		NodeId:   rec.ID,
		Alias:    rec.Alias,
		Config:   cfg,
	}, nil
}

// registerNodeReadyListener installs the NodeReadyRequest handler. The node sends
// this after applying its config and installing its message handler.
func (s *Server) registerNodeReadyListener(client *toki.TcpClient) {
	toki.AddRequestListenerTyped[*iop.NodeReadyRequest, *iop.NodeReadyResponse](
		&client.Communicator,
		func(req *iop.NodeReadyRequest) (*iop.NodeReadyResponse, error) {
			return s.handleReadyRequest(client, req), nil
		},
	)
}

// handleReadyRequest transitions the accepted connection identified by
// (req.NodeId, client) to dispatch-ready. Only the current owner of that id — the
// exact client that still holds the registry entry — can ready it, and only the
// first ready runs the authoritative connect lifecycle (which re-activates the
// node's provider resources and pumps its stranded waiters) and emits the
// connected event. A duplicate ready acks success without repeating either; a
// stale ready (superseded by a reconnect, or the entry already gone) is rejected
// so the node closes and reconnects instead of sitting idle believing it is live.
func (s *Server) handleReadyRequest(client *toki.TcpClient, req *iop.NodeReadyRequest) *iop.NodeReadyResponse {
	nodeID := req.GetNodeId()
	entry, transitioned, ok := s.registry.MarkDispatchReadyOwner(nodeID, client)
	if !ok {
		s.logger.Warn("stale node ready signal rejected", zap.String("node_id", nodeID))
		return &iop.NodeReadyResponse{Ready: false, Reason: "connection is not the current registry owner"}
	}
	if !transitioned {
		// Duplicate ready for an already-ready owner: idempotent success, no extra
		// pump or event.
		return &iop.NodeReadyResponse{Ready: true}
	}
	// First ready for this connection. Authoritative connected lifecycle first,
	// then the observability event.
	//
	// Both steps are fenced on this exact connection, but by two gates that share
	// the same generation, not one lock: the activation gate lives inside the
	// queue lock (HandleNodeConnect → activateNode re-checks the current owner
	// generation atomically with the resource mutation and pump, serialized against
	// a disconnect's releaseNode), and the event gate is WithCurrentOwner. They are
	// deliberately not wrapped in a single WithCurrentOwner: notifyNodeConnected
	// activates provider resources whose pump re-resolves candidates against the
	// registry (queue-lock → registry-lock order), so holding the registry lock
	// across it would invert that order and deadlock. Each gate independently makes
	// a stale (superseded or already-disconnected) generation a no-op, so a
	// reconnect that supersedes this connection between the two steps still yields
	// zero activation and zero connected event for the stale generation, and
	// exactly one of each for the current owner.
	s.notifyNodeConnected(nodeID, entry.ConnectionGeneration)
	s.registry.WithCurrentOwner(entry, func() {
		s.emitNodeReady(entry)
	})
	return &iop.NodeReadyResponse{Ready: true}
}

// rejectDuplicateRegistration logs and emits the duplicate-connection failure
// event and returns the rejection response.
func (s *Server) rejectDuplicateRegistration(rec *edgenode.NodeRecord) *iop.RegisterResponse {
	s.logger.Warn("duplicate registration rejected",
		zap.String("node_id", rec.ID),
	)
	s.emitNodeEvent(events.NewEdgeNodeEvent(
		events.SourceEdge,
		events.TypeNodeRegistrationFailed,
		rec.ID,
		rec.Alias,
		events.ReasonDuplicateConnection,
		map[string]string{events.MetadataFailureReason: events.ReasonDuplicateConnection},
	))
	return &iop.RegisterResponse{Accepted: false, Reason: "node already connected"}
}

// notifyNodeConnected invokes the authoritative connect handler for an accepted
// registration, ahead of the observability fanout. The handler read mirrors the
// disconnect path: it runs synchronously so a reconnect's resource activation
// and queue pump cannot be lost to a dropped bus delivery.
func (s *Server) notifyNodeConnected(nodeID string, generation uint64) {
	s.handlerMu.RLock()
	handler := s.onNodeConnect
	s.handlerMu.RUnlock()
	if handler != nil {
		handler(nodeID, generation)
	}
}

// emitNodeReady logs and emits the node-connected event once the node has
// signalled dispatch readiness. It is driven off the live registry entry so the
// announced identity is exactly the connection that became ready.
func (s *Server) emitNodeReady(entry *edgenode.NodeEntry) {
	s.logger.Info("node ready",
		zap.String("node_id", entry.NodeID),
		zap.String("alias", entry.Alias),
	)
	s.emitNodeEvent(events.NewEdgeNodeEvent(
		events.SourceEdge,
		events.TypeNodeConnected,
		entry.NodeID,
		entry.Alias,
		events.ReasonRegistered,
		map[string]string{events.MetadataLifecycleState: edgenode.LifecycleConnected},
	))
}

// bindNodeEventListener forwards node-originated EdgeNodeEvents, backfilling the
// node id/alias from the registered record when the node omitted them.
func (s *Server) bindNodeEventListener(client *toki.TcpClient, rec *edgenode.NodeRecord) {
	toki.AddListenerTyped[*iop.EdgeNodeEvent](&client.Communicator, func(e *iop.EdgeNodeEvent) {
		if e.NodeId == "" {
			e.NodeId = rec.ID
		}
		if e.Alias == "" {
			e.Alias = rec.Alias
		}
		s.emitNodeEvent(e)
	})
}

// bindDisconnectListener classifies the disconnect reason, removes the node from
// the registry, and — only when this client still owned the entry — settles
// lifecycle cleanup before emitting the disconnected event.
//
// The ownership check gates everything downstream: a stale duplicate connection
// closing must not release the live connection's leases, nor announce that the
// still-connected node went away.
func (s *Server) bindDisconnectListener(client *toki.TcpClient, rec *edgenode.NodeRecord) {
	client.AddDisconnectListener(func(_ *toki.TcpClient) {
		transportInfo := client.DisconnectInfo()
		fields := []zap.Field{zap.String("node_id", rec.ID)}
		fields = append(fields, transportDisconnectFields(transportInfo)...)

		// UnregisterIfClient is the ownership gate: it succeeds only for the client
		// that still owns the entry and returns that connection's generation. A
		// rejected duplicate or a connection already superseded by a reconnect gets
		// ok=false here, so nothing downstream — lifecycle callback, lease release,
		// or disconnected event — runs for a stale close.
		generation, ok := s.registry.UnregisterIfClient(rec.ID, client)
		if !ok {
			s.logger.Debug("stale node connection closed", fields...)
			return
		}
		fields = append(fields, zap.Uint64("connection_generation", generation))
		s.logger.Info("node unregistered", fields...)

		reason := events.ReasonTransportClosed
		if transportInfo.Reason == "heartbeat_timeout" {
			reason = events.ReasonHeartbeatTimeout
		} else if s.stopping.Load() {
			reason = events.ReasonEdgeShutdown
		}

		s.handlerMu.RLock()
		disconnectHandler := s.onNodeDisconnect
		s.handlerMu.RUnlock()
		if disconnectHandler != nil {
			disconnectHandler(rec.ID, generation, reason)
		}

		meta := transportDisconnectMetadata(transportInfo)
		if meta == nil {
			meta = make(map[string]string)
		}
		if reason == events.ReasonHeartbeatTimeout {
			meta[events.MetadataFailureReason] = events.ReasonHeartbeatTimeout
			meta[events.MetadataLifecycleState] = edgenode.LifecycleFailed
		} else {
			meta[events.MetadataLifecycleState] = edgenode.LifecycleFailed
		}
		s.emitNodeEvent(events.NewEdgeNodeEvent(
			events.SourceEdge,
			events.TypeNodeDisconnected,
			rec.ID,
			rec.Alias,
			reason,
			meta,
		))
	})
}
