package services

import (
	"bookhoard/internal/database"
	"context"
	"fmt"
	"math"

	"github.com/google/uuid"
	"github.com/jackc/pgx/v5/pgtype"
)

// BookMatch represents a potential match with confidence score
type BookMatch struct {
	MediaItemID   uuid.UUID `json:"media_item_id"`
	BookhoardUUID uuid.UUID `json:"bookhoard_uuid"`
	Confidence    float64   `json:"confidence"`
	MatchMethod   string    `json:"match_method"`
}

// BookQueryRequest represents a book query from a device
type BookQueryRequest struct {
	Identifiers []string `json:"identifiers"` // ["isbn:...", "uuid:...", "opf_uuid:..."]
	SHA256      string   `json:"sha256"`
	Title       string   `json:"title"`
	Author      string   `json:"author"`
	FileSize    int64    `json:"file_size"`
}

// BookQueryResponse represents the response to a book query
type BookQueryResponse struct {
	Matches []BookMatch `json:"matches"`
	Action  string      `json:"action"` // "auto_link", "multiple_matches", "no_match"
}

// LinkBookRequest represents a manual linking request
type LinkBookRequest struct {
	DeviceFile struct {
		FilePath string `json:"file_path"`
		SHA256   string `json:"sha256"`
		Title    string `json:"title"`
	} `json:"device_file"`
	MediaItemID     uuid.UUID `json:"media_item_id"`
	ConfidenceScore float64   `json:"confidence_score"`
}

// BookMatchingService handles universal book matching
type BookMatchingService struct {
	db       *database.Queries
	resolver *BookResolver
}

// NewBookMatchingService creates a new book matching service
func NewBookMatchingService(db *database.Queries) *BookMatchingService {
	return &BookMatchingService{
		db:       db,
		resolver: NewBookResolver(db),
	}
}

// QueryBooks queries for a book using multiple identifier types with confidence scoring
func (s *BookMatchingService) QueryBooks(ctx context.Context, req *BookQueryRequest) (*BookQueryResponse, error) {
	var matches []BookMatch

	// Priority 1: Bookhoard UUID (canonical) - Confidence: 1.0
	if match := s.matchByBookhoardUUID(ctx, req.Identifiers); match != nil {
		matches = append(matches, *match)
	}

	// Priority 2: OPF UUID (from EPUB metadata) - Confidence: 0.95
	if match := s.matchByOPFUUID(ctx, req.Identifiers); match != nil {
		matches = append(matches, *match)
	}

	// Priority 3: SHA-256 hash - Confidence: 0.9
	if req.SHA256 != "" {
		if match := s.matchBySHA256(ctx, req.SHA256); match != nil {
			matches = append(matches, *match)
		}
	}

	// Priority 4: OPF identifier (non-UUID) - Confidence: 0.85
	if match := s.matchByOPFIdentifier(ctx, req.Identifiers); match != nil {
		matches = append(matches, *match)
	}

	// Priority 5: ISBN/ASIN - Confidence: 0.8
	if match := s.matchByISBNASIN(ctx, req.Identifiers); match != nil {
		matches = append(matches, *match)
	}

	// Priority 6: File path (device-specific) - Need device_id for this
	// This will be handled at API layer with device context

	// Priority 7: Title + author + file size - Confidence: 0.5
	if req.Title != "" && req.Author != "" && req.FileSize > 0 {
		if matches := s.matchByTitleAuthorSize(ctx, req.Title, req.Author, req.FileSize); len(matches) > 0 {
			matches = append(matches, matches...)
		}
	}

	// Priority 8: Title only (last resort) - Confidence: 0.3
	if req.Title != "" && len(matches) == 0 {
		if matches := s.matchByTitleOnly(ctx, req.Title); len(matches) > 0 {
			matches = append(matches, matches...)
		}
	}

	// Determine action
	action := s.determineAction(matches)

	return &BookQueryResponse{
		Matches: matches,
		Action:  action,
	}, nil
}

