package backend

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"strings"
	"sync"
	"time"

	"github.com/libnovel/backend/internal/cfai"
	"github.com/libnovel/backend/internal/domain"
)

// ── Cancel registry ────────────────────────────────────────────────────────────
// cancelJobsMu guards cancelJobs.
var cancelJobsMu sync.Mutex

// cancelJobs maps a job ID to its CancelFunc. Entries are added when a batch
// job starts and removed when it finishes or is cancelled.
var cancelJobs = map[string]context.CancelFunc{}

func registerCancelJob(id string, cancel context.CancelFunc) {
	cancelJobsMu.Lock()
	cancelJobs[id] = cancel
	cancelJobsMu.Unlock()
}

func deregisterCancelJob(id string) {
	cancelJobsMu.Lock()
	delete(cancelJobs, id)
	cancelJobsMu.Unlock()
}

// ── AI Job list / get / cancel ─────────────────────────────────────────────────

// handleAdminListAIJobs handles GET /api/admin/ai-jobs.
// Returns all ai_job records sorted by started descending.
func (s *Server) handleAdminListAIJobs(w http.ResponseWriter, r *http.Request) {
	if s.deps.AIJobStore == nil {
		jsonError(w, http.StatusServiceUnavailable, "ai job store not configured")
		return
	}
	jobs, err := s.deps.AIJobStore.ListAIJobs(r.Context())
	if err != nil {
		s.deps.Log.Error("admin: list ai jobs failed", "err", err)
		jsonError(w, http.StatusInternalServerError, "list ai jobs: "+err.Error())
		return
	}
	writeJSON(w, 0, map[string]any{"jobs": jobs})
}

// handleAdminGetAIJob handles GET /api/admin/ai-jobs/{id}.
func (s *Server) handleAdminGetAIJob(w http.ResponseWriter, r *http.Request) {
	if s.deps.AIJobStore == nil {
		jsonError(w, http.StatusServiceUnavailable, "ai job store not configured")
		return
	}
	id := r.PathValue("id")
	job, ok, err := s.deps.AIJobStore.GetAIJob(r.Context(), id)
	if err != nil {
		jsonError(w, http.StatusInternalServerError, err.Error())
		return
	}
	if !ok {
		jsonError(w, http.StatusNotFound, fmt.Sprintf("job %q not found", id))
		return
	}
	writeJSON(w, 0, job)
}

// handleAdminCancelAIJob handles POST /api/admin/ai-jobs/{id}/cancel.
// Marks the job as cancelled in PB and cancels the in-memory context if present.
func (s *Server) handleAdminCancelAIJob(w http.ResponseWriter, r *http.Request) {
	if s.deps.AIJobStore == nil {
		jsonError(w, http.StatusServiceUnavailable, "ai job store not configured")
		return
	}
	id := r.PathValue("id")

	// Cancel in-memory context if the job is still running in this process.
	cancelJobsMu.Lock()
	if cancel, ok := cancelJobs[id]; ok {
		cancel()
	}
	cancelJobsMu.Unlock()

	// Mark as cancelled in PB.
	if err := s.deps.AIJobStore.UpdateAIJob(r.Context(), id, map[string]any{
		"status":   string(domain.TaskStatusCancelled),
		"finished": time.Now().Format(time.RFC3339),
	}); err != nil {
		s.deps.Log.Error("admin: cancel ai job failed", "id", id, "err", err)
		jsonError(w, http.StatusInternalServerError, "cancel ai job: "+err.Error())
		return
	}

	s.deps.Log.Info("admin: ai job cancelled", "id", id)
	writeJSON(w, 0, map[string]any{"cancelled": true})
}

// ── Auto-prompt ────────────────────────────────────────────────────────────────

// autoPromptRequest is the JSON body for POST /api/admin/image-gen/auto-prompt.
type autoPromptRequest struct {
	// Slug is the book slug.
	Slug string `json:"slug"`
	// Type is "cover" or "chapter".
	Type string `json:"type"`
	// Chapter number (required when type == "chapter").
	Chapter int `json:"chapter"`
	// Model is the text-gen model to use. Defaults to DefaultTextModel.
	Model string `json:"model"`
}

// autoPromptResponse is returned by POST /api/admin/image-gen/auto-prompt.
type autoPromptResponse struct {
	Prompt string `json:"prompt"`
	Model  string `json:"model"`
}

