package backend

import (
	"encoding/base64"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"strings"

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

// handleAdminImageGenModels handles GET /api/admin/image-gen/models.
// Returns the list of supported Cloudflare AI image generation models.
func (s *Server) handleAdminImageGenModels(w http.ResponseWriter, r *http.Request) {
	if s.deps.ImageGen == nil {
		jsonError(w, http.StatusServiceUnavailable, "image generation not configured (CFAI_ACCOUNT_ID/CFAI_API_TOKEN missing)")
		return
	}
	models := s.deps.ImageGen.Models()
	writeJSON(w, 0, map[string]any{"models": models})
}

// imageGenRequest is the JSON body for POST /api/admin/image-gen.
type imageGenRequest struct {
	// Prompt is the text description of the desired image.
	Prompt string `json:"prompt"`

	// Model is the CF Workers AI model ID (e.g. "@cf/black-forest-labs/flux-2-dev").
	// Defaults to the recommended model for the given type.
	Model string `json:"model"`

	// Type is either "cover" or "chapter".
	Type string `json:"type"`

	// Slug is the book slug. Required for cover; required for chapter.
	Slug string `json:"slug"`

	// Chapter number (1-based). Required when type == "chapter".
	Chapter int `json:"chapter"`

	// ReferenceImageB64 is an optional base64-encoded PNG/JPEG reference image.
	// When present the img2img path is used.
	ReferenceImageB64 string `json:"reference_image_b64"`

	// NumSteps overrides inference steps (default 20).
	NumSteps int `json:"num_steps"`

	// Width / Height override output dimensions (0 = model default).
	Width  int `json:"width"`
	Height int `json:"height"`

	// Guidance overrides prompt guidance scale (0 = model default).
	Guidance float64 `json:"guidance"`

	// Strength for img2img: 0.0–1.0, default 0.75.
	Strength float64 `json:"strength"`

	// SaveToCover when true stores the result as the book cover in MinIO
	// (overwriting any existing cover) and sets the book's cover URL.
	// Only valid when type == "cover".
	SaveToCover bool `json:"save_to_cover"`
}

// imageGenResponse is the JSON body returned by POST /api/admin/image-gen.
type imageGenResponse struct {
	// ImageB64 is the generated image as a base64-encoded PNG string.
	ImageB64 string `json:"image_b64"`
	// ContentType is "image/png" or "image/jpeg".
	ContentType string `json:"content_type"`
	// Saved indicates whether the image was persisted to MinIO.
	Saved bool `json:"saved"`
	// CoverURL is the URL the cover is now served from (only set when Saved==true).
	CoverURL string `json:"cover_url,omitempty"`
	// Model is the model that was used.
	Model string `json:"model"`
	// Bytes is the raw image size in bytes.
	Bytes int `json:"bytes"`
}

// handleAdminImageGen handles POST /api/admin/image-gen.
//
// Generates an image using Cloudflare Workers AI and optionally stores it.
// Multipart/form-data is also accepted so the reference image can be uploaded
// directly; otherwise the reference is expected as base64 JSON.
func (s *Server) handleAdminImageGen(w http.ResponseWriter, r *http.Request) {
	if s.deps.ImageGen == nil {
		jsonError(w, http.StatusServiceUnavailable, "image generation not configured (CFAI_ACCOUNT_ID/CFAI_API_TOKEN missing)")
		return
	}

	var req imageGenRequest
	var refImageData []byte

	ct := r.Header.Get("Content-Type")
	if strings.HasPrefix(ct, "multipart/form-data") {
		// Multipart: parse JSON fields from a "json" part + optional "reference" file part.
		if err := r.ParseMultipartForm(32 << 20); err != nil {
			jsonError(w, http.StatusBadRequest, "parse multipart: "+err.Error())
			return
		}
		if jsonPart := r.FormValue("json"); jsonPart != "" {
			if err := json.Unmarshal([]byte(jsonPart), &req); err != nil {
				jsonError(w, http.StatusBadRequest, "parse json field: "+err.Error())
				return
			}
		}
		if f, _, err := r.FormFile("reference"); err == nil {
			defer f.Close()
			refImageData, _ = io.ReadAll(f)
		}
	} else {
		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
			jsonError(w, http.StatusBadRequest, "parse body: "+err.Error())
			return
		}
		if req.ReferenceImageB64 != "" {
			var decErr error
			refImageData, decErr = base64.StdEncoding.DecodeString(req.ReferenceImageB64)
			if decErr != nil {
				// Try std without padding
				refImageData, decErr = base64.RawStdEncoding.DecodeString(req.ReferenceImageB64)
				if decErr != nil {
					jsonError(w, http.StatusBadRequest, "decode reference_image_b64: "+decErr.Error())
					return
				}
			}
		}
	}

	if strings.TrimSpace(req.Prompt) == "" {
		jsonError(w, http.StatusBadRequest, "prompt is required")
		return
	}
	if req.Type != "cover" && req.Type != "chapter" {
		jsonError(w, http.StatusBadRequest, `type must be "cover" or "chapter"`)
		return
	}
	if req.Slug == "" {
		jsonError(w, http.StatusBadRequest, "slug is required")
		return
	}
	if req.Type == "chapter" && req.Chapter <= 0 {
		jsonError(w, http.StatusBadRequest, "chapter must be > 0 when type is chapter")
		return
	}

	// Resolve model
	model := cfai.ImageModel(req.Model)
	if model == "" {
		if req.Type == "cover" {
			model = cfai.DefaultImageModel
		} else {
			model = cfai.ImageModelFlux2Klein4B
		}
	}

	imgReq := cfai.ImageRequest{
		Prompt:   req.Prompt,
		Model:    model,
		NumSteps: req.NumSteps,
		Width:    req.Width,
		Height:   req.Height,
		Guidance: req.Guidance,
		Strength: req.Strength,
	}

	s.deps.Log.Info("admin: image gen requested",
		"type", req.Type, "slug", req.Slug, "chapter", req.Chapter,
		"model", model, "has_reference", len(refImageData) > 0)

	var imgData []byte
	var genErr error
	if len(refImageData) > 0 {
		imgData, genErr = s.deps.ImageGen.GenerateImageFromReference(r.Context(), imgReq, refImageData)
	} else {
		imgData, genErr = s.deps.ImageGen.GenerateImage(r.Context(), imgReq)
	}
	if genErr != nil {
		s.deps.Log.Error("admin: image gen failed", "err", genErr)
		jsonError(w, http.StatusBadGateway, "image generation failed: "+genErr.Error())
		return
	}

	contentType := sniffImageContentType(imgData)

	// ── Optional persistence ──────────────────────────────────────────────────
	var saved bool
	var coverURL string

	if req.SaveToCover && req.Type == "cover" && s.deps.CoverStore != nil {
		if err := s.deps.CoverStore.PutCover(r.Context(), req.Slug, imgData, contentType); err != nil {
			s.deps.Log.Error("admin: save generated cover failed", "slug", req.Slug, "err", err)
			// Non-fatal: still return the image
		} else {
			saved = true
			coverURL = fmt.Sprintf("/api/cover/novelfire.net/%s", req.Slug)
			s.deps.Log.Info("admin: generated cover saved", "slug", req.Slug, "bytes", len(imgData))
		}
	}

	// Encode result as base64
	b64 := base64.StdEncoding.EncodeToString(imgData)

	writeJSON(w, 0, imageGenResponse{
		ImageB64:    b64,
		ContentType: contentType,
		Saved:       saved,
		CoverURL:    coverURL,
		Model:       string(model),
		Bytes:       len(imgData),
	})
}

