package main

import (
	"context"
	"database/sql"
	"encoding/hex"
	"flag"
	"fmt"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/google/uuid"
	_ "github.com/lib/pq"

	router "github.com/rwadurian/mpc-system/api/grpc/router/v1"
	"github.com/rwadurian/mpc-system/pkg/config"
	"github.com/rwadurian/mpc-system/pkg/crypto"
	"github.com/rwadurian/mpc-system/pkg/logger"
	grpcclient "github.com/rwadurian/mpc-system/services/server-party/adapters/output/grpc"
	"github.com/rwadurian/mpc-system/services/server-party/adapters/output/postgres"
	"github.com/rwadurian/mpc-system/services/server-party/application/use_cases"
	"github.com/rwadurian/mpc-system/services/server-party/infrastructure/cache"
	"go.uber.org/zap"
)

// Global share cache for delegate parties
var globalShareCache *cache.ShareCache

func main() {
	// Parse flags
	configPath := flag.String("config", "", "Path to config file")
	flag.Parse()

	// Load configuration
	cfg, err := config.Load(*configPath)
	if err != nil {
		fmt.Printf("Failed to load config: %v\n", err)
		os.Exit(1)
	}

	// Initialize logger
	if err := logger.Init(&logger.Config{
		Level:    cfg.Logger.Level,
		Encoding: cfg.Logger.Encoding,
	}); err != nil {
		fmt.Printf("Failed to initialize logger: %v\n", err)
		os.Exit(1)
	}
	defer logger.Sync()

	logger.Info("Starting Server Party Service",
		zap.String("environment", cfg.Server.Environment),
		zap.Int("http_port", cfg.Server.HTTPPort))

	// Initialize share cache for delegate parties (15 minute TTL)
	globalShareCache = cache.NewShareCache(15 * time.Minute)
	logger.Info("Share cache initialized", zap.Duration("ttl", 15*time.Minute))

	// Initialize database connection
	db, err := initDatabase(cfg.Database)
	if err != nil {
		logger.Fatal("Failed to connect to database", zap.Error(err))
	}
	defer db.Close()

	// Initialize crypto service with master key from environment
	masterKeyHex := os.Getenv("MPC_CRYPTO_MASTER_KEY")
	if masterKeyHex == "" {
		masterKeyHex = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" // 64 hex chars = 32 bytes
	}
	masterKey, err := hex.DecodeString(masterKeyHex)
	if err != nil {
		logger.Fatal("Invalid master key format", zap.Error(err))
	}
	cryptoService, err := crypto.NewCryptoService(masterKey)
	if err != nil {
		logger.Fatal("Failed to create crypto service", zap.Error(err))
	}

	// Get Message Router address from environment
	// Server-parties ONLY connect to Message Router (not Session Coordinator)
	// Message Router proxies session operations to Session Coordinator
	routerAddr := os.Getenv("MESSAGE_ROUTER_ADDR")
	if routerAddr == "" {
		routerAddr = "localhost:9092"
	}

	// Initialize Message Router client (the only gRPC connection needed)
	messageRouter, err := grpcclient.NewMessageRouterClient(routerAddr)
	if err != nil {
		logger.Fatal("Failed to connect to message router", zap.Error(err))
	}
	defer messageRouter.Close()

	// Initialize repositories
	keyShareRepo := postgres.NewKeySharePostgresRepo(db)

	// Initialize use cases with Message Router client
	// Message Router handles both messaging AND session operations (proxied to coordinator)
	participateKeygenUC := use_cases.NewParticipateKeygenUseCase(
		keyShareRepo,
		messageRouter, // MessageRouterClient implements SessionCoordinatorClient interface
		messageRouter,
		cryptoService,
	)
	participateSigningUC := use_cases.NewParticipateSigningUseCase(
		keyShareRepo,
		messageRouter, // MessageRouterClient implements SessionCoordinatorClient interface
		messageRouter,
		cryptoService,
	)

	// Create shutdown context
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	// Get party ID from environment (use pod name in K8s, or hostname)
	partyID := os.Getenv("PARTY_ID")
	if partyID == "" {
		partyID, _ = os.Hostname()
		if partyID == "" {
			partyID = "server-party-" + uuid.New().String()[:8]
		}
	}

	// Get party role from environment (default: persistent)
	partyRole := os.Getenv("PARTY_ROLE")
	if partyRole == "" {
		partyRole = "persistent"
	}

	// Get optional notification channels from environment
	// If notification channels are set, party operates in OFFLINE mode (24h async)
	// If no notification channels, party operates in REAL-TIME mode (Message Router push)
	var notificationConfig *grpcclient.NotificationConfig
	partyEmail := os.Getenv("PARTY_NOTIFICATION_EMAIL")
	partyPhone := os.Getenv("PARTY_NOTIFICATION_PHONE")
	partyPushToken := os.Getenv("PARTY_NOTIFICATION_PUSH_TOKEN")

	if partyEmail != "" || partyPhone != "" || partyPushToken != "" {
		notificationConfig = &grpcclient.NotificationConfig{
			Email:     partyEmail,
			Phone:     partyPhone,
			PushToken: partyPushToken,
		}
		logger.Info("Party configured for OFFLINE mode (notification channels set)",
			zap.Bool("has_email", partyEmail != ""),
			zap.Bool("has_phone", partyPhone != ""),
			zap.Bool("has_push", partyPushToken != ""))
	} else {
		logger.Info("Party configured for REAL-TIME mode (no notification channels)")
	}

	// Register this party with Message Router
	// This should be called again whenever party configuration changes
	logger.Info("Registering party with Message Router",
		zap.String("party_id", partyID),
		zap.String("role", partyRole))

	if err := messageRouter.RegisterPartyWithNotification(ctx, partyID, partyRole, "1.0.0", notificationConfig); err != nil {
		logger.Fatal("Failed to register party", zap.Error(err))
	}

	// Start heartbeat to keep party registered and detect pending messages
	// Heartbeat interval: 30 seconds, callback for pending messages notification
	heartbeatCancel := messageRouter.StartHeartbeat(ctx, partyID, 30*time.Second, func(pendingCount int32) {
		if pendingCount > 0 {
			logger.Info("Pending messages detected via heartbeat",
				zap.String("party_id", partyID),
				zap.Int32("pending_count", pendingCount))
		}
	})
	defer heartbeatCancel()
	logger.Info("Heartbeat started", zap.String("party_id", partyID), zap.Duration("interval", 30*time.Second))

	// Subscribe to session events and handle them automatically
	logger.Info("Subscribing to session events", zap.String("party_id", partyID))

	eventHandler := createSessionEventHandler(
		ctx,
		partyID,
		participateKeygenUC,
		participateSigningUC,
	)

	if err := messageRouter.SubscribeSessionEvents(ctx, partyID, eventHandler); err != nil {
		logger.Fatal("Failed to subscribe to session events", zap.Error(err))
	}

	logger.Info("Party-driven architecture initialized successfully",
		zap.String("party_id", partyID),
		zap.String("role", partyRole))

	// Start HTTP server
	errChan := make(chan error, 1)
	go func() {
		if err := startHTTPServer(cfg, participateKeygenUC, participateSigningUC, keyShareRepo); err != nil {
			errChan <- fmt.Errorf("HTTP server error: %w", err)
		}
	}()

	// Wait for shutdown signal
	sigChan := make(chan os.Signal, 1)
	signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)

	select {
	case sig := <-sigChan:
		logger.Info("Received shutdown signal", zap.String("signal", sig.String()))
	case err := <-errChan:
		logger.Error("Server error", zap.Error(err))
	}

	// Graceful shutdown
	logger.Info("Shutting down...")
	cancel()

	time.Sleep(5 * time.Second)
	logger.Info("Shutdown complete")

	_ = ctx
}

