package handlers

import (
	"context"
	"database/sql"
	"encoding/json"
	"fmt"
	"io"
	"log/slog"
	"net/http"
	"strings"
	"time"

	"lookbook/internal/data/item"
	"lookbook/internal/data/media"
	"lookbook/internal/data/tag"
	"lookbook/internal/embed"
	"lookbook/internal/opengraph"
	"lookbook/internal/video"
)

// strOrNil returns nil if s is empty, otherwise a pointer to s.
func strOrNil(s string) *string {
	if s == "" {
		return nil
	}
	return &s
}

// nullStr creates a sql.Null[string] from a string (empty string = invalid).
func nullStr(s string) sql.Null[string] {
	if s == "" {
		return sql.Null[string]{}
	}
	return sql.Null[string]{V: s, Valid: true}
}

// downloadAndStoreImages downloads images from URLs and stores them for an item.
// All images are stored with media_type "image". The first image (by ID order) serves as the thumbnail.
// If imageURLs is empty but imageURL is set, stores that single image.
func downloadAndStoreImages(
	ctx context.Context,
	db *sql.DB,
	logger *slog.Logger,
	itemID int64,
	imageURL string,
	imageURLs []string,
) {
	if len(imageURLs) > 0 {
		// Multi-image (e.g., Twitter with multiple photos)
		for i, imgURL := range imageURLs {
			imgData, contentType, err := opengraph.DownloadImage(ctx, imgURL)
			if err != nil {
				logger.Warn("failed to download image", "url", imgURL, "index", i, "error", err)
				continue
			}
			_, err = media.QCreate(ctx, db, media.CreateParams{
				ItemID:      itemID,
				MediaType:   "image",
				ContentType: contentType,
				Data:        imgData,
				SourceURL:   &imgURL,
			})
			if err != nil {
				logger.Warn("failed to store image", "index", i, "error", err)
			}
		}
	} else if imageURL != "" {
		// Single image
		imgData, contentType, err := opengraph.DownloadImage(ctx, imageURL)
		if err != nil {
			logger.Warn("failed to download image", "url", imageURL, "error", err)
			return
		}
		_, err = media.QCreate(ctx, db, media.CreateParams{
			ItemID:      itemID,
			MediaType:   "image",
			ContentType: contentType,
			Data:        imgData,
			SourceURL:   &imageURL,
		})
		if err != nil {
			logger.Warn("failed to store image", "error", err)
		}
	}
}

type urlMetadata struct {
	Title       string
	Description string
	ImageURL    string
	ImageURLs   []string // All image URLs (for multi-image tweets)
	SiteName    string
	IsEmbed     bool
	Provider    string
	VideoID     string
	EmbedHTML   string
	VideoURL    string // Direct video URL (for Twitter)
}

func fetchURLMetadata(ctx context.Context, url string) (*urlMetadata, error) {
	// Check if it's a YouTube/Vimeo/Twitter embed
	videoInfo, err := embed.Detect(ctx, url)
	if err == nil && videoInfo != nil {
		return &urlMetadata{
			Title:       videoInfo.Title,
			Description: videoInfo.Description,
			ImageURL:    videoInfo.ThumbnailURL,
			ImageURLs:   videoInfo.ThumbnailURLs,
			IsEmbed:     true,
			Provider:    string(videoInfo.Provider),
			VideoID:     videoInfo.VideoID,
			EmbedHTML:   videoInfo.EmbedHTML,
			VideoURL:    videoInfo.VideoURL,
		}, nil
	}

	// Fetch OpenGraph metadata
	meta, err := opengraph.Fetch(ctx, url)
	if err != nil {
		return nil, err
	}

	return &urlMetadata{
		Title:       meta.Title,
		Description: meta.Description,
		ImageURL:    meta.ImageURL,
		SiteName:    meta.SiteName,
		IsEmbed:     false,
	}, nil
}

type previewResponse struct {
	Title       string `json:"title,omitempty"`
	Description string `json:"description,omitempty"`
	ImageURL    string `json:"imageUrl,omitempty"`
	SiteName    string `json:"siteName,omitempty"`
	IsEmbed     bool   `json:"isEmbed"`
	Provider    string `json:"provider,omitempty"`
	VideoID     string `json:"videoId,omitempty"`
	EmbedHTML   string `json:"embedHtml,omitempty"`
}

