package handlers

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"errors"
	"fmt"
	"image"
	"image/color"
	"image/draw"
	"image/png"
	"io"
	"log/slog"
	"net/http"
	"strings"
	"time"

	"github.com/gofiber/fiber/v2"

	filemanager "wucher/internal/domain/file_manager"
	mastersettings "wucher/internal/domain/master_settings"
	"wucher/internal/shared/pkg/metrics"
	"wucher/internal/transport/http/jsonapi"
)

const (
	brandingLogoManagedCacheControl  = "public, max-age=300, stale-while-revalidate=86400"
	brandingCoverManagedCacheControl = "public, max-age=300, stale-while-revalidate=86400"
	brandingContentCacheControl      = "public, max-age=60, stale-while-revalidate=300"
	brandingFallbackCache            = "public, max-age=60, stale-while-revalidate=300"
	brandingPresignTTL               = time.Minute
	brandingFetchTimeout             = 10 * time.Second
	brandingMaxBytes                 = 5 << 20
	brandingCORPMode                 = "same-site"
	brandingLoginTitleDefault        = "Welcome back"
	brandingLoginSubtitleDefault     = "Sign in to continue"
)

var brandingDefaultLastModified = time.Date(2026, time.January, 1, 0, 0, 0, 0, time.UTC)
var _ = jsonapi.ErrorDocument{}
var brandingDefaultEmailLogoPNG = mustBuildDefaultEmailLogoPNG()

const brandingDefaultLogoSVG = `<svg xmlns="http://www.w3.org/2000/svg" width="220" height="48" viewBox="0 0 220 48" role="img" aria-label="Company Logo"><rect width="220" height="48" rx="8" fill="#0F172A"/><text x="20" y="31" font-family="Arial, Helvetica, sans-serif" font-size="18" font-weight="700" fill="#F8FAFC">COMPANY</text></svg>`
const brandingDefaultCoverSVG = `<svg xmlns="http://www.w3.org/2000/svg" width="1600" height="900" viewBox="0 0 1600 900" role="img" aria-label="Login Background"><defs><linearGradient id="g" x1="0" x2="1" y1="0" y2="1"><stop offset="0%" stop-color="#0F172A"/><stop offset="100%" stop-color="#1D4ED8"/></linearGradient></defs><rect width="1600" height="900" fill="url(#g)"/><circle cx="1300" cy="120" r="180" fill="#FFFFFF" fill-opacity="0.08"/><circle cx="200" cy="760" r="260" fill="#FFFFFF" fill-opacity="0.06"/></svg>`

type brandingLogoPayload struct {
	Body         []byte
	ContentType  string
	ETag         string
	LastModified time.Time
	CacheControl string
	Source       string
}

type loginBrandingContentDocument struct {
	Data loginBrandingContentData `json:"data"`
}

type loginBrandingContentData struct {
	Type       string                       `json:"type"`
	Attributes loginBrandingContentAttrsDTO `json:"attributes"`
}

type loginBrandingContentAttrsDTO struct {
	Title     string `json:"title"`
	Subtitle  string `json:"subtitle"`
	UpdatedAt string `json:"updated_at"`
	Version   string `json:"version"`
}

type BrandingHandler struct {
	masterSvc  mastersettings.Service
	fileSvc    filemanager.Service
	storage    filemanager.ObjectStorage
	httpClient *http.Client
	logger     *slog.Logger
}

func NewBrandingHandler(masterSvc mastersettings.Service, fileSvc filemanager.Service, storage filemanager.ObjectStorage) *BrandingHandler {
	return &BrandingHandler{
		masterSvc: masterSvc,
		fileSvc:   fileSvc,
		storage:   storage,
		httpClient: &http.Client{
			Timeout: brandingFetchTimeout,
		},
		logger: slog.Default(),
	}
}