func initDatabase(cfg config.DatabaseConfig) (*sql.DB, error) {
	const maxRetries = 10
	const retryDelay = 2 * time.Second

	var db *sql.DB
	var err error

	for i := 0; i < maxRetries; i++ {
		db, err = sql.Open("postgres", cfg.DSN())
		if err != nil {
			logger.Warn("Failed to open database connection, retrying...",
				zap.Int("attempt", i+1),
				zap.Int("max_retries", maxRetries),
				zap.Error(err))
			time.Sleep(retryDelay * time.Duration(i+1))
			continue
		}

		db.SetMaxOpenConns(cfg.MaxOpenConns)
		db.SetMaxIdleConns(cfg.MaxIdleConns)
		db.SetConnMaxLifetime(cfg.ConnMaxLife)

		// Test connection with Ping
		if err = db.Ping(); err != nil {
			logger.Warn("Failed to ping database, retrying...",
				zap.Int("attempt", i+1),
				zap.Int("max_retries", maxRetries),
				zap.Error(err))
			db.Close()
			time.Sleep(retryDelay * time.Duration(i+1))
			continue
		}

		// Verify database is actually usable with a simple query
		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		var result int
		err = db.QueryRowContext(ctx, "SELECT 1").Scan(&result)
		cancel()
		if err != nil {
			logger.Warn("Database ping succeeded but query failed, retrying...",
				zap.Int("attempt", i+1),
				zap.Int("max_retries", maxRetries),
				zap.Error(err))
			db.Close()
			time.Sleep(retryDelay * time.Duration(i+1))
			continue
		}

		logger.Info("Connected to PostgreSQL and verified connectivity",
			zap.Int("attempt", i+1))
		return db, nil
	}

	return nil, fmt.Errorf("failed to connect to database after %d retries: %w", maxRetries, err)
}

