// Package worker provides session-related HTTP handlers.
package worker

import (
	"context"
	"encoding/json"
	"net/http"
	"strconv"
	"strings"
	"time"

	"github.com/go-chi/chi/v5"
	"github.com/lukaszraczylo/claude-mnemonic/internal/privacy"
	"github.com/lukaszraczylo/claude-mnemonic/internal/worker/session"
	"github.com/lukaszraczylo/claude-mnemonic/pkg/models"
	"github.com/rs/zerolog/log"
)

// SessionInitRequest is the request body for session initialization.
type SessionInitRequest struct {
	ClaudeSessionID     string `json:"claudeSessionId"`
	Project             string `json:"project"`
	Prompt              string `json:"prompt"`
	MatchedObservations int    `json:"matchedObservations"`
}

// SessionInitResponse is the response for session initialization.
type SessionInitResponse struct {
	Reason       string `json:"reason,omitempty"`
	SessionDBID  int64  `json:"sessionDbId"`
	PromptNumber int    `json:"promptNumber"`
	Skipped      bool   `json:"skipped,omitempty"`
}

// DuplicatePromptWindowSeconds is the time window for detecting duplicate prompt submissions.
// If the same prompt text is seen within this window, it's considered a duplicate hook invocation.
const DuplicatePromptWindowSeconds = 10

// handleSessionInit handles session initialization from user-prompt hook.
// This handler is idempotent - duplicate requests within a short time window
// return the existing prompt data without creating duplicates.
func (s *Service) handleSessionInit(w http.ResponseWriter, r *http.Request) {
	var req SessionInitRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}
	// Reject requests from internal CLI calls (callClaudeCLI runs from /tmp with no session ID).
	// These are the memory extraction agent's own prompts, not real user prompts.
	if req.ClaudeSessionID == "" {
		log.Debug().Str("project", req.Project).Msg("Rejecting session init with empty claude session ID (internal call)")
		writeJSON(w, SessionInitResponse{
			Skipped: true,
			Reason:  "internal",
		})
		return
	}

	// Reject prompts that contain the internal system prompt (from SDK processor).
	// The SDK processor sets CLAUDE_MNEMONIC_INTERNAL=1 on the subprocess env,
	// but Claude Code does NOT propagate custom env vars to hook subprocesses,
	// so that guard fails and the system prompt leaks into the database as a
	// regular user prompt. This server-side check catches those cases reliably.
	if strings.Contains(req.Prompt, "You are a memory extraction agent for Claude Code sessions") {
		log.Debug().Str("project", req.Project).Msg("Rejecting session init with internal system prompt")
		writeJSON(w, SessionInitResponse{
			Skipped: true,
			Reason:  "internal",
		})
		return
	}
	// Privacy check
	if privacy.IsEntirelyPrivate(req.Prompt) {
		// Create session but skip processing
		sessionID, _ := s.sessionStore.CreateSDKSession(r.Context(), req.ClaudeSessionID, req.Project, "")
		promptNum, _ := s.sessionStore.IncrementPromptCounter(r.Context(), sessionID)

		writeJSON(w, SessionInitResponse{
			SessionDBID:  sessionID,
			PromptNumber: promptNum,
			Skipped:      true,
			Reason:       "private",
		})
		return
	}

	// Clean prompt
	cleanedPrompt := privacy.Clean(req.Prompt)

	// DUPLICATE DETECTION: Check if this exact prompt was already saved recently.
	// This prevents the bug where the hook fires multiple times for the same user action,
	// creating many duplicate prompts with incrementing numbers.
	if existingID, existingNum, found := s.promptStore.FindRecentPromptByText(r.Context(), req.ClaudeSessionID, cleanedPrompt, DuplicatePromptWindowSeconds); found {
		// Get or create session (idempotent)
		sessionID, _ := s.sessionStore.CreateSDKSession(r.Context(), req.ClaudeSessionID, req.Project, cleanedPrompt)

		log.Debug().
			Int64("sessionId", sessionID).
			Int("promptNumber", existingNum).
			Int64("promptId", existingID).
			Msg("Duplicate prompt detected - returning existing")

		// Return existing prompt data without incrementing or saving again
		writeJSON(w, SessionInitResponse{
			SessionDBID:  sessionID,
			PromptNumber: existingNum,
		})
		return
	}

	// CROSS-SESSION DUPLICATE DETECTION: Same prompt text from ANY session within the window.
	// This catches duplicates when the SDK processor's callClaudeCLI subprocess fires
	// UserPromptSubmit with a different Claude session ID than the original hook.
	if _, existingNum, found := s.promptStore.FindRecentPromptByTextGlobal(r.Context(), cleanedPrompt, DuplicatePromptWindowSeconds); found {
		sessionID, _ := s.sessionStore.CreateSDKSession(r.Context(), req.ClaudeSessionID, req.Project, cleanedPrompt)

		log.Debug().
			Int64("sessionId", sessionID).
			Int("promptNumber", existingNum).
			Msg("Cross-session duplicate prompt detected")

		writeJSON(w, SessionInitResponse{
			SessionDBID:  sessionID,
			PromptNumber: existingNum,
		})
		return
	}

	// Create session (idempotent)
	sessionID, err := s.sessionStore.CreateSDKSession(r.Context(), req.ClaudeSessionID, req.Project, cleanedPrompt)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	// Increment prompt counter
	promptNum, err := s.sessionStore.IncrementPromptCounter(r.Context(), sessionID)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	// Save user prompt with matched observation count
	promptID, err := s.promptStore.SaveUserPromptWithMatches(r.Context(), req.ClaudeSessionID, promptNum, cleanedPrompt, req.MatchedObservations)
	if err != nil {
		log.Warn().Err(err).Msg("Failed to save user prompt")
		// Non-fatal: continue with session initialization
	} else if s.vectorSync != nil {
		// Sync to vector DB asynchronously (non-blocking)
		now := time.Now()
		promptWithSession := &models.UserPromptWithSession{
			UserPrompt: models.UserPrompt{
				ID:                  promptID,
				ClaudeSessionID:     req.ClaudeSessionID,
				PromptNumber:        promptNum,
				PromptText:          cleanedPrompt,
				MatchedObservations: req.MatchedObservations,
				CreatedAt:           now.Format(time.RFC3339),
				CreatedAtEpoch:      now.UnixMilli(),
			},
			Project:      req.Project,
			SDKSessionID: req.ClaudeSessionID,
		}
		s.asyncVectorSync(func() {
			// Use service context as parent to respect shutdown signals
			ctx, cancel := context.WithTimeout(s.ctx, 10*time.Second)
			defer cancel()
			if err := s.vectorSync.SyncUserPrompt(ctx, promptWithSession); err != nil {
				if s.ctx.Err() == nil { // Don't log during shutdown
					log.Warn().Err(err).Int64("id", promptID).Msg("Failed to sync user prompt to sqlite-vec")
				}
			}
		})
	}

	log.Info().
		Int64("sessionId", sessionID).
		Int("promptNumber", promptNum).
		Str("project", req.Project).
		Msg("Session initialized")

	// Broadcast prompt event for dashboard refresh
	s.sseBroadcaster.Broadcast(map[string]any{
		"type":    "prompt",
		"action":  "created",
		"project": req.Project,
	})

	writeJSON(w, SessionInitResponse{
		SessionDBID:  sessionID,
		PromptNumber: promptNum,
	})
}

