package attachment

import (
	"context"
	"errors"
	"fmt"
	"net/http"
	"strings"

	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/objectstorage"
)

// mediaObjectTypeAvatarDerivative is the object-storage type prefix for avatar
// derivatives (thumbnails). Objects are keyed deterministically —
// "avatar-derivatives/{workspaceId}/{avatarId}/{variant}.{ext}" — so the
// existence of the object IS the cache, mirroring attachment derivatives.
const mediaObjectTypeAvatarDerivative = "avatar-derivatives"

// AvatarVariantRef identifies the avatar object behind an owner-scoped avatar
// URL. ExternalURL is set when the profile points at a non-AgentTeam avatar URL, in
// which case no AgentTeam thumbnail exists and the caller falls back to a redirect.
type AvatarVariantRef struct {
	AvatarID    string
	WorkspaceID string
	ExternalURL string
}

// ResolveUserAvatarRef resolves the avatar object a user's profile currently
// points at, without loading its blob.
func (txs TxStore) ResolveUserAvatarRef(ctx context.Context, userID string) (AvatarVariantRef, error) {
	trimmedUserID := strings.TrimSpace(userID)
	if trimmedUserID == "" {
		return AvatarVariantRef{}, badRequest("userId is required")
	}

	var avatarURL string
	row := txs.tx.QueryRow(ctx, `
SELECT COALESCE(avatar_url, '')
FROM users
WHERE id = $1
`, trimmedUserID)
	if err := row.Scan(&avatarURL); err != nil {
		if coredb.IsNotFound(err) {
			return AvatarVariantRef{}, ErrNotFound
		}
		return AvatarVariantRef{}, fmt.Errorf("resolve user avatar: %w", err)
	}

	trimmedAvatarURL := strings.TrimSpace(avatarURL)
	if trimmedAvatarURL == "" {
		return AvatarVariantRef{}, ErrNotFound
	}
	if avatarID := avatarIDFromAgentTeamAvatarURL(trimmedAvatarURL); avatarID != "" {
		return txs.avatarVariantRefByID(ctx, avatarID)
	}
	if isAgentTeamUserAvatarURL(trimmedAvatarURL, trimmedUserID) {
		return txs.latestAvatarVariantRefForOwner(ctx, "", avatarOwnerKindUser, trimmedUserID)
	}
	return AvatarVariantRef{ExternalURL: trimmedAvatarURL}, nil
}

// ResolveAgentAvatarRef resolves the latest AgentTeam-managed avatar object for an
// agent, without loading its blob.
func (txs TxStore) ResolveAgentAvatarRef(ctx context.Context, workspaceID string, agentID string) (AvatarVariantRef, error) {
	trimmedWorkspaceID := strings.TrimSpace(workspaceID)
	if trimmedWorkspaceID == "" {
		return AvatarVariantRef{}, badRequest("workspaceId is required")
	}
	trimmedAgentID := strings.TrimSpace(agentID)
	if trimmedAgentID == "" {
		return AvatarVariantRef{}, badRequest("agentId is required")
	}
	return txs.latestAvatarVariantRefForOwner(ctx, trimmedWorkspaceID, avatarOwnerKindAgent, trimmedAgentID)
}

func (txs TxStore) latestAvatarVariantRefForOwner(ctx context.Context, workspaceID string, ownerKind string, ownerID string) (AvatarVariantRef, error) {
	avatarID, err := txs.latestAvatarIDForOwner(ctx, workspaceID, ownerKind, ownerID)
	if err != nil {
		return AvatarVariantRef{}, err
	}
	return txs.avatarVariantRefByID(ctx, avatarID)
}

func (txs TxStore) avatarVariantRefByID(ctx context.Context, avatarID string) (AvatarVariantRef, error) {
	trimmedID := strings.TrimSpace(avatarID)
	if trimmedID == "" {
		return AvatarVariantRef{}, ErrNotFound
	}
	var workspaceID string
	err := txs.tx.QueryRow(ctx, `
SELECT COALESCE(workspace_id, '')
FROM avatars
WHERE id = $1
`, trimmedID).Scan(&workspaceID)
	if err != nil {
		if coredb.IsNotFound(err) {
			return AvatarVariantRef{}, ErrNotFound
		}
		return AvatarVariantRef{}, fmt.Errorf("resolve avatar workspace: %w", err)
	}
	return AvatarVariantRef{AvatarID: trimmedID, WorkspaceID: strings.TrimSpace(workspaceID)}, nil
}