// HandlePreviewLink handles POST /api/preview - fetches metadata for a URL
func HandlePreviewLink(rc *RequestContext, w http.ResponseWriter, r *http.Request) error {
	if !rc.RequireAdmin(w) {
		return nil
	}

	var req struct {
		URL string `json:"url"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		return writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request"})
	}

	if req.URL == "" {
		return writeJSON(w, http.StatusBadRequest, map[string]string{"error": "url required"})
	}

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

	meta, err := fetchURLMetadata(ctx, req.URL)
	if err != nil {
		return writeJSON(w, http.StatusBadRequest, map[string]string{"error": fmt.Sprintf("failed to fetch: %v", err)})
	}

	return writeJSON(w, http.StatusOK, previewResponse{
		Title:       meta.Title,
		Description: meta.Description,
		ImageURL:    meta.ImageURL,
		SiteName:    meta.SiteName,
		IsEmbed:     meta.IsEmbed,
		Provider:    meta.Provider,
		VideoID:     meta.VideoID,
		EmbedHTML:   meta.EmbedHTML,
	})
}

type createFromLinkRequest struct {
	URL         string   `json:"url"`
	Title       *string  `json:"title"`
	Description *string  `json:"description"`
	Tags        []string `json:"tags"`
	// For embeds:
	Provider  *string `json:"provider"`
	VideoID   *string `json:"videoId"`
	EmbedHTML *string `json:"embedHtml"`
	// For downloading hero image:
	ImageURL *string `json:"imageUrl"`
}

// HandleCreateFromLink handles POST /api/items/from-link
func HandleCreateFromLink(rc *RequestContext, w http.ResponseWriter, r *http.Request) error {
	if !rc.RequireAdmin(w) {
		return nil
	}

	var req createFromLinkRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		return writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request"})
	}

	ctx, cancel := context.WithTimeout(r.Context(), 60*time.Second)
	defer cancel()

	var itemType string
	var embedProvider, embedVideoID, embedHTML *string

	if req.Provider != nil && *req.Provider != "" {
		// It's an embed
		itemType = "embed"
		embedProvider = req.Provider
		embedVideoID = req.VideoID
		embedHTML = req.EmbedHTML
	} else if req.ImageURL != nil && *req.ImageURL != "" {
		// It's a link with an image
		itemType = "image"
	} else {
		// Just a link (will be shown as a card)
		itemType = "link"
	}

	// For embeds, fetch thumbnail(s) and video URL
	var imageURL, videoURL string
	var imageURLs []string // For multi-image tweets
	if req.ImageURL != nil {
		imageURL = *req.ImageURL
	}
	if itemType == "embed" && embedProvider != nil {
		if videoInfo, err := embed.Detect(ctx, req.URL); err == nil && videoInfo != nil {
			imageURL = videoInfo.ThumbnailURL
			imageURLs = videoInfo.ThumbnailURLs
			videoURL = videoInfo.VideoURL
		}
	}

	// Create the item
	it, err := item.QCreate(ctx, rc.DB, item.CreateParams{
		Title:         item.Nullable(req.Title),
		Description:   item.Nullable(req.Description),
		LinkURL:       item.Nullable(&req.URL),
		ItemType:      itemType,
		EmbedProvider: item.Nullable(embedProvider),
		EmbedVideoID:  item.Nullable(embedVideoID),
		EmbedHTML:     item.Nullable(embedHTML),
		EmbedVideoURL: nullStr(videoURL),
	})
	if err != nil {
		return err
	}

	// Download and store images
	downloadAndStoreImages(ctx, rc.DB, rc.Logger, it.ID, imageURL, imageURLs)

	// Set tags
	if len(req.Tags) > 0 {
		if err := tag.QSetTagsForItem(ctx, rc.DB, it.ID, req.Tags); err != nil {
			return err
		}
	}

	resp, err := buildItemResponse(ctx, rc, it)
	if err != nil {
		return err
	}

	return writeJSON(w, http.StatusCreated, resp)
}

// HandleUpload handles POST /api/items/upload - multipart file upload
func HandleUpload(rc *RequestContext, w http.ResponseWriter, r *http.Request) error {
	if !rc.RequireAdmin(w) {
		return nil
	}

	// Parse multipart form (max 500MB)
	if err := r.ParseMultipartForm(500 << 20); err != nil {
		return writeJSON(w, http.StatusBadRequest, map[string]string{"error": "failed to parse form"})
	}

	file, header, err := r.FormFile("file")
	if err != nil {
		return writeJSON(w, http.StatusBadRequest, map[string]string{"error": "file required"})
	}
	defer file.Close()

	title := r.FormValue("title")
	description := r.FormValue("description")
	tagsStr := r.FormValue("tags")

	var tags []string
	if tagsStr != "" {
		tags = strings.Split(tagsStr, ",")
		for i := range tags {
			tags[i] = strings.TrimSpace(tags[i])
		}
	}

	ctx, cancel := context.WithTimeout(r.Context(), 5*time.Minute)
	defer cancel()

	// Read file data
	data, err := io.ReadAll(file)
	if err != nil {
		return writeJSON(w, http.StatusBadRequest, map[string]string{"error": "failed to read file"})
	}

	contentType := header.Header.Get("Content-Type")
	if contentType == "" {
		contentType = http.DetectContentType(data)
	}

	var itemType string
	var originalData, thumbnailData []byte
	var originalContentType string

	if video.IsVideo(contentType) {
		itemType = "video"
		// Process video: transcode and extract thumbnail
		transcoded, thumbnail, err := video.ProcessVideo(ctx, data, contentType)
		if err != nil {
			return writeJSON(w, http.StatusInternalServerError, map[string]string{"error": fmt.Sprintf("video processing failed: %v", err)})
		}
		originalData = transcoded
		originalContentType = "video/mp4"
		thumbnailData = thumbnail
	} else if video.IsImage(contentType) {
		itemType = "image"
		originalData = data
		originalContentType = contentType
	} else {
		return writeJSON(w, http.StatusBadRequest, map[string]string{"error": "unsupported file type"})
	}

	// Create item
	it, err := item.QCreate(ctx, rc.DB, item.CreateParams{
		Title:       item.Nullable(strOrNil(title)),
		Description: item.Nullable(strOrNil(description)),
		ItemType:    itemType,
	})
	if err != nil {
		return err
	}

	// Store original media
	_, err = media.QCreate(ctx, rc.DB, media.CreateParams{
		ItemID:      it.ID,
		MediaType:   "original",
		ContentType: originalContentType,
		Data:        originalData,
	})
	if err != nil {
		return err
	}

	// Store thumbnail for videos
	if len(thumbnailData) > 0 {
		_, err = media.QCreate(ctx, rc.DB, media.CreateParams{
			ItemID:      it.ID,
			MediaType:   "thumbnail",
			ContentType: "image/jpeg",
			Data:        thumbnailData,
		})
		if err != nil {
			rc.Logger.Warn("failed to store thumbnail", "error", err)
		}
	}

	// Set tags
	if len(tags) > 0 {
		if err := tag.QSetTagsForItem(ctx, rc.DB, it.ID, tags); err != nil {
			return err
		}
	}

	resp, err := buildItemResponse(ctx, rc, it)
	if err != nil {
		return err
	}

	return writeJSON(w, http.StatusCreated, resp)
}

type createQuoteRequest struct {
	Text      string   `json:"text"`
	Source    *string  `json:"source"` // Optional attribution
	SourceURL *string  `json:"sourceUrl"`
	Tags      []string `json:"tags"`
}

// HandleCreateQuote handles POST /api/items/quote
func HandleCreateQuote(rc *RequestContext, w http.ResponseWriter, r *http.Request) error {
	if !rc.RequireAdmin(w) {
		return nil
	}

	var req createQuoteRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		return writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request"})
	}

	if req.Text == "" {
		return writeJSON(w, http.StatusBadRequest, map[string]string{"error": "text required"})
	}

	ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
	defer cancel()

	it, err := item.QCreate(ctx, rc.DB, item.CreateParams{
		Title:       item.Nullable(req.Source),
		Description: nullStr(req.Text),
		LinkURL:     item.Nullable(req.SourceURL),
		ItemType:    "quote",
	})
	if err != nil {
		return err
	}

	// Set tags
	if len(req.Tags) > 0 {
		if err := tag.QSetTagsForItem(ctx, rc.DB, it.ID, req.Tags); err != nil {
			return err
		}
	}

	resp, err := buildItemResponse(ctx, rc, it)
	if err != nil {
		return err
	}

	return writeJSON(w, http.StatusCreated, resp)
}

// HandleReplaceMedia handles POST /api/items/{id}/media - replaces media for an item
func HandleReplaceMedia(rc *RequestContext, w http.ResponseWriter, r *http.Request) error {
	if !rc.RequireAdmin(w) {
		return nil
	}

	pubID := r.PathValue("id")
	if pubID == "" {
		return writeJSON(w, http.StatusBadRequest, map[string]string{"error": "missing id"})
	}

	// Parse multipart form (max 500MB)
	if err := r.ParseMultipartForm(500 << 20); err != nil {
		return writeJSON(w, http.StatusBadRequest, map[string]string{"error": "failed to parse form"})
	}

	file, header, err := r.FormFile("file")
	if err != nil {
		return writeJSON(w, http.StatusBadRequest, map[string]string{"error": "file required"})
	}
	defer file.Close()

	ctx, cancel := context.WithTimeout(r.Context(), 5*time.Minute)
	defer cancel()

	it, err := item.QFindByPubID(ctx, rc.DB, pubID)
	if err != nil {
		return err
	}
	if it == nil {
		return writeJSON(w, http.StatusNotFound, map[string]string{"error": "not found"})
	}

	// Read file data
	data, err := io.ReadAll(file)
	if err != nil {
		return writeJSON(w, http.StatusBadRequest, map[string]string{"error": "failed to read file"})
	}

	contentType := header.Header.Get("Content-Type")
	if contentType == "" {
		contentType = http.DetectContentType(data)
	}

	var originalData, thumbnailData []byte
	var originalContentType string
	var newItemType string

	if video.IsVideo(contentType) {
		newItemType = "video"
		transcoded, thumbnail, err := video.ProcessVideo(ctx, data, contentType)
		if err != nil {
			return writeJSON(w, http.StatusInternalServerError, map[string]string{"error": fmt.Sprintf("video processing failed: %v", err)})
		}
		originalData = transcoded
		originalContentType = "video/mp4"
		thumbnailData = thumbnail
	} else if video.IsImage(contentType) {
		newItemType = "image"
		originalData = data
		originalContentType = contentType
	} else {
		return writeJSON(w, http.StatusBadRequest, map[string]string{"error": "unsupported file type"})
	}

	// Delete existing media
	media.QDeleteByItemID(ctx, rc.DB, it.ID)

	// Store new original media
	_, err = media.QCreate(ctx, rc.DB, media.CreateParams{
		ItemID:      it.ID,
		MediaType:   "original",
		ContentType: originalContentType,
		Data:        originalData,
	})
	if err != nil {
		return err
	}

	// Store thumbnail for videos
	if len(thumbnailData) > 0 {
		_, err = media.QCreate(ctx, rc.DB, media.CreateParams{
			ItemID:      it.ID,
			MediaType:   "thumbnail",
			ContentType: "image/jpeg",
			Data:        thumbnailData,
		})
		if err != nil {
			rc.Logger.Warn("failed to store thumbnail", "error", err)
		}
	}

	// Update item type if it changed (e.g., embed -> image)
	if it.ItemType != newItemType && (it.ItemType == "embed" || it.ItemType == "link") {
		item.QUpdateType(ctx, rc.DB, it.ID, newItemType)
	}

	// Refetch and return updated item
	it, err = item.QFindByID(ctx, rc.DB, it.ID)
	if err != nil {
		return err
	}

	resp, err := buildItemResponse(ctx, rc, *it)
	if err != nil {
		return err
	}

	return writeJSON(w, http.StatusOK, resp)
}

// HandleRefreshMetadata handles POST /api/items/{id}/refresh
// Re-fetches metadata and thumbnail for an item with a link URL
func HandleRefreshMetadata(rc *RequestContext, w http.ResponseWriter, r *http.Request) error {
	if !rc.RequireAdmin(w) {
		return nil
	}

	pubID := r.PathValue("id")
	if pubID == "" {
		return writeJSON(w, http.StatusBadRequest, map[string]string{"error": "missing id"})
	}

	ctx, cancel := context.WithTimeout(r.Context(), 60*time.Second)
	defer cancel()

	it, err := item.QFindByPubID(ctx, rc.DB, pubID)
	if err != nil {
		return err
	}
	if it == nil {
		return writeJSON(w, http.StatusNotFound, map[string]string{"error": "not found"})
	}

	if !it.LinkURL.Valid || it.LinkURL.V == "" {
		return writeJSON(w, http.StatusBadRequest, map[string]string{"error": "item has no link URL"})
	}

	meta, err := fetchURLMetadata(ctx, it.LinkURL.V)
	if err != nil {
		return writeJSON(w, http.StatusBadRequest, map[string]string{"error": fmt.Sprintf("failed to fetch: %v", err)})
	}

	// Update title, description, and video URL
	item.QUpdate(ctx, rc.DB, it.ID, item.UpdateParams{
		Title:       nullStr(meta.Title),
		Description: nullStr(meta.Description),
		LinkURL:     it.LinkURL,
	})
	if meta.VideoURL != "" {
		item.QUpdateVideoURL(ctx, rc.DB, it.ID, meta.VideoURL)
	}

	// Download and replace images
	if len(meta.ImageURLs) > 0 || meta.ImageURL != "" {
		// Delete existing media (thumbnails and gallery)
		media.QDeleteByItemID(ctx, rc.DB, it.ID)
		downloadAndStoreImages(ctx, rc.DB, rc.Logger, it.ID, meta.ImageURL, meta.ImageURLs)
	}

	// Refetch and return updated item
	it, err = item.QFindByID(ctx, rc.DB, it.ID)
	if err != nil {
		return err
	}

	resp, err := buildItemResponse(ctx, rc, *it)
	if err != nil {
		return err
	}

	return writeJSON(w, http.StatusOK, resp)
}
