package handlers

import (
	"bytes"
	"context"
	"crypto/ed25519"
	"crypto/subtle"
	"database/sql"
	"encoding/hex"
	"encoding/json"
	"io"
	"log"
	"net/http"
	"os"
	"regexp"
	"strings"

	"github.com/gin-gonic/gin"

	"github.com/Molecule-AI/molecule-monorepo/platform/internal/channels"
	"github.com/Molecule-AI/molecule-monorepo/platform/internal/db"
)

// ChannelHandler manages workspace social channel integrations.
type ChannelHandler struct {
	manager *channels.Manager
}

// NewChannelHandler creates a channel handler with the given manager.
func NewChannelHandler(manager *channels.Manager) *ChannelHandler {
	return &ChannelHandler{manager: manager}
}

// ListAdapters returns all available channel adapter types.
func (h *ChannelHandler) ListAdapters(c *gin.Context) {
	c.JSON(http.StatusOK, channels.ListAdapters())
}

// List returns all channels for a workspace.
func (h *ChannelHandler) List(c *gin.Context) {
	workspaceID := c.Param("id")
	ctx := c.Request.Context()

	rows, err := db.DB.QueryContext(ctx, `
		SELECT id, workspace_id, channel_type, channel_config, enabled, allowed_users,
		       last_message_at, message_count, created_at, updated_at
		FROM workspace_channels WHERE workspace_id = $1
		ORDER BY created_at
	`, workspaceID)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "query failed"})
		return
	}
	defer rows.Close()

	result := make([]map[string]interface{}, 0)
	for rows.Next() {
		var id, wsID, chType string
		var configJSON, allowedJSON []byte
		var enabled bool
		var lastMsg sql.NullTime
		var msgCount int
		var createdAt, updatedAt sql.NullTime

		if err := rows.Scan(&id, &wsID, &chType, &configJSON, &enabled, &allowedJSON, &lastMsg, &msgCount, &createdAt, &updatedAt); err != nil {
			continue
		}

		var config map[string]interface{}
		json.Unmarshal(configJSON, &config)
		// #319: decrypt sensitive fields first so the mask operates on
		// plaintext (first-4 / last-4 of the real token, not the ciphertext
		// prefix). Decrypt errors are logged but non-fatal — List must keep
		// returning the rest of the channel even if one field is corrupt.
		if err := channels.DecryptSensitiveFields(config); err != nil {
			log.Printf("Channels: decrypt config on list for channel %s: %v", id, err)
		}
		// Mask bot_token in list response
		if _, ok := config["bot_token"]; ok {
			token, _ := config["bot_token"].(string)
			if len(token) > 8 {
				config["bot_token"] = token[:4] + "..." + token[len(token)-4:]
			} else {
				config["bot_token"] = "***"
			}
		}

		var allowed []string
		json.Unmarshal(allowedJSON, &allowed)

		entry := map[string]interface{}{
			"id":            id,
			"workspace_id":  wsID,
			"channel_type":  chType,
			"config":        config,
			"enabled":       enabled,
			"allowed_users": allowed,
			"message_count": msgCount,
			"created_at":    createdAt.Time,
			"updated_at":    updatedAt.Time,
		}
		if lastMsg.Valid {
			entry["last_message_at"] = lastMsg.Time
		}
		result = append(result, entry)
	}

	c.JSON(http.StatusOK, result)
}