// LoadAvatarThumbnail returns a cached avatar thumbnail derivative from object
// storage, or attachment.ErrNotFound when none has been generated yet (or
// object storage is disabled — the endpoint then regenerates per request).
func (store *Store) LoadAvatarThumbnail(ctx context.Context, avatarID string, variant string) (AttachmentVariant, error) {
	trimmedID := strings.TrimSpace(avatarID)
	if trimmedID == "" {
		return AttachmentVariant{}, ErrNotFound
	}
	if store.mediaObjectStorage == nil {
		return AttachmentVariant{}, ErrNotFound
	}
	workspaceID, err := store.avatarWorkspaceID(ctx, trimmedID)
	if err != nil {
		return AttachmentVariant{}, err
	}
	blob, err := store.mediaObjectStorage.Get(ctx, avatarDerivativeObjectKey(workspaceID, trimmedID, variant))
	if err != nil {
		if errors.Is(err, objectstorage.ErrObjectNotFound) {
			return AttachmentVariant{}, ErrNotFound
		}
		return AttachmentVariant{}, fmt.Errorf("load avatar thumbnail: %w", err)
	}
	return AttachmentVariant{ContentType: http.DetectContentType(blob), Blob: blob}, nil
}

// SaveAvatarThumbnail persists a generated avatar thumbnail so later requests
// hit the cache. When object storage is disabled the bytes are returned without
// persisting, mirroring attachment thumbnail behavior.
func (store *Store) SaveAvatarThumbnail(ctx context.Context, avatarID string, variant string, gen GeneratedAvatarThumbnail) (AttachmentVariant, error) {
	trimmedID := strings.TrimSpace(avatarID)
	if trimmedID == "" {
		return AttachmentVariant{}, badRequest("avatarId is required")
	}
	if store.mediaObjectStorage == nil {
		return AttachmentVariant{ContentType: gen.ContentType, Blob: gen.Blob, Width: gen.Width, Height: gen.Height}, nil
	}
	workspaceID, err := store.avatarWorkspaceID(ctx, trimmedID)
	if err != nil {
		return AttachmentVariant{}, err
	}
	objectKey := avatarDerivativeObjectKey(workspaceID, trimmedID, variant)
	if err := store.mediaObjectStorage.Put(ctx, objectKey, gen.ContentType, gen.Blob); err != nil {
		return AttachmentVariant{}, fmt.Errorf("store avatar thumbnail object: %w", err)
	}
	return AttachmentVariant{ContentType: gen.ContentType, Blob: gen.Blob, Width: gen.Width, Height: gen.Height}, nil
}

func (store *Store) avatarWorkspaceID(ctx context.Context, avatarID string) (string, error) {
	var workspaceID string
	err := coredb.WithTx(ctx, store.db, func(tx coredb.Tx) error {
		return store.InTx(tx).tx.QueryRow(ctx, `
SELECT COALESCE(workspace_id, '')
FROM avatars
WHERE id = $1
`, avatarID).Scan(&workspaceID)
	})
	if err != nil {
		if coredb.IsNotFound(err) {
			return "", ErrNotFound
		}
		return "", fmt.Errorf("load avatar workspace: %w", err)
	}
	return strings.TrimSpace(workspaceID), nil
}

func avatarDerivativeObjectKey(workspaceID string, avatarID string, variant string) string {
	// The extension is informational: derivatives are JPEG or PNG and the
	// content type is sniffed back from the bytes on read.
	return mediaObjectKey(
		mediaObjectTypeAvatarDerivative,
		workspaceID,
		strings.TrimSpace(avatarID)+"/"+strings.TrimSpace(variant)+".img",
	)
}