// SessionStartRequest is the request body for starting SDK agent.
type SessionStartRequest struct {
	UserPrompt   string `json:"userPrompt"`
	PromptNumber int    `json:"promptNumber"`
}

// handleSessionStart handles SDK agent session start.
func (s *Service) handleSessionStart(w http.ResponseWriter, r *http.Request) {
	idStr := chi.URLParam(r, "id")
	id, err := strconv.ParseInt(idStr, 10, 64)
	if err != nil {
		http.Error(w, "invalid session id", http.StatusBadRequest)
		return
	}

	var req SessionStartRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	// Initialize session in manager
	sess, err := s.sessionManager.InitializeSession(r.Context(), id, req.UserPrompt, req.PromptNumber)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	if sess == nil {
		http.Error(w, "session not found", http.StatusNotFound)
		return
	}

	// Session is now registered. Observations will be processed
	// asynchronously by the background queue processor (processQueue in service.go).
	log.Info().
		Int64("sessionId", id).
		Int("promptNumber", req.PromptNumber).
		Msg("SDK agent session initialized")

	s.broadcastProcessingStatus()
	w.WriteHeader(http.StatusOK)
}

// ObservationRequest is the request body for posting observations.
type ObservationRequest struct {
	ClaudeSessionID string `json:"claudeSessionId"`
	Project         string `json:"project"`
	ToolName        string `json:"tool_name"`
	ToolInput       any    `json:"tool_input"`
	ToolResponse    any    `json:"tool_response"`
	CWD             string `json:"cwd"`
}