// Create adds a new channel to a workspace.
func (h *ChannelHandler) Create(c *gin.Context) {
	workspaceID := c.Param("id")
	ctx := c.Request.Context()

	var body struct {
		ChannelType  string                 `json:"channel_type"`
		Config       map[string]interface{} `json:"config"`
		AllowedUsers []string               `json:"allowed_users"`
		Enabled      *bool                  `json:"enabled"`
	}
	if err := c.ShouldBindJSON(&body); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid JSON"})
		return
	}

	if body.ChannelType == "" {
		c.JSON(http.StatusBadRequest, gin.H{"error": "channel_type is required"})
		return
	}

	adapter, ok := channels.GetAdapter(body.ChannelType)
	if !ok {
		c.JSON(http.StatusBadRequest, gin.H{"error": "unsupported channel_type: " + body.ChannelType})
		return
	}

	if err := adapter.ValidateConfig(body.Config); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid channel config"})
		return
	}

	// #319: encrypt sensitive fields (bot_token, webhook_secret) before
	// persisting so a DB read/backup leak can't recover the credentials.
	// Validation above ran against plaintext; storage is ciphertext.
	if err := channels.EncryptSensitiveFields(body.Config); err != nil {
		log.Printf("Channels: encrypt config failed for workspace %s: %v", workspaceID, err)
		c.JSON(http.StatusInternalServerError, gin.H{"error": "encrypt failed"})
		return
	}

	// #319: encrypt sensitive fields (bot_token, webhook_secret) before
	// persisting so a DB read/backup leak can't recover the credentials.
	// Validation above ran against plaintext; storage is ciphertext.
	if err := channels.EncryptSensitiveFields(body.Config); err != nil {
		log.Printf("Channels: encrypt config failed for workspace %s: %v", workspaceID, err)
		c.JSON(http.StatusInternalServerError, gin.H{"error": "encrypt failed"})
		return
	}

	configJSON, _ := json.Marshal(body.Config)
	allowedJSON, _ := json.Marshal(body.AllowedUsers)
	enabled := true
	if body.Enabled != nil {
		enabled = *body.Enabled
	}

	var id string
	err := db.DB.QueryRowContext(ctx, `
		INSERT INTO workspace_channels (workspace_id, channel_type, channel_config, enabled, allowed_users)
		VALUES ($1, $2, $3::jsonb, $4, $5::jsonb)
		RETURNING id
	`, workspaceID, body.ChannelType, string(configJSON), enabled, string(allowedJSON)).Scan(&id)
	if err != nil {
		log.Printf("Channels: create failed for workspace %s: %v", workspaceID, err)
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create channel"})
		return
	}

	// Hot reload
	h.manager.Reload(ctx)

	c.JSON(http.StatusCreated, gin.H{
		"id":           id,
		"channel_type": body.ChannelType,
		"enabled":      enabled,
	})
}

// Update modifies a channel's config, allowlist, or enabled state.
func (h *ChannelHandler) Update(c *gin.Context) {
	workspaceID := c.Param("id")
	channelID := c.Param("channelId")
	ctx := c.Request.Context()

	var body struct {
		Config       map[string]interface{} `json:"config"`
		AllowedUsers []string               `json:"allowed_users"`
		Enabled      *bool                  `json:"enabled"`
	}
	if err := c.ShouldBindJSON(&body); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid JSON"})
		return
	}

	// COALESCE-based update
	var configArg, allowedArg interface{}
	if body.Config != nil {
		// #319: re-encrypt sensitive fields on every config update — the
		// PATCH body carries plaintext (client already had them plaintext in
		// List response's unmasked path or typed fresh).
		if err := channels.EncryptSensitiveFields(body.Config); err != nil {
			log.Printf("Channels: encrypt update for workspace %s: %v", workspaceID, err)
			c.JSON(http.StatusInternalServerError, gin.H{"error": "encrypt failed"})
			return
		}
		j, _ := json.Marshal(body.Config)
		configArg = string(j)
	}
	if body.AllowedUsers != nil {
		j, _ := json.Marshal(body.AllowedUsers)
		allowedArg = string(j)
	}

	result, err := db.DB.ExecContext(ctx, `
		UPDATE workspace_channels
		SET channel_config = COALESCE($3::jsonb, channel_config),
		    allowed_users = COALESCE($4::jsonb, allowed_users),
		    enabled = COALESCE($5, enabled),
		    updated_at = now()
		WHERE id = $1 AND workspace_id = $2
	`, channelID, workspaceID, configArg, allowedArg, body.Enabled)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "update failed"})
		return
	}

	if n, _ := result.RowsAffected(); n == 0 {
		c.JSON(http.StatusNotFound, gin.H{"error": "channel not found"})
		return
	}

	// Hot reload
	h.manager.Reload(ctx)

	c.JSON(http.StatusOK, gin.H{"status": "updated"})
}

