package server

import (
	"fmt"
	"log"
	"time"

	"github.com/openp2p-cn/inp2p/pkg/auth"
	"github.com/openp2p-cn/inp2p/pkg/protocol"
)

// ConnectCoordinator handles the complete punch coordination flow:
//   1. Client A sends ConnectReq to server
//   2. Server looks up Client B
//   3. Server pushes PunchStart to BOTH A and B simultaneously
//   4. Both sides call punch.Connect() at the same time
//   5. Success/failure reported back via PunchResult

// HandleConnectReq processes a connection request from node A to node B.
func (s *Server) HandleConnectReq(from *NodeInfo, req protocol.ConnectReq) error {
	to := s.GetNodeForUser(req.To, from.Token)
	if to == nil || !to.IsOnline() {
		// Peer offline or not visible — respond with generic not found
		from.Conn.Write(protocol.MsgPush, protocol.SubPushConnectRsp, protocol.ConnectRsp{
			Error:  1,
			Detail: "node not found",
			From:   req.To,
			To:     req.From,
		})
		return &NodeOfflineError{Node: req.To}
	}

	log.Printf("[coord] %s → %s: coordinating punch", from.Name, to.Name)

	// Build punch parameters for both sides
	from.mu.RLock()
	fromParams := protocol.PunchParams{
		IP:      from.PublicIP,
		Port:    from.PublicPort,
		NATType: from.NATType,
		HasIPv4: from.HasIPv4,
		Token:   auth.GenTOTP(from.Token, time.Now().Unix()),
	}
	from.mu.RUnlock()

	to.mu.RLock()
	toParams := protocol.PunchParams{
		IP:      to.PublicIP,
		Port:    to.PublicPort,
		NATType: to.NATType,
		HasIPv4: to.HasIPv4,
		Token:   auth.GenTOTP(to.Token, time.Now().Unix()),
	}
	to.mu.RUnlock()

	// Check if punch is possible
	if !protocol.CanPunch(fromParams.NATType, toParams.NATType) {
		log.Printf("[coord] %s(%s) ↔ %s(%s): punch impossible, suggesting relay",
			from.Name, fromParams.NATType, to.Name, toParams.NATType)
		// Respond to A with B's info but mark that punch is unlikely
		from.Conn.Write(protocol.MsgPush, protocol.SubPushConnectRsp, protocol.ConnectRsp{
			Error:  0,
			From:   to.Name,
			To:     from.Name,
			Peer:   toParams,
			Detail: "punch-unlikely",
		})
		return nil
	}

	// Push PunchStart to BOTH sides simultaneously
	punchID := fmt.Sprintf("%s-%s-%d", from.Name, to.Name, time.Now().UnixMilli())

	// Tell B about A (so B starts punching toward A)
	punchToB := protocol.ConnectReq{
		From:     from.Name,
		To:       to.Name,
		FromIP:   from.PublicIP,
		Peer:     fromParams,
		AppName:  req.AppName,
		Protocol: req.Protocol,
		SrcPort:  req.SrcPort,
		DstHost:  req.DstHost,
		DstPort:  req.DstPort,
	}
	if err := to.Conn.Write(protocol.MsgPush, protocol.SubPushConnectReq, punchToB); err != nil {
		log.Printf("[coord] push to %s failed: %v", to.Name, err)
	}

	// Tell A about B (so A starts punching toward B)
	rspToA := protocol.ConnectRsp{
		Error: 0,
		From:  to.Name,
		To:    from.Name,
		Peer:  toParams,
	}
	if err := from.Conn.Write(protocol.MsgPush, protocol.SubPushConnectRsp, rspToA); err != nil {
		log.Printf("[coord] rsp to %s failed: %v", from.Name, err)
	}

	log.Printf("[coord] punch started: %s(%s:%s) ↔ %s(%s:%s) id=%s",
		from.Name, fromParams.IP, fromParams.NATType,
		to.Name, toParams.IP, toParams.NATType,
		punchID)

	return nil
}

// HandleEditApp pushes an app configuration to a node, triggering tunnel creation.
func (s *Server) HandleEditApp(nodeName string, app protocol.AppConfig) error {
	node := s.GetNode(nodeName)
	if node == nil || !node.IsOnline() {
		return &NodeOfflineError{Node: nodeName}
	}

	log.Printf("[coord] push EditApp to %s: %s (:%d → %s:%d)",
		nodeName, app.AppName, app.SrcPort, app.PeerNode, app.DstPort)

	return node.Conn.Write(protocol.MsgPush, protocol.SubPushEditApp, app)
}

// HandleDeleteApp pushes app deletion to a node.
func (s *Server) HandleDeleteApp(nodeName string, appName string) error {
	node := s.GetNode(nodeName)
	if node == nil || !node.IsOnline() {
		return &NodeOfflineError{Node: nodeName}
	}

	return node.Conn.Write(protocol.MsgPush, protocol.SubPushDeleteApp, struct {
		AppName string `json:"appName"`
	}{AppName: appName})
}

// HandleReportApps pushes a report-apps request to a node.
func (s *Server) HandleReportApps(nodeName string) error {
	node := s.GetNode(nodeName)
	if node == nil || !node.IsOnline() {
		return &NodeOfflineError{Node: nodeName}
	}

	return node.Conn.Write(protocol.MsgPush, protocol.SubPushReportApps, nil)
}