// GetBrandingLogo godoc
// @Summary Get branding logo
// @Description Return a stable company logo asset for frontend consumption.
// @Description Backend resolves private object storage internally (no presigned URL exposure) and serves fallback logo when managed logo is unavailable.
// @Description Supports conditional requests with `If-None-Match` / `If-Modified-Since`.
// @Tags Branding
// @Produce image/png,image/jpeg,image/webp,image/svg+xml,application/octet-stream
// @Success 200 {file} file "Branding logo binary"
// @Success 304 {string} string "Not Modified"
// @Router /api/v1/branding/logo [get]
func (h *BrandingHandler) GetLogo(c *fiber.Ctx) error {
	started := time.Now()
	payload, fetchErr := h.loadLogo(c.UserContext())
	if fetchErr != nil {
		h.logger.Warn("branding logo fetch degraded to fallback", "error", fetchErr)
	}
	statusCode := fiber.StatusOK
	h.writeImageCacheHeaders(c, payload)
	if brandingNotModified(c, payload.ETag, payload.LastModified) {
		statusCode = fiber.StatusNotModified
		metrics.ObserveBrandingRequest("api", "logo", payload.Source, statusCode, fetchErr, started)
		return c.SendStatus(statusCode)
	}
	metrics.ObserveBrandingRequest("api", "logo", payload.Source, statusCode, fetchErr, started)
	return c.Status(statusCode).Send(payload.Body)
}

// GetBrandingEmailLogo godoc
// @Summary Get email-safe branding logo
// @Description Return a stable company logo optimized for email clients like Outlook.
// @Description Backend prefers managed raster thumbnail/logo and falls back to backend-generated PNG when necessary.
// @Description Supports conditional requests with `If-None-Match` / `If-Modified-Since`.
// @Tags Branding
// @Produce image/png,image/jpeg,image/gif,application/octet-stream
// @Success 200 {file} file "Email-safe branding logo binary"
// @Success 304 {string} string "Not Modified"
// @Router /api/v1/public/branding/email-logo [get]
func (h *BrandingHandler) GetEmailLogo(c *fiber.Ctx) error {
	started := time.Now()
	payload, fetchErr := h.loadEmailLogo(c.UserContext())
	if fetchErr != nil {
		h.logger.Warn("branding email logo fetch degraded to fallback", "error", fetchErr)
	}
	statusCode := fiber.StatusOK
	h.writeImageCacheHeaders(c, payload)
	h.relaxEmailLogoSecurityHeaders(c)
	if brandingNotModified(c, payload.ETag, payload.LastModified) {
		statusCode = fiber.StatusNotModified
		metrics.ObserveBrandingRequest("api", "email_logo", payload.Source, statusCode, fetchErr, started)
		return c.SendStatus(statusCode)
	}
	metrics.ObserveBrandingRequest("api", "email_logo", payload.Source, statusCode, fetchErr, started)
	return c.Status(statusCode).Send(payload.Body)
}

// GetBrandingLoginCover godoc
// @Summary Get login cover image
// @Description Return public stable login cover image for sign-in page.
// @Description Supports conditional requests with `If-None-Match` / `If-Modified-Since`.
// @Tags Branding
// @Produce image/png,image/jpeg,image/webp,image/svg+xml,application/octet-stream
// @Success 200 {file} file "Login cover image binary"
// @Success 304 {string} string "Not Modified"
// @Router /api/v1/branding/login-cover [get]
func (h *BrandingHandler) GetLoginCover(c *fiber.Ctx) error {
	started := time.Now()
	payload, fetchErr := h.loadLoginCover(c.UserContext())
	if fetchErr != nil {
		h.logger.Warn("branding login cover fetch degraded to fallback", "error", fetchErr)
	}

	statusCode := fiber.StatusOK
	h.writeImageCacheHeaders(c, payload)
	c.Set("Cross-Origin-Resource-Policy", brandingCORPMode)
	if brandingNotModified(c, payload.ETag, payload.LastModified) {
		statusCode = fiber.StatusNotModified
		metrics.ObserveBrandingRequest("api", "login_cover", payload.Source, statusCode, fetchErr, started)
		return c.SendStatus(statusCode)
	}
	metrics.ObserveBrandingRequest("api", "login_cover", payload.Source, statusCode, fetchErr, started)
	return c.Status(statusCode).Send(payload.Body)
}