// matchByBookhoardUUID attempts to match by Bookhoard UUID
func (s *BookMatchingService) matchByBookhoardUUID(ctx context.Context, identifiers []string) *BookMatch {
	for _, id := range identifiers {
		if len(id) > 4 && id[:4] == "uuid:" {
			uuidStr := id[5:]
			parsedUUID, err := uuid.Parse(uuidStr)
			if err != nil {
				continue
			}

			// Check if media item exists
			item, err := s.db.GetMediaItem(ctx, pgtype.UUID{Bytes: parsedUUID, Valid: true})
			if err == nil {
				mediaUUID := item.ID.Bytes
				return &BookMatch{
					MediaItemID:   mediaUUID,
					BookhoardUUID: mediaUUID,
					Confidence:    1.0,
					MatchMethod:   "uuid_match",
				}
			}
		}
	}
	return nil
}

// matchByOPFUUID attempts to match by OPF UUID
func (s *BookMatchingService) matchByOPFUUID(ctx context.Context, identifiers []string) *BookMatch {
	for _, id := range identifiers {
		if len(id) > 8 && id[:8] == "opf_uuid:" {
			uuidStr := id[9:]
			items, err := s.db.ListMediaItems(ctx, database.ListMediaItemsParams{
				Limit:  100,
				Offset: 0,
			})
			if err != nil {
				continue
			}

			for _, item := range items {
				if item.OpfUuid.Valid && item.OpfUuid.String == uuidStr {
					return &BookMatch{
						MediaItemID:   item.ID.Bytes,
						BookhoardUUID: item.ID.Bytes,
						Confidence:    0.95,
						MatchMethod:   "opf_uuid_match",
					}
				}
			}
		}
	}
	return nil
}

// matchBySHA256 attempts to match by file SHA-256 hash.
// Uses the shared BookResolver so it is both indexed (no full-table scan) and
// format-aware: a converted/alternate format hash (media_item_formats) matches
// in addition to the primary media_items.file_sha256.
func (s *BookMatchingService) matchBySHA256(ctx context.Context, sha256 string) *BookMatch {
	item, method, err := s.resolver.ResolveBySHA256(ctx, sha256)
	if err != nil || !item.ID.Valid {
		return nil
	}
	return &BookMatch{
		MediaItemID:   item.ID.Bytes,
		BookhoardUUID: item.ID.Bytes,
		Confidence:    0.9,
		MatchMethod:   "sha256_" + string(method),
	}
}

// matchByOPFIdentifier attempts to match by OPF identifier
func (s *BookMatchingService) matchByOPFIdentifier(ctx context.Context, identifiers []string) *BookMatch {
	for _, id := range identifiers {
		if len(id) > 15 && id[:15] == "opf_identifier:" {
			identifier := id[16:]
			items, err := s.db.ListMediaItems(ctx, database.ListMediaItemsParams{
				Limit:  1000,
				Offset: 0,
			})
			if err != nil {
				continue
			}

			for _, item := range items {
				if item.OpfIdentifier.Valid && item.OpfIdentifier.String == identifier {
					return &BookMatch{
						MediaItemID:   item.ID.Bytes,
						BookhoardUUID: item.ID.Bytes,
						Confidence:    0.85,
						MatchMethod:   "opf_identifier_match",
					}
				}
			}
		}
	}
	return nil
}

// matchByISBNASIN attempts to match by ISBN or ASIN
func (s *BookMatchingService) matchByISBNASIN(ctx context.Context, identifiers []string) *BookMatch {
	for _, id := range identifiers {
		if len(id) > 5 && id[:5] == "isbn:" {
			isbn := id[6:]
			items, err := s.db.ListMediaItems(ctx, database.ListMediaItemsParams{
				Limit:  1000,
				Offset: 0,
			})
			if err != nil {
				continue
			}

			for _, item := range items {
				if item.Isbn.Valid && item.Isbn.String == isbn {
					return &BookMatch{
						MediaItemID:   item.ID.Bytes,
						BookhoardUUID: item.ID.Bytes,
						Confidence:    0.8,
						MatchMethod:   "isbn_match",
					}
				}
			}
		}

		if len(id) > 5 && id[:5] == "asin:" {
			asin := id[6:]
			items, err := s.db.ListMediaItems(ctx, database.ListMediaItemsParams{
				Limit:  1000,
				Offset: 0,
			})
			if err != nil {
				continue
			}

			for _, item := range items {
				if item.Asin.Valid && item.Asin.String == asin {
					return &BookMatch{
						MediaItemID:   item.ID.Bytes,
						BookhoardUUID: item.ID.Bytes,
						Confidence:    0.8,
						MatchMethod:   "asin_match",
					}
				}
			}
		}
	}
	return nil
}

