package node

import (
	"context"
	"fmt"
	"sort"
	"strconv"
	"strings"
	"time"

	"go.uber.org/zap"

	"iop/apps/node/internal/transport"
	runtime "iop/packages/go/execution"
	iop "iop/proto/gen/iop"
)

// OnCommandRequest handles an incoming node command from a transport Session.
func (n *Node) OnCommandRequest(ctx context.Context, sess *transport.Session, req *iop.NodeCommandRequest) (*iop.NodeCommandResponse, error) {
	n.logger.Info("command request",
		zap.String("request_id", req.GetRequestId()),
		zap.String("type", req.GetType().String()),
		zap.String("adapter", req.GetAdapter()),
		zap.String("target", req.GetTarget()),
	)

	cmdType, ok := protoCommandTypeToDomain(req.GetType())
	if !ok {
		return n.commandErrorResponse(req, fmt.Sprintf("node: unsupported command type %q", req.GetType().String())), nil
	}

	execCtx := ctx
	if req.GetTimeoutSec() > 0 {
		var cancel context.CancelFunc
		execCtx, cancel = context.WithTimeout(ctx, time.Duration(req.GetTimeoutSec())*time.Second)
		defer cancel()
	}

	switch cmdType {
	case runtime.CommandTypeCapabilities:
		return n.handleCapabilitiesCommand(execCtx, sess, req), nil
	case runtime.CommandTypeTransportStatus:
		return n.handleTransportStatusCommand(sess, req), nil
	default:
		return n.dispatchAdapterCommand(execCtx, req), nil
	}
}

func (n *Node) handleCapabilitiesCommand(ctx context.Context, sess *transport.Session, req *iop.NodeCommandRequest) *iop.NodeCommandResponse {
	adapter, err := n.router.LookupAdapter(req.GetAdapter())
	if err != nil {
		return n.commandErrorResponse(req, fmt.Sprintf("node: %s", err.Error()))
	}
	caps, err := adapter.Capabilities(ctx)
	if err != nil {
		return n.commandErrorResponse(req, err.Error())
	}

	targets := append([]string(nil), caps.Targets...)
	// CAPABILITIES health is a real bounded exact-target probe, not the raw
	// Capabilities status and not an adapter-specific error mapping. ProbeHealth
	// validates the adapter type, instance key, and target and collapses every
	// inconclusive path to unknown. The request adapter key and target remain the
	// immutable Edge binding carried in the response envelope/result.
	healthEvidence := ProbeHealth(caps.AdapterName, caps.InstanceKey, req.GetTarget(), ResolveProbeFunc(adapter))
	providerStatus := runtime.ProviderStatusUnknown
	switch healthEvidence.Health {
	case runtime.RequestStalled:
		providerStatus = runtime.ProviderStatusAvailable
	case runtime.ProviderUnhealthy:
		providerStatus = runtime.ProviderStatusUnavailable
	}
	providerDetail := healthEvidence.Detail

	sort.Strings(targets)

	n.adapterGatesMu.Lock()
	gate, ok := n.adapterGates[req.GetAdapter()]
	n.adapterGatesMu.Unlock()
	inFlight := 0
	queued := 0
	if ok {
		inFlight = gate.activeCount()
	}

	result := map[string]string{
		"adapter":         caps.AdapterName,
		"adapter_key":     req.GetAdapter(),
		"instance_key":    caps.InstanceKey,
		"target":          req.GetTarget(),
		"targets":         strings.Join(targets, ","),
		"max_concurrency": strconv.Itoa(caps.MaxConcurrency),
		"provider_status": string(runtime.NormalizeProviderStatus(providerStatus)),
		"capacity":        strconv.Itoa(caps.MaxConcurrency),
		"in_flight":       strconv.Itoa(inFlight),
		"queued":          strconv.Itoa(queued),
	}
	if sess != nil {
		result["health_observation_seq"] = strconv.FormatUint(sess.NextHealthObservationSeq(), 10)
	}
	if providerDetail != "" {
		result["provider_detail"] = providerDetail
	}

	providerSnapshot := &iop.ProviderSnapshot{
		Adapter:  req.GetAdapter(),
		Status:   string(runtime.NormalizeProviderStatus(providerStatus)),
		Health:   string(runtime.NormalizeProviderStatus(providerStatus)),
		Capacity: int32(caps.MaxConcurrency),
		InFlight: int32(inFlight),
		Queued:   int32(queued),
	}

	return &iop.NodeCommandResponse{
		RequestId:         req.GetRequestId(),
		Type:              req.GetType(),
		Adapter:           req.GetAdapter(),
		Target:            req.GetTarget(),
		SessionId:         req.GetSessionId(),
		Result:            result,
		ProviderSnapshots: []*iop.ProviderSnapshot{providerSnapshot},
	}
}