// GetBrandingLoginContent godoc
// @Summary Get login content
// @Description Return public stable login title and subtitle from active master settings with backend defaults.
// @Description Supports conditional requests with `If-None-Match` / `If-Modified-Since`.
// @Tags Branding
// @Produce json
// @Success 200 {object} loginBrandingContentDocument
// @Success 304 {string} string "Not Modified"
// @Router /api/v1/branding/login-content [get]
func (h *BrandingHandler) GetLoginContent(c *fiber.Ctx) error {
	started := time.Now()
	doc, etag, lastModified, source, err := h.loadLoginContent(c.UserContext())
	if err != nil {
		h.logger.Warn("branding login content degraded to defaults", "error", err)
	}

	statusCode := fiber.StatusOK
	h.writeJSONCacheHeaders(c, etag, lastModified, brandingContentCacheControl)
	if brandingNotModified(c, etag, lastModified) {
		statusCode = fiber.StatusNotModified
		metrics.ObserveBrandingRequest("api", "login_content", source, statusCode, err, started)
		return c.SendStatus(statusCode)
	}
	metrics.ObserveBrandingRequest("api", "login_content", source, statusCode, err, started)
	return c.Status(statusCode).JSON(doc)
}

func (h *BrandingHandler) loadLogo(ctx context.Context) (brandingLogoPayload, error) {
	row, err := h.getActiveMasterSettings(ctx)
	if err != nil {
		return h.defaultLogoPayload(), err
	}
	if row == nil || len(row.LogoFileID) == 0 {
		return h.defaultLogoPayload(), nil
	}
	return h.loadManagedImage(ctx, row, row.LogoFileID, h.defaultLogoPayload(), "logo", brandingLogoManagedCacheControl, false)
}

func (h *BrandingHandler) loadLoginCover(ctx context.Context) (brandingLogoPayload, error) {
	row, err := h.getActiveMasterSettings(ctx)
	if err != nil {
		return h.defaultCoverPayload(), err
	}
	if row == nil || len(row.CoverFileID) == 0 {
		return h.defaultCoverPayload(), nil
	}
	return h.loadManagedImage(ctx, row, row.CoverFileID, h.defaultCoverPayload(), "login_cover", brandingCoverManagedCacheControl, false)
}

func (h *BrandingHandler) loadEmailLogo(ctx context.Context) (brandingLogoPayload, error) {
	row, err := h.getActiveMasterSettings(ctx)
	if err != nil {
		return h.defaultEmailLogoPayload(), err
	}
	if row == nil || len(row.LogoFileID) == 0 {
		return h.defaultEmailLogoPayload(), nil
	}
	return h.loadManagedImage(ctx, row, row.LogoFileID, h.defaultEmailLogoPayload(), "email_logo", brandingLogoManagedCacheControl, true)
}

func (h *BrandingHandler) loadManagedImage(
	ctx context.Context,
	ms *mastersettings.MasterSettings,
	fileID []byte,
	fallback brandingLogoPayload,
	resourceType string,
	cacheControl string,
	emailSafe bool,
) (brandingLogoPayload, error) {
	if h.fileSvc == nil {
		return fallback, errors.New("file manager service is not configured")
	}
	if h.storage == nil {
		return fallback, errors.New("object storage is not configured")
	}

	fileRow, err := h.fileSvc.GetFileByID(ctx, fileID)
	if err != nil {
		return fallback, fmt.Errorf("get managed branding file: %w", err)
	}
	if fileRow == nil {
		return fallback, errors.New("managed branding file not found")
	}
	if !strings.EqualFold(strings.TrimSpace(fileRow.Status), filemanager.FileStatusReady) {
		return fallback, errors.New("managed branding file is not ready")
	}

	payload, err := h.downloadManagedImage(ctx, ms, fileRow, resourceType, cacheControl, emailSafe)
	if err != nil {
		return fallback, err
	}
	return payload, nil
}