// saveCoverRequest is the JSON body for POST /api/admin/image-gen/save-cover.
type saveCoverRequest struct {
	// Slug is the book slug whose cover should be overwritten.
	Slug string `json:"slug"`
	// ImageB64 is the base64-encoded image bytes (PNG or JPEG).
	ImageB64 string `json:"image_b64"`
}

// handleAdminImageGenSaveCover handles POST /api/admin/image-gen/save-cover.
//
// Accepts a pre-generated image as base64 and stores it as the book cover in
// MinIO, replacing the existing one. Does not call Cloudflare AI at all.
func (s *Server) handleAdminImageGenSaveCover(w http.ResponseWriter, r *http.Request) {
	if s.deps.CoverStore == nil {
		jsonError(w, http.StatusServiceUnavailable, "cover store not configured")
		return
	}

	var req saveCoverRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		jsonError(w, http.StatusBadRequest, "parse body: "+err.Error())
		return
	}
	if req.Slug == "" {
		jsonError(w, http.StatusBadRequest, "slug is required")
		return
	}
	if req.ImageB64 == "" {
		jsonError(w, http.StatusBadRequest, "image_b64 is required")
		return
	}

	imgData, err := base64.StdEncoding.DecodeString(req.ImageB64)
	if err != nil {
		imgData, err = base64.RawStdEncoding.DecodeString(req.ImageB64)
		if err != nil {
			jsonError(w, http.StatusBadRequest, "decode image_b64: "+err.Error())
			return
		}
	}

	contentType := sniffImageContentType(imgData)
	if err := s.deps.CoverStore.PutCover(r.Context(), req.Slug, imgData, contentType); err != nil {
		s.deps.Log.Error("admin: save-cover failed", "slug", req.Slug, "err", err)
		jsonError(w, http.StatusInternalServerError, "save cover: "+err.Error())
		return
	}

	s.deps.Log.Info("admin: cover saved via image-gen", "slug", req.Slug, "bytes", len(imgData))
	writeJSON(w, 0, map[string]any{
		"saved":     true,
		"cover_url": fmt.Sprintf("/api/cover/novelfire.net/%s", req.Slug),
		"bytes":     len(imgData),
	})
}

// sniffImageContentType returns the MIME type of the image bytes.
func sniffImageContentType(data []byte) string {
	if len(data) >= 4 {
		// PNG: 0x89 P N G
		if data[0] == 0x89 && data[1] == 0x50 && data[2] == 0x4e && data[3] == 0x47 {
			return "image/png"
		}
		// JPEG: FF D8 FF
		if data[0] == 0xFF && data[1] == 0xD8 && data[2] == 0xFF {
			return "image/jpeg"
		}
		// WebP: RIFF....WEBP
		if len(data) >= 12 && data[0] == 'R' && data[1] == 'I' && data[2] == 'F' && data[3] == 'F' &&
			data[8] == 'W' && data[9] == 'E' && data[10] == 'B' && data[11] == 'P' {
			return "image/webp"
		}
	}
	return "image/png"
}