// Delete removes a channel from a workspace.
func (h *ChannelHandler) Delete(c *gin.Context) {
	workspaceID := c.Param("id")
	channelID := c.Param("channelId")
	ctx := c.Request.Context()

	result, err := db.DB.ExecContext(ctx, `
		DELETE FROM workspace_channels WHERE id = $1 AND workspace_id = $2
	`, channelID, workspaceID)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "delete failed"})
		return
	}

	if n, _ := result.RowsAffected(); n == 0 {
		c.JSON(http.StatusNotFound, gin.H{"error": "channel not found"})
		return
	}

	// Hot reload
	h.manager.Reload(ctx)

	c.JSON(http.StatusOK, gin.H{"status": "deleted"})
}

// Send sends an outbound message from a workspace to its social channel.
func (h *ChannelHandler) Send(c *gin.Context) {
	channelID := c.Param("channelId")
	ctx := c.Request.Context()

	var body struct {
		Text string `json:"text"`
	}
	if err := c.ShouldBindJSON(&body); err != nil || body.Text == "" {
		c.JSON(http.StatusBadRequest, gin.H{"error": "text is required"})
		return
	}

	// Per-channel budget enforcement (#368).
	// Reads message_count and channel_budget in one query. If channel_budget IS
	// NOT NULL and message_count has already reached it, reject with 429.
	// DB errors are logged and treated as fail-open (budget not enforced) so a
	// transient DB hiccup doesn't silently block outbound messages.
	var msgCount int
	var budget sql.NullInt64
	if err := db.DB.QueryRowContext(ctx,
		`SELECT message_count, channel_budget FROM workspace_channels WHERE id = $1`,
		channelID,
	).Scan(&msgCount, &budget); err != nil && err != sql.ErrNoRows {
		log.Printf("Channels: budget check failed for channel %s: %v", channelID, err)
	}
	if budget.Valid && int64(msgCount) >= budget.Int64 {
		c.JSON(http.StatusTooManyRequests, gin.H{"error": "channel budget exceeded"})
		return
	}

	if err := h.manager.SendOutbound(ctx, channelID, body.Text); err != nil {
		log.Printf("Channels: send outbound failed for channel %s: %v", channelID, err)
		c.JSON(http.StatusInternalServerError, gin.H{"error": "send failed"})
		return
	}

	c.JSON(http.StatusOK, gin.H{"status": "sent"})
}

// Test sends a test message to verify the channel is working.
func (h *ChannelHandler) Test(c *gin.Context) {
	channelID := c.Param("channelId")
	ctx := c.Request.Context()

	if err := h.manager.SendOutbound(ctx, channelID, "🔔 Molecule AI channel test — connection successful!"); err != nil {
		log.Printf("Channels: test message failed for channel %s: %v", channelID, err)
		c.JSON(http.StatusInternalServerError, gin.H{"error": "test message failed"})
		return
	}

	c.JSON(http.StatusOK, gin.H{"status": "ok", "message": "test message sent"})
}

// matchesChatID returns true if chatID is in the channel's comma-separated chat_id list.
// Exact match — avoids the substring-match bug of SQL LIKE.
func matchesChatID(config map[string]interface{}, chatID string) bool {
	raw, _ := config["chat_id"].(string)
	if raw == "" {
		return false
	}
	for _, s := range strings.Split(raw, ",") {
		if strings.TrimSpace(s) == chatID {
			return true
		}
	}
	return false
}