func (h *BrandingHandler) defaultLogoPayload() brandingLogoPayload {
	body := []byte(brandingDefaultLogoSVG)
	return brandingLogoPayload{
		Body:         body,
		ContentType:  "image/svg+xml; charset=utf-8",
		ETag:         quotedSHA256("branding-default:" + string(body)),
		LastModified: brandingDefaultLastModified,
		CacheControl: brandingFallbackCache,
		Source:       "default_logo",
	}
}

func (h *BrandingHandler) defaultEmailLogoPayload() brandingLogoPayload {
	body := append([]byte(nil), brandingDefaultEmailLogoPNG...)
	return brandingLogoPayload{
		Body:         body,
		ContentType:  "image/png",
		ETag:         quotedSHA256("branding-email-default:" + string(body)),
		LastModified: brandingDefaultLastModified,
		CacheControl: brandingFallbackCache,
		Source:       "default_email_logo",
	}
}

func (h *BrandingHandler) defaultCoverPayload() brandingLogoPayload {
	body := []byte(brandingDefaultCoverSVG)
	return brandingLogoPayload{
		Body:         body,
		ContentType:  "image/svg+xml; charset=utf-8",
		ETag:         quotedSHA256("branding-cover-default:" + string(body)),
		LastModified: brandingDefaultLastModified,
		CacheControl: brandingFallbackCache,
		Source:       "default_cover",
	}
}

func (h *BrandingHandler) downloadManagedImage(ctx context.Context, ms *mastersettings.MasterSettings, logoFile *filemanager.File, resourceType, cacheControl string, emailSafe bool) (brandingLogoPayload, error) {
	objectKey, declaredContentType, err := selectBrandingObjectVariant(logoFile, emailSafe)
	if err != nil {
		return brandingLogoPayload{}, err
	}
	if objectKey == "" {
		return brandingLogoPayload{}, errors.New("managed branding object key is empty")
	}

	presigned, err := h.storage.PresignGetObject(ctx, objectKey, brandingPresignTTL)
	if err != nil {
		return brandingLogoPayload{}, fmt.Errorf("presign managed branding object: %w", err)
	}
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, presigned, nil)
	if err != nil {
		return brandingLogoPayload{}, fmt.Errorf("build object request: %w", err)
	}
	resp, err := h.httpClient.Do(req)
	if err != nil {
		return brandingLogoPayload{}, fmt.Errorf("download logo object: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return brandingLogoPayload{}, fmt.Errorf("object storage returned status %d", resp.StatusCode)
	}

	limited := io.LimitReader(resp.Body, brandingMaxBytes+1)
	body, err := io.ReadAll(limited)
	if err != nil {
		return brandingLogoPayload{}, fmt.Errorf("read logo object body: %w", err)
	}
	if len(body) == 0 {
		return brandingLogoPayload{}, errors.New("logo object body is empty")
	}
	if len(body) > brandingMaxBytes {
		return brandingLogoPayload{}, errors.New("logo object exceeds max allowed size")
	}

	contentType := strings.TrimSpace(resp.Header.Get(fiber.HeaderContentType))
	if contentType == "" {
		contentType = declaredContentType
	}
	if contentType == "" {
		contentType = "application/octet-stream"
	}
	if emailSafe && !isEmailSafeImageContentType(contentType) {
		return brandingLogoPayload{}, errors.New("managed branding asset is not email-safe")
	}

	lastModified := ms.UpdatedAt
	if lastModified.IsZero() || (!logoFile.UpdatedAt.IsZero() && logoFile.UpdatedAt.After(lastModified)) {
		lastModified = logoFile.UpdatedAt
	}
	if lastModified.IsZero() {
		lastModified = time.Now().UTC()
	}

	etagSeed := strings.Join([]string{
		"branding-managed-" + resourceType,
		idString(ms.ID),
		idString(logoFile.ID),
		lastModified.UTC().Format(time.RFC3339Nano),
		strings.TrimSpace(logoFile.ETag),
		strings.TrimSpace(logoFile.ObjectVersion),
	}, "|")

	return brandingLogoPayload{
		Body:         body,
		ContentType:  contentType,
		ETag:         quotedSHA256(etagSeed),
		LastModified: lastModified.UTC(),
		CacheControl: cacheControl,
		Source:       "managed_" + resourceType,
	}, nil
}

