package face

import (
	"context"
	"crypto/sha256"
	"fmt"
	"log"
	"time"
)

// PhotoRepository defines the photo operations needed by the face service
type PhotoRepository interface {
	GetPhoto(ctx context.Context, id string) (*PhotoInfo, error)
	CreateFaceEmbedding(ctx context.Context, embedding *FaceEmbeddingInfo) (*FaceEmbeddingInfo, error)
}

// PhotoInfo represents photo metadata needed by the face service
type PhotoInfo struct {
	ID           string `json:"id"`
	SpaceID      string `json:"spaceId"`
	URL          string `json:"url"`
	ThumbnailURL string `json:"thumbnailUrl"`
	FaceCount    int    `json:"faceCount"`
}

// FaceEmbeddingInfo represents a face embedding record
type FaceEmbeddingInfo struct {
	ID          string    `json:"id"`
	PhotoID     string    `json:"photoId"`
	SpaceID     string    `json:"spaceId"`
	Embedding   []float32 `json:"embedding"`
	BoundingBox string    `json:"boundingBox"`
	CreatedAt   time.Time `json:"createdAt"`
}

// Service provides face detection, indexing, and search operations
type Service struct {
	client Client
	repo   PhotoRepository
	cache  CacheService
	config *Config
	keys   CacheKeys
}

// NewService creates a new face service
func NewService(client Client, repo PhotoRepository, cache CacheService, config *Config) *Service {
	return &Service{
		client: client,
		repo:   repo,
		cache:  cache,
		config: config,
	}
}

// ValidateImage validates that image bytes are a valid image
func (s *Service) ValidateImage(imageBytes []byte) error {
	if len(imageBytes) > s.config.MaxImageSize {
		return ErrImageTooLarge
	}
	if len(imageBytes) < 100 { // Minimum viable image size
		return ErrImageTooSmall
	}
	// Check for valid image headers
	if len(imageBytes) < 4 {
		return ErrInvalidImageFormat
	}
	// JPEG: starts with FF D8 FF
	// PNG: starts with 89 50 4E 47
	// WebP: starts with 52 49 46 46 (RIFF)
	if imageBytes[0] == 0xFF && imageBytes[1] == 0xD8 && imageBytes[2] == 0xFF {
		return nil // JPEG
	}
	if imageBytes[0] == 0x89 && imageBytes[1] == 0x50 && imageBytes[2] == 0x4E && imageBytes[3] == 0x47 {
		return nil // PNG
	}
	if imageBytes[0] == 0x52 && imageBytes[1] == 0x49 && imageBytes[2] == 0x46 && imageBytes[3] == 0x46 {
		return nil // WebP (RIFF)
	}
	return ErrInvalidImageFormat
}

// IndexPhoto indexes all faces in a photo
func (s *Service) IndexPhoto(ctx context.Context, photoID string, imageBytes []byte) error {
	// Check if already indexed
	indexKey := s.keys.Indexed(photoID)
	if indexed, _ := s.cache.Get(ctx, indexKey); indexed == "true" {
		return nil
	}

	// Detect faces
	detections, err := s.client.DetectFaces(ctx, imageBytes)
	if err != nil {
		return fmt.Errorf("failed to detect faces: %w", err)
	}

	if len(detections) == 0 {
		log.Printf("No faces detected in photo %s", photoID)
		return nil
	}

	// Get photo metadata
	photo, err := s.repo.GetPhoto(ctx, photoID)
	if err != nil {
		return fmt.Errorf("failed to get photo: %w", err)
	}

	// Index each face with confidence > 99%
	indexedCount := 0
	for _, detection := range detections {
		if detection.Confidence < 99.0 {
			continue
		}

		// Index face in Rekognition
		_, err := s.client.IndexFace(ctx, imageBytes, photoID)
		if err != nil {
			log.Printf("Failed to index face in photo %s: %v", photoID, err)
			continue
		}

		// Store in database
		embedding := &FaceEmbeddingInfo{
			PhotoID:     photoID,
			SpaceID:     photo.SpaceID,
			Embedding:   []float32{}, // Rekognition handles embeddings internally
			BoundingBox: fmt.Sprintf("%.2f,%.2f,%.2f,%.2f",
				detection.BoundingBox.Left,
				detection.BoundingBox.Top,
				detection.BoundingBox.Width,
				detection.BoundingBox.Height),
		}

		if _, err := s.repo.CreateFaceEmbedding(ctx, embedding); err != nil {
			log.Printf("Failed to store face embedding for photo %s: %v", photoID, err)
			continue
		}

		indexedCount++
	}

	// Mark as indexed in cache
	s.cache.Set(ctx, indexKey, "true", s.config.PhotoCacheTTL)

	log.Printf("Indexed %d faces in photo %s", indexedCount, photoID)
	return nil
}

// SearchFaces searches for faces in a space
func (s *Service) SearchFaces(ctx context.Context, spaceID string, imageBytes []byte, limit int) (*FaceSearchResult, error) {
	startTime := time.Now()

	// Check cache for similar searches
	imageHash := computeHash(imageBytes)
	cacheKey := s.keys.Search(spaceID, imageHash)

	var cachedResult FaceSearchResult
	if err := s.cache.GetJSON(ctx, cacheKey, &cachedResult); err == nil && cachedResult.Matches != nil {
		cachedResult.CacheHit = true
		cachedResult.SearchTime = time.Since(startTime)
		return &cachedResult, nil
	}

	// Search in Rekognition
	matches, err := s.client.SearchFacesByImage(ctx, imageBytes, limit, s.config.Threshold)
	if err != nil {
		return nil, fmt.Errorf("failed to search faces: %w", err)
	}

	// Get photo metadata for matches
	var resultMatches []FaceMatch
	for _, match := range matches {
		// Get photo ID from external image ID
		photoID := match.ExternalImageID
		photo, err := s.repo.GetPhoto(ctx, photoID)
		if err != nil {
			log.Printf("Failed to get photo %s: %v", photoID, err)
			continue
		}

		// Enrich match with photo data
		match.PhotoID = photoID
		match.PhotoURL = photo.URL
		match.ThumbnailURL = photo.ThumbnailURL
		resultMatches = append(resultMatches, match)
	}

	// Build result
	result := &FaceSearchResult{
		Matches:    resultMatches,
		PhotoCount: len(resultMatches),
		SearchTime: time.Since(startTime),
		CacheHit:   false,
	}

	// Cache result
	s.cache.SetJSON(ctx, cacheKey, result, s.config.SearchCacheTTL)

	return result, nil
}

// CreateCollection creates a face collection for a space
func (s *Service) CreateCollection(ctx context.Context, collectionName string) error {
	return s.client.CreateCollection(ctx, collectionName)
}

// DeleteCollection deletes a face collection for a space
func (s *Service) DeleteCollection(ctx context.Context, collectionName string) error {
	return s.client.DeleteCollection(ctx, collectionName)
}

// computeHash computes a SHA256 hash of the image bytes for caching
func computeHash(data []byte) string {
	// Use first 1KB for quick hashing (full hash is expensive for large images)
	sampleSize := 1024
	if len(data) < sampleSize {
		sampleSize = len(data)
	}
	hash := sha256.Sum256(data[:sampleSize])
	return fmt.Sprintf("%x", hash[:8]) // Use first 8 bytes for shorter key
}