// Discover auto-detects chats/groups a bot has been added to by calling the platform API.
// User flow: enter bot token → add bot to groups → send a message → click Detect → select groups.
func (h *ChannelHandler) Discover(c *gin.Context) {
	var body struct {
		ChannelType string `json:"channel_type"`
		BotToken    string `json:"bot_token"`
		WorkspaceID string `json:"workspace_id"`
	}
	if err := c.ShouldBindJSON(&body); err != nil || body.BotToken == "" {
		c.JSON(http.StatusBadRequest, gin.H{"error": "bot_token is required"})
		return
	}
	// #329: workspace_id is required so PausePollersForToken can scope the
	// decryption lookup to the caller's tenant. Legacy clients that omit
	// the field still work — they just won't be able to pause a previously-
	// saved poller sharing the same token, which fails loudly at Telegram
	// with a 409 Conflict rather than silently decrypting every tenant's
	// token.
	if body.WorkspaceID == "" {
		c.JSON(http.StatusBadRequest, gin.H{"error": "workspace_id is required"})
		return
	}

	adapter, ok := channels.GetAdapter(body.ChannelType)
	if !ok {
		c.JSON(http.StatusBadRequest, gin.H{"error": "unsupported channel_type"})
		return
	}

	// Only Telegram supports discovery currently
	tg, ok := adapter.(*channels.TelegramAdapter)
	if !ok {
		c.JSON(http.StatusBadRequest, gin.H{"error": "discovery not supported for " + body.ChannelType})
		return
	}

	// Pause any active poller in THIS workspace using this bot token to
	// avoid Telegram's "only one getUpdates at a time" 409 Conflict.
	// #329: scoped to workspace_id so we never decrypt other tenants' tokens.
	resumeFn := h.manager.PausePollersForToken(body.WorkspaceID, body.BotToken)
	defer resumeFn()

	result, err := tg.DiscoverChats(c.Request.Context(), body.BotToken)
	if err != nil {
		// Map known errors to user-friendly messages
		msg := err.Error()
		userMsg := "Failed to connect to Telegram. Check your bot token and try again."
		if strings.Contains(msg, "invalid bot token") || strings.Contains(msg, "Unauthorized") || strings.Contains(msg, "Not Found") {
			userMsg = "Invalid bot token. Check the token from @BotFather and try again."
		} else if strings.Contains(msg, "Conflict") || strings.Contains(msg, "terminated by other") {
			userMsg = "This bot is already connected to another channel. Disconnect the existing channel first, or wait 30 seconds and retry."
		} else if strings.Contains(msg, "no route to host") || strings.Contains(msg, "i/o timeout") {
			userMsg = "Cannot reach Telegram API. Check your network connection and try again."
		}
		log.Printf("Channels: discover error: %v", err)
		c.JSON(http.StatusBadRequest, gin.H{"error": userMsg})
		return
	}

	hint := "For groups: add bot and send a message. For DMs: send /start to the bot. Then retry."
	var warning string
	if !result.CanReadAllGroupMessages {
		warning = "⚠️ Group privacy mode is ON for this bot — it only sees commands and @mentions in groups. " +
			"To let it see all group messages: open @BotFather → /mybots → " + result.BotUsername +
			" → Bot Settings → Group Privacy → Turn off, then re-add the bot to the group."
	}

	c.JSON(http.StatusOK, gin.H{
		"bot_username":                  result.BotUsername,
		"chats":                         result.Chats,
		"can_read_all_group_messages":   result.CanReadAllGroupMessages,
		"privacy_warning":               warning,
		"hint":                          hint,
	})
}