func selectBrandingObjectVariant(fileRow *filemanager.File, emailSafe bool) (string, string, error) {
	if fileRow == nil {
		return "", "", errors.New("managed branding file is missing")
	}
	if emailSafe {
		if fileRow.ThumbnailObjectKey != nil {
			thumbKey := strings.TrimSpace(*fileRow.ThumbnailObjectKey)
			thumbType := strings.TrimSpace(ptrStringValue(fileRow.ThumbnailMimeType))
			if thumbKey != "" && isEmailSafeImageContentType(thumbType) {
				return thumbKey, thumbType, nil
			}
		}
		originalKey := strings.TrimSpace(fileRow.ObjectKey)
		originalType := strings.TrimSpace(fileRow.MimeType)
		if originalKey != "" && isEmailSafeImageContentType(originalType) {
			return originalKey, originalType, nil
		}
		return "", "", errors.New("managed branding file has no email-safe variant")
	}
	return strings.TrimSpace(fileRow.ObjectKey), strings.TrimSpace(fileRow.MimeType), nil
}

func isEmailSafeImageContentType(contentType string) bool {
	contentType = strings.ToLower(strings.TrimSpace(contentType))
	if idx := strings.Index(contentType, ";"); idx >= 0 {
		contentType = strings.TrimSpace(contentType[:idx])
	}
	switch contentType {
	case "image/png", "image/jpeg", "image/jpg", "image/gif":
		return true
	default:
		return false
	}
}

func ptrStringValue(v *string) string {
	if v == nil {
		return ""
	}
	return *v
}

func mustBuildDefaultEmailLogoPNG() []byte {
	var buf strings.Builder
	_ = buf
	img := image.NewRGBA(image.Rect(0, 0, 220, 48))
	draw.Draw(img, img.Bounds(), &image.Uniform{C: color.RGBA{R: 15, G: 23, B: 42, A: 255}}, image.Point{}, draw.Src)
	draw.Draw(img, image.Rect(0, 0, 220, 4), &image.Uniform{C: color.RGBA{R: 148, G: 163, B: 184, A: 255}}, image.Point{}, draw.Src)
	draw.Draw(img, image.Rect(16, 14, 48, 34), &image.Uniform{C: color.RGBA{R: 255, G: 255, B: 255, A: 255}}, image.Point{}, draw.Src)
	var out strings.Builder
	_ = out
	var pngBuf []byte
	w := &sliceWriter{buf: &pngBuf}
	if err := png.Encode(w, img); err != nil {
		panic(err)
	}
	return pngBuf
}

type sliceWriter struct {
	buf *[]byte
}

func (w *sliceWriter) Write(p []byte) (int, error) {
	*w.buf = append(*w.buf, p...)
	return len(p), nil
}

func (h *BrandingHandler) loadLoginContent(ctx context.Context) (loginBrandingContentDocument, string, time.Time, string, error) {
	row, err := h.getActiveMasterSettings(ctx)
	source := "default_content"
	updatedAt := brandingDefaultLastModified
	title := brandingLoginTitleDefault
	subtitle := brandingLoginSubtitleDefault

	if err == nil && row != nil {
		if strings.TrimSpace(row.Title) != "" {
			title = strings.TrimSpace(row.Title)
		}
		if strings.TrimSpace(row.Subtitle) != "" {
			subtitle = strings.TrimSpace(row.Subtitle)
		}
		if !row.UpdatedAt.IsZero() {
			updatedAt = row.UpdatedAt.UTC()
		}
		source = "managed_content"
	}

	version := updatedAt.Format(time.RFC3339)
	doc := loginBrandingContentDocument{
		Data: loginBrandingContentData{
			Type: "login_branding",
			Attributes: loginBrandingContentAttrsDTO{
				Title:     title,
				Subtitle:  subtitle,
				UpdatedAt: version,
				Version:   version,
			},
		},
	}
	etagSeed := strings.Join([]string{
		"branding-login-content",
		title,
		subtitle,
		version,
	}, "|")
	return doc, quotedSHA256(etagSeed), updatedAt, source, err
}