func sessionConnected(sess *transport.Session) (alive bool) {
	if sess == nil {
		return false
	}
	defer func() {
		if r := recover(); r != nil {
			alive = false
		}
	}()
	return sess.IsAlive()
}

func (n *Node) handleTransportStatusCommand(sess *transport.Session, req *iop.NodeCommandRequest) *iop.NodeCommandResponse {
	connected := "false"
	state := "disconnected"
	if sessionConnected(sess) {
		connected = "true"
		state = "connected"
	}
	result := map[string]string{
		"node_id":    n.nodeID,
		"connected":  connected,
		"state":      state,
		"adapter":    req.GetAdapter(),
		"target":     req.GetTarget(),
		"session_id": normalizeSessionID(req.GetSessionId()),
	}
	return &iop.NodeCommandResponse{
		RequestId: req.GetRequestId(),
		Type:      req.GetType(),
		Adapter:   req.GetAdapter(),
		Target:    req.GetTarget(),
		SessionId: normalizeSessionID(req.GetSessionId()),
		Result:    result,
	}
}

func (n *Node) dispatchAdapterCommand(ctx context.Context, req *iop.NodeCommandRequest) *iop.NodeCommandResponse {
	adapter, err := n.router.LookupAdapter(req.GetAdapter())
	if err != nil {
		return n.commandErrorResponse(req, fmt.Sprintf("node: %s", err.Error()))
	}
	handler, ok := adapter.(runtime.CommandHandler)
	if !ok {
		return n.commandErrorResponse(req, fmt.Sprintf("node: adapter %q does not support commands", req.GetAdapter()))
	}
	domainReq := toDomainCommandRequest(req)
	domainResp, err := handler.HandleCommand(ctx, domainReq)
	if err != nil {
		return n.commandErrorResponse(req, err.Error())
	}
	return toProtoCommandResponse(domainResp)
}

func (n *Node) commandErrorResponse(req *iop.NodeCommandRequest, msg string) *iop.NodeCommandResponse {
	return &iop.NodeCommandResponse{
		RequestId: req.GetRequestId(),
		Type:      req.GetType(),
		Adapter:   req.GetAdapter(),
		Target:    req.GetTarget(),
		SessionId: req.GetSessionId(),
		Error:     msg,
	}
}

// protoCommandTypeToDomain maps a proto NodeCommandType to its runtime
// CommandType. The second return is false for UNSPECIFIED or unknown values
// so callers can reject unsupported commands before dispatching.
func protoCommandTypeToDomain(t iop.NodeCommandType) (runtime.CommandType, bool) {
	switch t {
	case iop.NodeCommandType_NODE_COMMAND_TYPE_CAPABILITIES:
		return runtime.CommandTypeCapabilities, true
	case iop.NodeCommandType_NODE_COMMAND_TYPE_TRANSPORT_STATUS:
		return runtime.CommandTypeTransportStatus, true
	case iop.NodeCommandType_NODE_COMMAND_TYPE_OLLAMA_API:
		return runtime.CommandTypeOllamaAPI, true
	default:
		return "", false
	}
}

func domainCommandTypeToProto(t runtime.CommandType) iop.NodeCommandType {
	switch t {
	case runtime.CommandTypeCapabilities:
		return iop.NodeCommandType_NODE_COMMAND_TYPE_CAPABILITIES
	case runtime.CommandTypeTransportStatus:
		return iop.NodeCommandType_NODE_COMMAND_TYPE_TRANSPORT_STATUS
	case runtime.CommandTypeOllamaAPI:
		return iop.NodeCommandType_NODE_COMMAND_TYPE_OLLAMA_API
	default:
		return iop.NodeCommandType_NODE_COMMAND_TYPE_UNSPECIFIED
	}
}

func toDomainCommandRequest(req *iop.NodeCommandRequest) runtime.CommandRequest {
	cmdType, _ := protoCommandTypeToDomain(req.GetType())
	return runtime.CommandRequest{
		RequestID:  req.GetRequestId(),
		Type:       cmdType,
		Adapter:    req.GetAdapter(),
		Target:     req.GetTarget(),
		SessionID:  normalizeSessionID(req.GetSessionId()),
		TimeoutSec: int(req.GetTimeoutSec()),
		Metadata:   req.GetMetadata(),
	}
}

func toProtoCommandResponse(resp runtime.CommandResponse) *iop.NodeCommandResponse {
	out := &iop.NodeCommandResponse{
		RequestId: resp.RequestID,
		Type:      domainCommandTypeToProto(resp.Type),
		Adapter:   resp.Adapter,
		Target:    resp.Target,
		SessionId: resp.SessionID,
		Result:    resp.Result,
	}
	return out
}
