package services

import (
	"bookhoard/internal/database"
	"context"
	"encoding/json"
	"fmt"
	"strings"

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

// Rule represents an auto-assign rule for collections
type Rule struct {
	ID       string `json:"id"`
	Field    string `json:"field"`    // "genre", "series", "author", "language", "publisher", "copyright_year", "tags"
	Operator string `json:"operator"` // "equals", "contains", "starts_with", "ends_with", "greater_than", "less_than"
	Value    string `json:"value"`
	Priority int    `json:"priority"` // 1-10, higher values take precedence
}

// RuleEvaluation represents the result of evaluating a rule
type RuleEvaluation struct {
	RuleID     string  `json:"rule_id"`
	Matches    bool    `json:"matches"`
	Confidence float64 `json:"confidence"`
}

// CollectionService handles collection management
type CollectionService struct {
	db *database.Queries
}

// NewCollectionService creates a new collection service
func NewCollectionService(db *database.Queries) *CollectionService {
	return &CollectionService{
		db: db,
	}
}

// CreateCollection creates a new collection
func (s *CollectionService) CreateCollection(ctx context.Context, userID uuid.UUID, name, description, color, icon string, autoAssignRules []Rule, viewSettings map[string]any) (database.Collections, error) {
	// Convert rules to JSONB ([]byte)
	var rulesJSON []byte
	if len(autoAssignRules) > 0 {
		var err error
		rulesJSON, err = json.Marshal(autoAssignRules)
		if err != nil {
			return database.Collections{}, fmt.Errorf("failed to marshal auto-assign rules: %v", err)
		}
	}

	// Convert view settings to JSONB ([]byte)
	var settingsJSON []byte
	if len(viewSettings) > 0 {
		var err error
		settingsJSON, err = json.Marshal(viewSettings)
		if err != nil {
			return database.Collections{}, fmt.Errorf("failed to marshal view settings: %v", err)
		}
	}

	collection, err := s.db.CreateCollection(ctx, database.CreateCollectionParams{
		UserID:          pgtype.UUID{Bytes: userID, Valid: true},
		Name:            name,
		Description:     pgtype.Text{String: description, Valid: description != ""},
		Color:           pgtype.Text{String: color, Valid: color != ""},
		Icon:            pgtype.Text{String: icon, Valid: icon != ""},
		AutoAssignRules: rulesJSON,
		ViewSettings:    settingsJSON,
	})

	if err != nil {
		return database.Collections{}, fmt.Errorf("failed to create collection: %v", err)
	}

	return collection, nil
}

// GetCollection gets a collection by ID
func (s *CollectionService) GetCollection(ctx context.Context, collectionID uuid.UUID) (database.Collections, error) {
	collection, err := s.db.GetCollection(ctx, pgtype.UUID{Bytes: collectionID, Valid: true})
	if err != nil {
		return database.Collections{}, fmt.Errorf("failed to get collection: %v", err)
	}

	return collection, nil
}

// GetCollectionWithBookCount gets a collection with book count
func (s *CollectionService) GetCollectionWithBookCount(ctx context.Context, collectionID uuid.UUID) (map[string]any, error) {
	collection, err := s.db.GetCollectionWithBookCount(ctx, pgtype.UUID{Bytes: collectionID, Valid: true})
	if err != nil {
		return nil, fmt.Errorf("failed to get collection: %v", err)
	}

	return map[string]any{
		"id":                uuid.UUID(collection.ID.Bytes).String(),
		"user_id":           uuid.UUID(collection.UserID.Bytes).String(),
		"name":              collection.Name,
		"description":       collection.Description.String,
		"color":             collection.Color.String,
		"icon":              collection.Icon.String,
		"auto_assign_rules": collection.AutoAssignRules,
		"view_settings":     collection.ViewSettings,
		"created_at":        collection.CreatedAt.Time.String(),
		"book_count":        collection.BookCount,
	}, nil
}

// GetUserCollections gets all collections for a user
func (s *CollectionService) GetUserCollections(ctx context.Context, userID uuid.UUID) ([]database.Collections, error) {
	collections, err := s.db.GetCollectionsByUser(ctx, pgtype.UUID{Bytes: userID, Valid: true})
	if err != nil {
		return nil, fmt.Errorf("failed to get user collections: %v", err)
	}

	return collections, nil
}

// UpdateCollection updates a collection
func (s *CollectionService) UpdateCollection(ctx context.Context, collectionID uuid.UUID, name, description, color, icon string, autoAssignRules []Rule, viewSettings map[string]any) (database.Collections, error) {
	// Convert rules to JSONB ([]byte)
	var rulesJSON []byte
	if len(autoAssignRules) > 0 {
		var err error
		rulesJSON, err = json.Marshal(autoAssignRules)
		if err != nil {
			return database.Collections{}, fmt.Errorf("failed to marshal auto-assign rules: %v", err)
		}
	}

	// Convert view settings to JSONB ([]byte)
	var settingsJSON []byte
	if len(viewSettings) > 0 {
		var err error
		settingsJSON, err = json.Marshal(viewSettings)
		if err != nil {
			return database.Collections{}, fmt.Errorf("failed to marshal view settings: %v", err)
		}
	}

	collection, err := s.db.UpdateCollection(ctx, database.UpdateCollectionParams{
		ID:              pgtype.UUID{Bytes: collectionID, Valid: true},
		Name:            name,
		Description:     pgtype.Text{String: description, Valid: description != ""},
		Color:           pgtype.Text{String: color, Valid: color != ""},
		Icon:            pgtype.Text{String: icon, Valid: icon != ""},
		AutoAssignRules: rulesJSON,
		ViewSettings:    settingsJSON,
	})

	if err != nil {
		return database.Collections{}, fmt.Errorf("failed to update collection: %v", err)
	}

	return collection, nil
}

// DeleteCollection deletes a collection
func (s *CollectionService) DeleteCollection(ctx context.Context, collectionID uuid.UUID) error {
	err := s.db.DeleteCollection(ctx, pgtype.UUID{Bytes: collectionID, Valid: true})
	if err != nil {
		return fmt.Errorf("failed to delete collection: %v", err)
	}

	return nil
}

// AddBookToCollection adds a book to a collection
func (s *CollectionService) AddBookToCollection(ctx context.Context, collectionID, mediaItemID, userID uuid.UUID) error {
	_, err := s.db.AddBookToCollection(ctx, database.AddBookToCollectionParams{
		CollectionID:  pgtype.UUID{Bytes: collectionID, Valid: true},
		MediaItemID:   pgtype.UUID{Bytes: mediaItemID, Valid: true},
		AddedByUserID: pgtype.UUID{Bytes: userID, Valid: true},
	})

	if err != nil {
		return fmt.Errorf("failed to add book to collection: %v", err)
	}

	return nil
}

// RemoveBookFromCollection removes a book from a collection
func (s *CollectionService) RemoveBookFromCollection(ctx context.Context, collectionID, mediaItemID uuid.UUID) error {
	err := s.db.RemoveBookFromCollection(ctx, database.RemoveBookFromCollectionParams{
		CollectionID: pgtype.UUID{Bytes: collectionID, Valid: true},
		MediaItemID:  pgtype.UUID{Bytes: mediaItemID, Valid: true},
	})

	if err != nil {
		return fmt.Errorf("failed to remove book from collection: %v", err)
	}

	return nil
}

// GetCollectionBooks gets all books in a collection
func (s *CollectionService) GetCollectionBooks(ctx context.Context, collectionID uuid.UUID) ([]database.GetCollectionItemsRow, error) {
	books, err := s.db.GetCollectionItems(ctx, pgtype.UUID{Bytes: collectionID, Valid: true})
	if err != nil {
		return nil, fmt.Errorf("failed to get collection books: %v", err)
	}

	return books, nil
}

// GetBookCollections gets all collections that contain a specific book
func (s *CollectionService) GetBookCollections(ctx context.Context, mediaItemID uuid.UUID) ([]database.Collections, error) {
	collections, err := s.db.GetCollectionsForBook(ctx, pgtype.UUID{Bytes: mediaItemID, Valid: true})
	if err != nil {
		return nil, fmt.Errorf("failed to get book collections: %v", err)
	}

	return collections, nil
}

// EvaluateRules evaluates auto-assign rules for a media item
func (s *CollectionService) EvaluateRules(mediaItem database.ListMediaItemsRow, rules []Rule) []RuleEvaluation {
	var evaluations []RuleEvaluation

	// Sort rules by priority (higher priority first)
	sortedRules := make([]Rule, len(rules))
	copy(sortedRules, rules)
	for i := range len(sortedRules) {
		for j := i + 1; j < len(sortedRules); j++ {
			if sortedRules[i].Priority < sortedRules[j].Priority {
				sortedRules[i], sortedRules[j] = sortedRules[j], sortedRules[i]
			}
		}
	}

	for _, rule := range sortedRules {
		eval := RuleEvaluation{
			RuleID:  rule.ID,
			Matches: false,
		}

		switch rule.Field {
		case "genre":
			if mediaItem.Genre.Valid {
				eval.Matches = s.evaluateRule(mediaItem.Genre.String, rule.Operator, rule.Value)
			}
		case "series":
			if mediaItem.Series.Valid {
				eval.Matches = s.evaluateRule(mediaItem.Series.String, rule.Operator, rule.Value)
			}
		case "author":
			if mediaItem.Author.Valid {
				eval.Matches = s.evaluateRule(mediaItem.Author.String, rule.Operator, rule.Value)
			}
		case "language":
			if mediaItem.Language.Valid {
				eval.Matches = s.evaluateRule(mediaItem.Language.String, rule.Operator, rule.Value)
			}
		case "publisher":
			if mediaItem.Publisher.Valid {
				eval.Matches = s.evaluateRule(mediaItem.Publisher.String, rule.Operator, rule.Value)
			}
		case "copyright_year":
			if mediaItem.CopyrightYear.Valid {
				yearStr := fmt.Sprintf("%d", mediaItem.CopyrightYear.Int32)
				eval.Matches = s.evaluateRule(yearStr, rule.Operator, rule.Value)
			}
		case "tags":
			if len(mediaItem.Tags) > 0 {
				tagsStr := strings.Join(mediaItem.Tags, ", ")
				eval.Matches = s.evaluateRule(tagsStr, rule.Operator, rule.Value)
			}
		}

		// Calculate confidence based on rule type and priority
		if eval.Matches {
			baseConfidence := 0.7
			switch rule.Field {
			case "genre", "series":
				baseConfidence = 0.9
			case "author", "publisher":
				baseConfidence = 0.8
			}
			// Add priority boost (0.01 per priority point, max 0.1)
			priorityBoost := float64(rule.Priority) * 0.01
			if priorityBoost > 0.1 {
				priorityBoost = 0.1
			}
			eval.Confidence = baseConfidence + priorityBoost
			if eval.Confidence > 1.0 {
				eval.Confidence = 1.0
			}
		}

		evaluations = append(evaluations, eval)
	}

	return evaluations
}

// evaluateRule evaluates a single rule
func (s *CollectionService) evaluateRule(fieldValue, operator, ruleValue string) bool {
	switch operator {
	case "equals":
		return containsIgnoreCase(fieldValue, ruleValue) && len(fieldValue) == len(ruleValue)
	case "contains":
		return containsIgnoreCase(fieldValue, ruleValue)
	case "starts_with":
		return hasPrefixIgnoreCase(fieldValue, ruleValue)
	case "ends_with":
		return hasSuffixIgnoreCase(fieldValue, ruleValue)
	case "greater_than":
		return compareNumbers(fieldValue, ruleValue, ">")
	case "less_than":
		return compareNumbers(fieldValue, ruleValue, "<")
	default:
		return false
	}
}

// containsIgnoreCase checks if a string contains another (case-insensitive)
func containsIgnoreCase(s, substr string) bool {
	s = toLower(s)
	substr = toLower(substr)
	return contains(s, substr)
}

// hasPrefixIgnoreCase checks if a string starts with a prefix (case-insensitive)
func hasPrefixIgnoreCase(s, prefix string) bool {
	s = toLower(s)
	prefix = toLower(prefix)
	return hasPrefix(s, prefix)
}

// hasSuffixIgnoreCase checks if a string ends with a suffix (case-insensitive)
func hasSuffixIgnoreCase(s, suffix string) bool {
	s = toLower(s)
	suffix = toLower(suffix)
	return hasSuffix(s, suffix)
}

// compareNumbers compares two numeric strings
func compareNumbers(a, b, op string) bool {
	aFloat, errA := parseFloat(a)
	bFloat, errB := parseFloat(b)

	if errA != nil || errB != nil {
		return false
	}

	switch op {
	case ">":
		return aFloat > bFloat
	case "<":
		return aFloat < bFloat
	case ">=":
		return aFloat >= bFloat
	case "<=":
		return aFloat <= bFloat
	default:
		return false
	}
}

// CreateDeviceShelfMapping creates a shelf mapping for a device
func (s *CollectionService) CreateDeviceShelfMapping(ctx context.Context, collectionID, deviceID uuid.UUID, deviceShelfName, syncDirection string) (database.DeviceShelfMappings, error) {
	mapping, err := s.db.CreateDeviceShelfMapping(ctx, database.CreateDeviceShelfMappingParams{
		CollectionID:    pgtype.UUID{Bytes: collectionID, Valid: true},
		DeviceID:        pgtype.UUID{Bytes: deviceID, Valid: true},
		DeviceShelfName: pgtype.Text{String: deviceShelfName, Valid: deviceShelfName != ""},
		SyncDirection:   pgtype.Text{String: syncDirection, Valid: syncDirection != ""},
	})

	if err != nil {
		return database.DeviceShelfMappings{}, fmt.Errorf("failed to create shelf mapping: %v", err)
	}

	return mapping, nil
}

// GetDeviceShelfMappings gets all shelf mappings for a device
func (s *CollectionService) GetDeviceShelfMappings(ctx context.Context, deviceID uuid.UUID) ([]database.GetDeviceShelfMappingsRow, error) {
	mappings, err := s.db.GetDeviceShelfMappings(ctx, pgtype.UUID{Bytes: deviceID, Valid: true})
	if err != nil {
		return nil, fmt.Errorf("failed to get shelf mappings: %v", err)
	}

	return mappings, nil
}

// UpdateDeviceShelfMapping updates a shelf mapping
func (s *CollectionService) UpdateDeviceShelfMapping(ctx context.Context, mappingID uuid.UUID, deviceShelfName, syncDirection string) (database.DeviceShelfMappings, error) {
	mapping, err := s.db.UpdateDeviceShelfMapping(ctx, database.UpdateDeviceShelfMappingParams{
		ID:              pgtype.UUID{Bytes: mappingID, Valid: true},
		DeviceShelfName: pgtype.Text{String: deviceShelfName, Valid: deviceShelfName != ""},
		SyncDirection:   pgtype.Text{String: syncDirection, Valid: syncDirection != ""},
	})

	if err != nil {
		return database.DeviceShelfMappings{}, fmt.Errorf("failed to update shelf mapping: %v", err)
	}

	return mapping, nil
}

// DeleteDeviceShelfMapping deletes a shelf mapping
func (s *CollectionService) DeleteDeviceShelfMapping(ctx context.Context, mappingID uuid.UUID) error {
	err := s.db.DeleteDeviceShelfMapping(ctx, pgtype.UUID{Bytes: mappingID, Valid: true})
	if err != nil {
		return fmt.Errorf("failed to delete shelf mapping: %v", err)
	}

	return nil
}

// ApplyAutoAssignRules applies auto-assign rules for all collections to a media item
func (s *CollectionService) ApplyAutoAssignRules(ctx context.Context, mediaItem database.ListMediaItemsRow, userID uuid.UUID) ([]uuid.UUID, error) {
	var matchedCollectionIDs []uuid.UUID

	collections, err := s.GetUserCollections(ctx, userID)
	if err != nil {
		return nil, fmt.Errorf("failed to get user collections: %v", err)
	}

	for _, collection := range collections {
		if len(collection.AutoAssignRules) == 0 {
			continue
		}

		var rules []Rule
		if err := json.Unmarshal(collection.AutoAssignRules, &rules); err != nil {
			continue
		}

		evaluations := s.EvaluateRules(mediaItem, rules)

		// Check if any rules match
		for _, eval := range evaluations {
			if eval.Matches && eval.Confidence > 0.7 {
				matchedCollectionIDs = append(matchedCollectionIDs, uuid.UUID(collection.ID.Bytes))
				break
			}
		}
	}

	return matchedCollectionIDs, nil
}

// Helper functions (Go standard library functions)
func toLower(s string) string {
	if len(s) == 0 {
		return s
	}

	result := make([]byte, len(s))
	for i := 0; i < len(s); i++ {
		c := s[i]
		if c >= 'A' && c <= 'Z' {
			result[i] = c + 32
		} else {
			result[i] = c
		}
	}
	return string(result)
}

func contains(s, substr string) bool {
	return len(s) >= len(substr) && indexOf(s, substr) >= 0
}

func hasPrefix(s, prefix string) bool {
	return len(s) >= len(prefix) && s[:len(prefix)] == prefix
}

func hasSuffix(s, suffix string) bool {
	return len(s) >= len(suffix) && s[len(s)-len(suffix):] == suffix
}

func indexOf(s, substr string) int {
	for i := 0; i <= len(s)-len(substr); i++ {
		if s[i:i+len(substr)] == substr {
			return i
		}
	}
	return -1
}

func parseFloat(s string) (float64, error) {
	var result float64
	var sign float64 = 1
	var divisor float64 = 1
	var decimalPlaces = 0
	var seenDecimal bool

	// Skip leading whitespace
	start := 0
	for start < len(s) && (s[start] == ' ' || s[start] == '\t' || s[start] == '\n') {
		start++
	}

	// Handle sign
	if start < len(s) && s[start] == '-' {
		sign = -1
		start++
	} else if start < len(s) && s[start] == '+' {
		start++
	}

	// Parse digits
	for i := start; i < len(s); i++ {
		c := s[i]
		if c >= '0' && c <= '9' {
			result = result*10 + float64(c-'0')
			if seenDecimal {
				decimalPlaces++
				divisor *= 10
			}
		} else if c == '.' && !seenDecimal {
			seenDecimal = true
		} else {
			break
		}
	}

	return sign * result / divisor, nil
}