func (h *BrandingHandler) getActiveMasterSettings(ctx context.Context) (*mastersettings.MasterSettings, error) {
	if h.masterSvc == nil {
		return nil, errors.New("master settings service is not configured")
	}
	rows, _, err := h.masterSvc.List(ctx, "", "-updated_at", 1, 0)
	if err != nil {
		return nil, fmt.Errorf("list master settings: %w", err)
	}
	if len(rows) == 0 {
		return nil, nil
	}
	return &rows[0], nil
}

func (h *BrandingHandler) writeImageCacheHeaders(c *fiber.Ctx, payload brandingLogoPayload) {
	c.Set(fiber.HeaderContentType, payload.ContentType)
	c.Set(fiber.HeaderCacheControl, payload.CacheControl)
	c.Set(fiber.HeaderETag, payload.ETag)
	c.Set(fiber.HeaderLastModified, payload.LastModified.UTC().Format(http.TimeFormat))
	c.Set(fiber.HeaderVary, "Origin")
	c.Set(fiber.HeaderXContentTypeOptions, "nosniff")
}

func (h *BrandingHandler) writeJSONCacheHeaders(c *fiber.Ctx, etag string, lastModified time.Time, cacheControl string) {
	c.Set(fiber.HeaderCacheControl, cacheControl)
	c.Set(fiber.HeaderETag, etag)
	c.Set(fiber.HeaderLastModified, lastModified.UTC().Format(http.TimeFormat))
	c.Set(fiber.HeaderVary, "Origin")
}

func (h *BrandingHandler) relaxEmailLogoSecurityHeaders(c *fiber.Ctx) {
	c.Set("Cross-Origin-Resource-Policy", "cross-origin")
	c.Response().Header.Del("Cross-Origin-Embedder-Policy")
	c.Response().Header.Del("Cross-Origin-Opener-Policy")
	c.Response().Header.Del("Origin-Agent-Cluster")
}

func brandingNotModified(c *fiber.Ctx, etag string, lastModified time.Time) bool {
	if etagMatches(c.Get(fiber.HeaderIfNoneMatch), etag) {
		return true
	}
	rawIfModifiedSince := strings.TrimSpace(c.Get(fiber.HeaderIfModifiedSince))
	if rawIfModifiedSince == "" || lastModified.IsZero() {
		return false
	}
	ifModifiedSince, err := http.ParseTime(rawIfModifiedSince)
	if err != nil {
		return false
	}
	return !lastModified.UTC().Truncate(time.Second).After(ifModifiedSince.UTC().Truncate(time.Second))
}

func etagMatches(rawHeader, currentETag string) bool {
	rawHeader = strings.TrimSpace(rawHeader)
	if rawHeader == "" || strings.TrimSpace(currentETag) == "" {
		return false
	}
	if rawHeader == "*" {
		return true
	}
	currentNormalized := normalizeETag(currentETag)
	parts := strings.Split(rawHeader, ",")
	for i := range parts {
		if normalizeETag(parts[i]) == currentNormalized {
			return true
		}
	}
	return false
}

func normalizeETag(raw string) string {
	raw = strings.TrimSpace(raw)
	raw = strings.TrimPrefix(raw, "W/")
	raw = strings.TrimPrefix(raw, "w/")
	return strings.TrimSpace(raw)
}

func quotedSHA256(input string) string {
	sum := sha256.Sum256([]byte(input))
	return `"` + hex.EncodeToString(sum[:]) + `"`
}