// handleObservation handles observation posting from post-tool-use hook.
func (s *Service) handleObservation(w http.ResponseWriter, r *http.Request) {
	var req ObservationRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}
	// Reject requests from internal CLI calls (no session ID = internal callClaudeCLI)
	if req.ClaudeSessionID == "" {
		w.WriteHeader(http.StatusOK)
		return
	}


	// Find session
	sess, err := s.sessionStore.FindAnySDKSession(r.Context(), req.ClaudeSessionID)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	if sess == nil {
		// Create session on-the-fly with project from request
		id, err := s.sessionStore.CreateSDKSession(r.Context(), req.ClaudeSessionID, req.Project, "")
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
		sess, _ = s.sessionStore.GetSessionByID(r.Context(), id)
	}

	// Queue observation
	if err := s.sessionManager.QueueObservation(r.Context(), sess.ID, session.ObservationData{
		ToolName:     req.ToolName,
		ToolInput:    req.ToolInput,
		ToolResponse: req.ToolResponse,
		CWD:          req.CWD,
	}); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	s.broadcastProcessingStatus()
	w.WriteHeader(http.StatusOK)
}

// SubagentCompleteRequest is the request body for subagent completion.
type SubagentCompleteRequest struct {
	ClaudeSessionID string `json:"claudeSessionId"`
	Project         string `json:"project"`
}

// handleSubagentComplete handles subagent/Task completion notifications.
// This triggers immediate processing of any queued observations from the subagent.
func (s *Service) handleSubagentComplete(w http.ResponseWriter, r *http.Request) {
	var req SubagentCompleteRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	// Find session
	sess, err := s.sessionStore.FindAnySDKSession(r.Context(), req.ClaudeSessionID)
	if err != nil || sess == nil {
		// Session not found - subagent may have been in a different context
		log.Debug().
			Str("claudeSessionId", req.ClaudeSessionID).
			Msg("Subagent complete - no active session found")
		w.WriteHeader(http.StatusOK)
		return
	}

	// Trigger immediate processing of queued observations
	messages := s.sessionManager.DrainMessages(sess.ID)
	if len(messages) > 0 && s.processor != nil {
		log.Info().
			Int64("sessionId", sess.ID).
			Int("messages", len(messages)).
			Msg("Processing queued observations from subagent")

		for _, msg := range messages {
			if msg.Type == session.MessageTypeObservation && msg.Observation != nil {
				err := s.processor.ProcessObservation(
					r.Context(),
					sess.SDKSessionID.String,
					sess.Project,
					msg.Observation.ToolName,
					msg.Observation.ToolInput,
					msg.Observation.ToolResponse,
					msg.Observation.PromptNumber,
					msg.Observation.CWD,
				)
				if err != nil {
					log.Error().Err(err).
						Str("tool", msg.Observation.ToolName).
						Msg("Failed to process subagent observation")
				}
			}
		}
	}

	s.broadcastProcessingStatus()
	w.WriteHeader(http.StatusOK)
}

// handleGetSessionByClaudeID looks up a session by Claude session ID.
func (s *Service) handleGetSessionByClaudeID(w http.ResponseWriter, r *http.Request) {
	claudeSessionID := r.URL.Query().Get("claudeSessionId")
	if claudeSessionID == "" {
		http.Error(w, "claudeSessionId required", http.StatusBadRequest)
		return
	}

	session, err := s.sessionStore.FindAnySDKSession(r.Context(), claudeSessionID)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	if session == nil {
		http.Error(w, "session not found", http.StatusNotFound)
		return
	}

	writeJSON(w, session)
}

// SummarizeRequest is the request body for summarize requests.
type SummarizeRequest struct {
	LastUserMessage      string `json:"lastUserMessage"`
	LastAssistantMessage string `json:"lastAssistantMessage"`
}

// handleSummarize handles summarize requests from stop hook.
func (s *Service) handleSummarize(w http.ResponseWriter, r *http.Request) {
	idStr := chi.URLParam(r, "id")
	id, err := strconv.ParseInt(idStr, 10, 64)
	if err != nil {
		http.Error(w, "invalid session id", http.StatusBadRequest)
		return
	}

	var req SummarizeRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	// Queue summarize request
	if err := s.sessionManager.QueueSummarize(r.Context(), id, req.LastUserMessage, req.LastAssistantMessage); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	s.broadcastProcessingStatus()
	w.WriteHeader(http.StatusOK)
}