func startHTTPServer(
	cfg *config.Config,
	participateKeygenUC *use_cases.ParticipateKeygenUseCase,
	participateSigningUC *use_cases.ParticipateSigningUseCase,
	keyShareRepo *postgres.KeySharePostgresRepo,
) error {
	if cfg.Server.Environment == "production" {
		gin.SetMode(gin.ReleaseMode)
	}

	router := gin.New()
	router.Use(gin.Recovery())
	router.Use(gin.Logger())

	// Health check
	router.GET("/health", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{
			"status":  "healthy",
			"service": "server-party",
		})
	})

	// API routes
	api := router.Group("/api/v1")
	{
		// Keygen participation endpoint
		api.POST("/keygen/participate", func(c *gin.Context) {
			var req struct {
				SessionID string `json:"session_id" binding:"required"`
				PartyID   string `json:"party_id" binding:"required"`
				JoinToken string `json:"join_token" binding:"required"`
			}

			if err := c.ShouldBindJSON(&req); err != nil {
				c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
				return
			}

			sessionID, err := uuid.Parse(req.SessionID)
			if err != nil {
				c.JSON(http.StatusBadRequest, gin.H{"error": "invalid session_id format"})
				return
			}

			// Execute keygen participation asynchronously
			go func() {
				ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
				defer cancel()

				input := use_cases.ParticipateKeygenInput{
					SessionID: sessionID,
					PartyID:   req.PartyID,
					JoinToken: req.JoinToken,
				}

				output, err := participateKeygenUC.Execute(ctx, input)
				if err != nil {
					logger.Error("Keygen participation failed",
						zap.String("session_id", req.SessionID),
						zap.String("party_id", req.PartyID),
						zap.Error(err))
					return
				}

				logger.Info("Keygen participation completed",
					zap.String("session_id", req.SessionID),
					zap.String("party_id", req.PartyID),
					zap.Bool("success", output.Success))

				// If this is a delegate party and share is available, store in cache
				if output.ShareForUser != nil && len(output.ShareForUser) > 0 {
					globalShareCache.Store(
						sessionID,
						req.PartyID,
						output.ShareForUser,
						output.PublicKey,
					)
					logger.Info("Share stored in cache for user retrieval (delegate party)",
						zap.String("session_id", req.SessionID),
						zap.String("party_id", req.PartyID),
						zap.Int("share_size", len(output.ShareForUser)))
				}
			}()

			c.JSON(http.StatusAccepted, gin.H{
				"message":    "keygen participation initiated",
				"session_id": req.SessionID,
				"party_id":   req.PartyID,
			})
		})

		// Signing participation endpoint
		api.POST("/sign/participate", func(c *gin.Context) {
			var req struct {
				SessionID   string `json:"session_id" binding:"required"`
				PartyID     string `json:"party_id" binding:"required"`
				JoinToken   string `json:"join_token" binding:"required"`
				MessageHash string `json:"message_hash"`
			}

			if err := c.ShouldBindJSON(&req); err != nil {
				c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
				return
			}

			sessionID, err := uuid.Parse(req.SessionID)
			if err != nil {
				c.JSON(http.StatusBadRequest, gin.H{"error": "invalid session_id format"})
				return
			}

			// Parse message hash if provided
			var messageHash []byte
			if req.MessageHash != "" {
				messageHash, err = hex.DecodeString(req.MessageHash)
				if err != nil {
					c.JSON(http.StatusBadRequest, gin.H{"error": "invalid message_hash format (expected hex)"})
					return
				}
			}

			// Execute signing participation asynchronously
			go func() {
				ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
				defer cancel()

				input := use_cases.ParticipateSigningInput{
					SessionID:   sessionID,
					PartyID:     req.PartyID,
					JoinToken:   req.JoinToken,
					MessageHash: messageHash,
				}

				output, err := participateSigningUC.Execute(ctx, input)
				if err != nil {
					logger.Error("Signing participation failed",
						zap.String("session_id", req.SessionID),
						zap.String("party_id", req.PartyID),
						zap.Error(err))
					return
				}

				logger.Info("Signing participation completed",
					zap.String("session_id", req.SessionID),
					zap.String("party_id", req.PartyID),
					zap.Bool("success", output.Success),
					zap.Int("signature_len", len(output.Signature)))
			}()

			c.JSON(http.StatusAccepted, gin.H{
				"message":    "signing participation initiated",
				"session_id": req.SessionID,
				"party_id":   req.PartyID,
			})
		})

		// Get key shares for a party
		api.GET("/shares/:party_id", func(c *gin.Context) {
			partyID := c.Param("party_id")

			ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
			defer cancel()

			shares, err := keyShareRepo.ListByParty(ctx, partyID)
			if err != nil {
				c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to fetch shares"})
				return
			}

			// Return share metadata (not the actual encrypted data)
			shareInfos := make([]gin.H, len(shares))
			for i, share := range shares {
				shareInfos[i] = gin.H{
					"id":          share.ID.String(),
					"party_id":    share.PartyID,
					"party_index": share.PartyIndex,
					"public_key":  hex.EncodeToString(share.PublicKey),
					"created_at":  share.CreatedAt,
					"last_used":   share.LastUsedAt,
				}
			}

			c.JSON(http.StatusOK, gin.H{
				"party_id": partyID,
				"count":    len(shares),
				"shares":   shareInfos,
			})
		})

		// Get user share for delegate parties (one-time retrieval)
		// This endpoint is ONLY for delegate parties to return shares to users
		api.GET("/sessions/:session_id/user-share", func(c *gin.Context) {
			sessionIDStr := c.Param("session_id")

			// Check if this is a delegate party
			partyRole := os.Getenv("PARTY_ROLE")
			if partyRole != "delegate" {
				c.JSON(http.StatusForbidden, gin.H{
					"error": "This endpoint is only available for delegate parties",
					"role":  partyRole,
				})
				return
			}

			sessionID, err := uuid.Parse(sessionIDStr)
			if err != nil {
				c.JSON(http.StatusBadRequest, gin.H{
					"error": "invalid session_id format",
				})
				return
			}

			// Retrieve and delete share from cache (one-time retrieval)
			entry, exists := globalShareCache.GetAndDelete(sessionID)
			if !exists {
				c.JSON(http.StatusNotFound, gin.H{
					"error": "Share not found or already retrieved",
					"note":  "Shares can only be retrieved once and expire after 15 minutes",
				})
				return
			}

			logger.Info("User share retrieved successfully",
				zap.String("session_id", sessionIDStr),
				zap.String("party_id", entry.PartyID))

			c.JSON(http.StatusOK, gin.H{
				"session_id": sessionIDStr,
				"party_id":   entry.PartyID,
				"share":      hex.EncodeToString(entry.Share),
				"public_key": hex.EncodeToString(entry.PublicKey),
				"note":       "This share has been deleted from memory and cannot be retrieved again",
			})
		})
	}

	logger.Info("Starting HTTP server", zap.Int("port", cfg.Server.HTTPPort))
	return router.Run(fmt.Sprintf(":%d", cfg.Server.HTTPPort))
}