// matchByTitleAuthorSize attempts to match by title, author, and file size
func (s *BookMatchingService) matchByTitleAuthorSize(ctx context.Context, title, author string, fileSize int64) []BookMatch {
	var matches []BookMatch

	items, err := s.db.ListMediaItems(ctx, database.ListMediaItemsParams{
		Limit:  1000,
		Offset: 0,
	})
	if err != nil {
		return matches
	}

	for _, item := range items {
		// Check title match (case-insensitive)
		titleMatch := item.Title == title

		// Check author match (case-insensitive)
		authorMatch := item.Author.Valid && item.Author.String == author

		// Check file size within 10%
		sizeMatch := item.FileSize.Valid && math.Abs(float64(item.FileSize.Int64-fileSize))/float64(fileSize) <= 0.1

		if titleMatch && authorMatch && sizeMatch {
			matches = append(matches, BookMatch{
				MediaItemID:   item.ID.Bytes,
				BookhoardUUID: item.ID.Bytes,
				Confidence:    0.5,
				MatchMethod:   "title_author_size_match",
			})
		}
	}

	return matches
}

// matchByTitleOnly attempts to match by title only (last resort)
func (s *BookMatchingService) matchByTitleOnly(ctx context.Context, title string) []BookMatch {
	var matches []BookMatch

	items, err := s.db.ListMediaItems(ctx, database.ListMediaItemsParams{
		Limit:  1000,
		Offset: 0,
	})
	if err != nil {
		return matches
	}

	for _, item := range items {
		if item.Title == title {
			matches = append(matches, BookMatch{
				MediaItemID:   item.ID.Bytes,
				BookhoardUUID: item.ID.Bytes,
				Confidence:    0.3,
				MatchMethod:   "title_match",
			})
		}
	}

	return matches
}

// determineAction determines the action to take based on matches
func (s *BookMatchingService) determineAction(matches []BookMatch) string {
	if len(matches) == 0 {
		return "no_match"
	}

	if len(matches) == 1 {
		// Auto-link if confidence is high enough (> 0.7)
		if matches[0].Confidence > 0.7 {
			return "auto_link"
		}
	}

	return "multiple_matches"
}

// LinkBook manually links a device file to a media item
func (s *BookMatchingService) LinkBook(ctx context.Context, deviceID uuid.UUID, req *LinkBookRequest) (*database.DeviceFileAliases, error) {
	// Create device file alias
	alias, err := s.db.CreateDeviceFileAlias(ctx, database.CreateDeviceFileAliasParams{
		MediaItemID:     pgtype.UUID{Bytes: req.MediaItemID, Valid: true},
		DeviceID:        pgtype.UUID{Bytes: deviceID, Valid: true},
		FilePath:        req.DeviceFile.FilePath,
		FileSha256:      pgtype.Text{String: req.DeviceFile.SHA256, Valid: req.DeviceFile.SHA256 != ""},
		ConfidenceScore: pgtype.Float8{Float64: req.ConfidenceScore, Valid: true},
	})

	if err != nil {
		return nil, fmt.Errorf("failed to create device file alias: %v", err)
	}

	return &alias, nil
}

// GetUnlinkedBooks returns books that need manual linking
func (s *BookMatchingService) GetUnlinkedBooks(ctx context.Context, deviceID uuid.UUID) ([]map[string]interface{}, error) {
	// Get device file aliases that don't have media_item_id set
	// This is a placeholder - actual implementation would query progress records
	// that exist without matching media items

	return []map[string]interface{}{}, nil
}