// Webhook handles incoming webhooks from any social platform.
func (h *ChannelHandler) Webhook(c *gin.Context) {
	channelType := c.Param("type")
	ctx := c.Request.Context()

	adapter, ok := channels.GetAdapter(channelType)
	if !ok {
		c.JSON(http.StatusNotFound, gin.H{"error": "unknown channel type"})
		return
	}

	// Discord: verify Ed25519 signature BEFORE the body is consumed by ParseWebhook.
	// The app_public_key is the Discord application's public key (not a secret —
	// it's a PUBLIC key and therefore stored in plaintext in channel_config).
	// We look it up from the DB (first enabled Discord channel with the field set)
	// and fall back to the DISCORD_APP_PUBLIC_KEY env var for self-hosted setups
	// that prefer global configuration. Fail closed: no key configured → 401.
	// verifyDiscordSignature restores r.Body after reading so ParseWebhook below
	// can still read the payload.
	if channelType == "discord" {
		pubKey := discordPublicKey(ctx)
		if pubKey == "" || !verifyDiscordSignature(c.Request, pubKey) {
			c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid signature"})
			return
		}
	}

	// For webhooks, we need to find the channel by type and match by chat_id in the message
	// Parse the webhook first to get the chat_id
	msg, err := adapter.ParseWebhook(c, nil)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "webhook parse failed"})
		return
	}
	if msg == nil {
		c.JSON(http.StatusOK, gin.H{"status": "ignored"}) // Non-message update
		return
	}

	// [slug] routing: if the message starts with [word], extract it as
	// a target agent slug and match against the channel config's username
	// field (lowercased). This lets humans type "[backend] what's #800?"
	// in a shared channel and route to a specific agent.
	targetSlug := ""
	routedText := msg.Text
	validSlugRe := regexp.MustCompile(`^[a-zA-Z0-9 _-]+$`)
	if len(msg.Text) > 2 && msg.Text[0] == '[' {
		if idx := strings.Index(msg.Text, "]"); idx > 1 && idx < 40 {
			candidate := strings.ToLower(strings.TrimSpace(msg.Text[1:idx]))
			if validSlugRe.MatchString(candidate) {
				targetSlug = candidate
				routedText = strings.TrimSpace(msg.Text[idx+1:])
				if routedText == "" {
					routedText = msg.Text
				}
			}
		}
	}

	// Look up channels by type and find one whose chat_id list contains msg.ChatID.
	rows, err := db.DB.QueryContext(ctx, `
		SELECT id, workspace_id, channel_type, channel_config, enabled, allowed_users
		FROM workspace_channels
		WHERE channel_type = $1 AND enabled = true
	`, channelType)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "channel lookup failed"})
		return
	}
	defer rows.Close()

	var ch channels.ChannelRow
	var candidates []channels.ChannelRow
	found := false
	for rows.Next() {
		var row channels.ChannelRow
		var configJSON, allowedJSON []byte
		if err := rows.Scan(&row.ID, &row.WorkspaceID, &row.ChannelType, &configJSON, &row.Enabled, &allowedJSON); err != nil {
			continue
		}
		json.Unmarshal(configJSON, &row.Config)
		json.Unmarshal(allowedJSON, &row.AllowedUsers)
		if err := channels.DecryptSensitiveFields(row.Config); err != nil {
			log.Printf("Channels: decrypt webhook row %s: %v", row.ID, err)
			continue
		}

		if expectedSecret, _ := row.Config["webhook_secret"].(string); expectedSecret != "" {
			receivedSecret := c.GetHeader("X-Telegram-Bot-Api-Secret-Token")
			if subtle.ConstantTimeCompare([]byte(receivedSecret), []byte(expectedSecret)) != 1 {
				continue
			}
		}

		if matchesChatID(row.Config, msg.ChatID) {
			candidates = append(candidates, row)
		}
	}

	if targetSlug != "" {
		// [slug] routing — match against config username (lowercased)
		for _, row := range candidates {
			username, _ := row.Config["username"].(string)
			usernameLC := strings.ToLower(username)
			// Match: [backend] → "Backend Engineer", [pm] → "PM", [dev lead] → "Dev Lead"
			if usernameLC == targetSlug ||
				strings.HasPrefix(strings.ReplaceAll(usernameLC, " ", "-"), targetSlug) ||
				strings.HasPrefix(strings.ReplaceAll(usernameLC, " ", ""), targetSlug) {
				ch = row
				found = true
				msg.Text = routedText // Strip the [slug] prefix before routing
				break
			}
		}
		if !found {
			// No match for slug — respond with available agents
			var names []string
			for _, row := range candidates {
				if u, _ := row.Config["username"].(string); u != "" {
					names = append(names, "["+strings.ToLower(strings.ReplaceAll(u, " ", "-"))+"]")
				}
			}
			c.JSON(http.StatusOK, gin.H{
				"status":          "unknown_agent",
				"requested_slug":  targetSlug,
				"available_slugs": names,
			})
			return
		}
	} else if len(candidates) > 0 {
		// No [slug] prefix — route to first matching channel (backward compat)
		ch = candidates[0]
		found = true
	}

	if !found {
		c.JSON(http.StatusOK, gin.H{"status": "no_channel"})
		return
	}

	// Process asynchronously — don't block the webhook response
	go func() {
		bgCtx := context.Background()
		if err := h.manager.HandleInbound(bgCtx, ch, msg); err != nil {
			log.Printf("Channels: async HandleInbound error for workspace %s: %v", ch.WorkspaceID[:12], err)
		}
	}()

	c.JSON(http.StatusOK, gin.H{"status": "accepted"})
}