// createSessionEventHandler creates a handler for session events (party-driven architecture)
// Parties automatically respond to session creation events by joining keygen or signing sessions
func createSessionEventHandler(
	ctx context.Context,
	partyID string,
	participateKeygenUC *use_cases.ParticipateKeygenUseCase,
	participateSigningUC *use_cases.ParticipateSigningUseCase,
) func(*router.SessionEvent) {
	return func(event *router.SessionEvent) {
		// Check if this party is selected for the session
		isSelected := false
		for _, selectedParty := range event.SelectedParties {
			if selectedParty == partyID {
				isSelected = true
				break
			}
		}

		if !isSelected {
			logger.Debug("Party not selected for this session",
				zap.String("session_id", event.SessionId),
				zap.String("party_id", partyID))
			return
		}

		// Get join token for this party
		joinToken, exists := event.JoinTokens[partyID]
		if !exists {
			logger.Error("No join token found for party",
				zap.String("session_id", event.SessionId),
				zap.String("party_id", partyID))
			return
		}

		logger.Info("Party selected for session, auto-participating",
			zap.String("session_id", event.SessionId),
			zap.String("party_id", partyID),
			zap.String("event_type", event.EventType))

		// Parse session ID
		sessionID, err := uuid.Parse(event.SessionId)
		if err != nil {
			logger.Error("Invalid session ID", zap.Error(err))
			return
		}

		// Automatically participate based on session type
		go func() {
			// Use parent context to allow proper cancellation
			participateCtx, cancel := context.WithTimeout(ctx, 5*time.Minute)
			defer cancel()

			// Determine session type from event
			if event.EventType == "session_created" {
				// Check if it's keygen or sign based on message_hash
				if len(event.MessageHash) == 0 {
					// Keygen session
					logger.Info("Auto-participating in keygen session",
						zap.String("session_id", event.SessionId),
						zap.String("party_id", partyID))

					input := use_cases.ParticipateKeygenInput{
						SessionID: sessionID,
						PartyID:   partyID,
						JoinToken: joinToken,
					}

					result, err := participateKeygenUC.Execute(participateCtx, input)
					if err != nil {
						logger.Error("Keygen participation failed",
							zap.Error(err),
							zap.String("session_id", event.SessionId))
					} else {
						logger.Info("Keygen participation completed",
							zap.String("session_id", event.SessionId),
							zap.String("public_key", hex.EncodeToString(result.PublicKey)))
					}
				} else {
					// Sign session
					logger.Info("Auto-participating in sign session",
						zap.String("session_id", event.SessionId),
						zap.String("party_id", partyID))

					input := use_cases.ParticipateSigningInput{
						SessionID:   sessionID,
						PartyID:     partyID,
						JoinToken:   joinToken,
						MessageHash: event.MessageHash,
					}

					// If this party is the delegate party and user share is provided, use it
					if event.DelegateUserShare != nil && event.DelegateUserShare.DelegatePartyId == partyID {
						input.UserShareData = event.DelegateUserShare.EncryptedShare
						logger.Info("Using user-provided share for delegate party signing",
							zap.String("session_id", event.SessionId),
							zap.String("party_id", partyID),
							zap.Int32("party_index", event.DelegateUserShare.PartyIndex))
					}

					result, err := participateSigningUC.Execute(participateCtx, input)
					if err != nil {
						logger.Error("Signing participation failed",
							zap.Error(err),
							zap.String("session_id", event.SessionId))
					} else {
						logger.Info("Signing participation completed",
							zap.String("session_id", event.SessionId),
							zap.String("signature", hex.EncodeToString(result.Signature)))
					}
				}
			}
		}()
	}
}