// handleAdminImageGenAutoPrompt handles POST /api/admin/image-gen/auto-prompt.
//
// Uses the text generation model to create a vivid image generation prompt
// based on the book's description (for covers) or chapter title/content (for chapters).
func (s *Server) handleAdminImageGenAutoPrompt(w http.ResponseWriter, r *http.Request) {
	if s.deps.TextGen == nil {
		jsonError(w, http.StatusServiceUnavailable, "text generation not configured")
		return
	}

	var req autoPromptRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		jsonError(w, http.StatusBadRequest, "parse body: "+err.Error())
		return
	}
	if strings.TrimSpace(req.Slug) == "" {
		jsonError(w, http.StatusBadRequest, "slug is required")
		return
	}
	if req.Type != "cover" && req.Type != "chapter" {
		jsonError(w, http.StatusBadRequest, `type must be "cover" or "chapter"`)
		return
	}

	meta, ok, err := s.deps.BookReader.ReadMetadata(r.Context(), req.Slug)
	if err != nil {
		jsonError(w, http.StatusInternalServerError, "read metadata: "+err.Error())
		return
	}
	if !ok {
		jsonError(w, http.StatusNotFound, fmt.Sprintf("book %q not found", req.Slug))
		return
	}

	model := req.Model
	if model == "" {
		model = string(cfai.DefaultTextModel)
	}

	var userPrompt string
	if req.Type == "cover" {
		userPrompt = fmt.Sprintf(
			"Book: \"%s\"\nAuthor: %s\nGenres: %s\n\nDescription:\n%s",
			meta.Title,
			meta.Author,
			strings.Join(meta.Genres, ", "),
			meta.Summary,
		)
	} else {
		// For chapter images, use chapter title if available.
		chapterTitle := fmt.Sprintf("Chapter %d", req.Chapter)
		if req.Chapter > 0 {
			chapters, listErr := s.deps.BookReader.ListChapters(r.Context(), req.Slug)
			if listErr == nil {
				for _, ch := range chapters {
					if ch.Number == req.Chapter {
						chapterTitle = ch.Title
						break
					}
				}
			}
		}
		userPrompt = fmt.Sprintf(
			"Book: \"%s\"\nGenres: %s\nChapter: %s\n\nBook description:\n%s",
			meta.Title,
			strings.Join(meta.Genres, ", "),
			chapterTitle,
			meta.Summary,
		)
	}

	systemPrompt := buildAutoPromptSystem(req.Type)

	s.deps.Log.Info("admin: image auto-prompt requested",
		"slug", req.Slug, "type", req.Type, "chapter", req.Chapter, "model", model)

	result, genErr := s.deps.TextGen.Generate(r.Context(), cfai.TextRequest{
		Model: cfai.TextModel(model),
		Messages: []cfai.TextMessage{
			{Role: "system", Content: systemPrompt},
			{Role: "user", Content: userPrompt},
		},
		MaxTokens: 256,
	})
	if genErr != nil {
		s.deps.Log.Error("admin: auto-prompt failed", "err", genErr)
		jsonError(w, http.StatusBadGateway, "text generation failed: "+genErr.Error())
		return
	}

	writeJSON(w, 0, autoPromptResponse{
		Prompt: strings.TrimSpace(result),
		Model:  model,
	})
}

func buildAutoPromptSystem(imageType string) string {
	if imageType == "cover" {
		return `You are a professional prompt engineer for AI image generation (Stable Diffusion / FLUX models). ` +
			`Given a book's title, genres, and description, write a single vivid image generation prompt ` +
			`for a book cover. The prompt should describe the visual composition, art style, lighting, ` +
			`and mood without mentioning text or typography. ` +
			`Format: comma-separated visual descriptors, 30–60 words. ` +
			`Output ONLY the prompt — no explanation, no quotes, no labels.`
	}
	return `You are a professional prompt engineer for AI image generation (Stable Diffusion / FLUX models). ` +
		`Given a book's title, genres, and a specific chapter title, write a single vivid scene illustration prompt. ` +
		`Describe the scene, characters, setting, lighting, and art style. ` +
		`Format: comma-separated visual descriptors, 30–60 words. ` +
		`Output ONLY the prompt — no explanation, no quotes, no labels.`
}