// discordPublicKey returns the Ed25519 public key to use for Discord request
// signature verification. It queries the DB for the first enabled Discord
// channel whose config contains a non-empty app_public_key (stored in
// plaintext — it is a PUBLIC key and is not in the sensitiveFields list),
// then falls back to the DISCORD_APP_PUBLIC_KEY environment variable.
//
// Returns "" when no key is configured, which causes the caller to reject
// the incoming request with 401 (fail-closed behaviour).
func discordPublicKey(ctx context.Context) string {
	var pubKey string
	row := db.DB.QueryRowContext(ctx, `
		SELECT COALESCE(channel_config->>'app_public_key', '')
		FROM workspace_channels
		WHERE channel_type = 'discord' AND enabled = true
		  AND channel_config->>'app_public_key' IS NOT NULL
		  AND channel_config->>'app_public_key' != ''
		LIMIT 1
	`)
	_ = row.Scan(&pubKey)
	if pubKey != "" {
		return pubKey
	}
	return os.Getenv("DISCORD_APP_PUBLIC_KEY")
}

// verifyDiscordSignature verifies a Discord Interactions request using the
// Ed25519 signature scheme described in Discord's Interactions documentation.
// Discord signs the concatenation of the X-Signature-Timestamp header and the
// raw request body with the application's private key; we verify with the
// public key stored in channel_config or DISCORD_APP_PUBLIC_KEY.
//
// The function reads r.Body in full and then replaces it with a bytes.Reader
// over the same bytes so that subsequent callers (adapter.ParseWebhook) can
// still read the body.
//
// Returns false when any required header is missing, when pubKeyHex cannot
// be hex-decoded to a 32-byte Ed25519 public key, when the signature header
// cannot be decoded, or when the Ed25519 verification itself fails.
func verifyDiscordSignature(r *http.Request, pubKeyHex string) bool {
	sig := r.Header.Get("X-Signature-Ed25519")
	ts := r.Header.Get("X-Signature-Timestamp")
	if sig == "" || ts == "" || pubKeyHex == "" {
		return false
	}

	pubKeyBytes, err := hex.DecodeString(pubKeyHex)
	if err != nil || len(pubKeyBytes) != ed25519.PublicKeySize {
		return false
	}

	body, err := io.ReadAll(r.Body)
	if err != nil {
		return false
	}
	// Restore body so adapter.ParseWebhook can read it.
	r.Body = io.NopCloser(bytes.NewReader(body))

	sigBytes, err := hex.DecodeString(sig)
	if err != nil {
		return false
	}

	msg := append([]byte(ts), body...)
	return ed25519.Verify(pubKeyBytes, msg, sigBytes)
}
