package services

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

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

func mediaItemsToListMediaItemsRow(item database.MediaItems) database.ListMediaItemsRow {
	return database.ListMediaItemsRow{
		ID:                 item.ID,
		LibraryID:          item.LibraryID,
		Title:              item.Title,
		Author:             item.Author,
		Isbn:               item.Isbn,
		Description:        item.Description,
		FilePath:           utils.ResolveMediaURL(item.LibraryID, pgtype.Text{String: item.FilePath, Valid: true}),
		FileSize:           item.FileSize,
		MimeType:           item.MimeType,
		CoverImagePath:     pgtype.Text{String: utils.ResolveMediaURL(item.LibraryID, item.CoverImagePath), Valid: true},
		Series:             item.Series,
		SeriesNumber:       item.SeriesNumber,
		Tags:               item.Tags,
		Asin:               item.Asin,
		DatePublished:      item.DatePublished,
		Publisher:          item.Publisher,
		Contributors:       item.Contributors,
		Language:           item.Language,
		Edition:            item.Edition,
		PageCount:          item.PageCount,
		Genre:              item.Genre,
		CopyrightYear:      item.CopyrightYear,
		GoodreadsID:        item.GoodreadsID,
		OpenlibraryID:      item.OpenlibraryID,
		GoogleBooksID:      item.GoogleBooksID,
		AddedByAdminID:     item.AddedByAdminID,
		CreatedAt:          item.CreatedAt,
		UpdatedAt:          item.UpdatedAt,
		FormatGroup:        item.FormatGroup,
		FormatMimetype:     item.FormatMimetype,
		IsReflowable:       item.IsReflowable,
		HasFixedLayout:     item.HasFixedLayout,
		TotalCharacters:    item.TotalCharacters,
		ChapterCount:       item.ChapterCount,
		EntitlementID:      item.EntitlementID,
		RevisionNumber:     item.RevisionNumber,
		KoboContentID:      item.KoboContentID,
		KoboMetadata:       item.KoboMetadata,
		TagsSearch:         item.TagsSearch,
		ContributorsSearch: item.ContributorsSearch,
		FileSha256:         item.FileSha256,
		OpfIdentifier:      item.OpfIdentifier,
		OpfUuid:            item.OpfUuid,
		HashConfidence:     item.HashConfidence,
	}
}

func getCollectionItemsRowToMediaItems(item database.GetCollectionItemsForDashboardRow) database.MediaItems {
	return database.MediaItems{
		ID:                 item.ID,
		LibraryID:          item.LibraryID,
		Title:              item.Title,
		Author:             item.Author,
		Isbn:               item.Isbn,
		Description:        item.Description,
		FilePath:           utils.ResolveMediaURL(item.LibraryID, pgtype.Text{String: item.FilePath, Valid: true}),
		FileSize:           item.FileSize,
		MimeType:           item.MimeType,
		CoverImagePath:     pgtype.Text{String: utils.ResolveMediaURL(item.LibraryID, item.CoverImagePath), Valid: true},
		Series:             item.Series,
		SeriesNumber:       item.SeriesNumber,
		Tags:               item.Tags,
		Asin:               item.Asin,
		DatePublished:      item.DatePublished,
		Publisher:          item.Publisher,
		Contributors:       item.Contributors,
		Language:           item.Language,
		Edition:            item.Edition,
		PageCount:          item.PageCount,
		Genre:              item.Genre,
		CopyrightYear:      item.CopyrightYear,
		GoodreadsID:        item.GoodreadsID,
		OpenlibraryID:      item.OpenlibraryID,
		GoogleBooksID:      item.GoogleBooksID,
		AddedByAdminID:     item.AddedByAdminID,
		CreatedAt:          item.CreatedAt,
		UpdatedAt:          item.UpdatedAt,
		FormatGroup:        item.FormatGroup,
		FormatMimetype:     item.FormatMimetype,
		IsReflowable:       item.IsReflowable,
		HasFixedLayout:     item.HasFixedLayout,
		TotalCharacters:    item.TotalCharacters,
		ChapterCount:       item.ChapterCount,
		EntitlementID:      item.EntitlementID,
		RevisionNumber:     item.RevisionNumber,
		KoboContentID:      item.KoboContentID,
		KoboMetadata:       item.KoboMetadata,
		TagsSearch:         item.TagsSearch,
		ContributorsSearch: item.ContributorsSearch,
		FileSha256:         item.FileSha256,
		OpfIdentifier:      item.OpfIdentifier,
		OpfUuid:            item.OpfUuid,
		HashConfidence:     item.HashConfidence,
	}
}

type DashboardService struct {
	db                *database.Queries
	collectionService *CollectionService
}

func NewDashboardService(db *database.Queries) *DashboardService {
	return &DashboardService{
		db:                db,
		collectionService: NewCollectionService(db),
	}
}

type DashboardSection struct {
	CollectionID   uuid.UUID
	CollectionName string
	Items          []database.MediaItems
	QueryType      string
	Priority       int
	IsSystem       bool
	Title          string
	Description    string
	Icon           string
}

func (s *DashboardService) GetDashboardSections(
	ctx context.Context,
	userID uuid.UUID,
	libraryID pgtype.UUID,
	limit int,
	collectionOrder []string,
	hiddenCollections []string,
) ([]DashboardSection, error) {
	var results []DashboardSection

	systemCollections, err := s.db.GetSystemCollectionsForDashboard(ctx, pgtype.UUID{Bytes: userID, Valid: true})
	if err != nil {
		return nil, err
	}

	for _, coll := range systemCollections {
		items, err := s.getCollectionItemsByQueryType(ctx, coll, userID, libraryID, limit)
		if err != nil {
			continue
		}

		results = append(results, DashboardSection{
			CollectionID:   coll.ID.Bytes,
			CollectionName: coll.Name,
			Items:          items,
			QueryType:      coll.QueryType.String,
			Priority:       int(coll.Priority.Int32),
			IsSystem:       coll.IsSystemCollection.Bool,
			Title:          coll.Name,
			Description:    coll.Description.String,
			Icon:           coll.Icon.String,
		})
	}

	userCollections, err := s.db.GetUserCollectionsForDashboard(ctx, pgtype.UUID{Bytes: userID, Valid: true})
	if err != nil {
		return nil, err
	}

	for _, coll := range userCollections {
		items, err := s.getUserCollectionItems(ctx, coll, userID, libraryID, limit)
		if err != nil {
			continue
		}

		if len(items) == 0 {
			continue
		}

		results = append(results, DashboardSection{
			CollectionID:   coll.ID.Bytes,
			CollectionName: coll.Name,
			Items:          items,
			QueryType:      coll.QueryType.String,
			Priority:       int(coll.Priority.Int32),
			IsSystem:       false,
			Title:          coll.Name,
			Description:    coll.Description.String,
			Icon:           coll.Icon.String,
		})
	}

	results = s.FilterHiddenCollections(results, hiddenCollections)
	results = s.reorderCollections(results, collectionOrder)

	if len(collectionOrder) == 0 {
		results = s.sortByPriority(results)
	}

	return results, nil
}

func (s *DashboardService) FilterHiddenCollections(sections []DashboardSection, hidden []string) []DashboardSection {
	if len(hidden) == 0 {
		return sections
	}

	var filtered []DashboardSection
	for _, section := range sections {
		isHidden := false
		for _, h := range hidden {
			if section.CollectionName == h {
				isHidden = true
				break
			}
		}
		if !isHidden {
			filtered = append(filtered, section)
		}
	}
	return filtered
}

func (s *DashboardService) reorderCollections(sections []DashboardSection, order []string) []DashboardSection {
	if len(order) == 0 {
		return sections
	}

	var ordered []DashboardSection
	remaining := make(map[string]DashboardSection)
	for _, section := range sections {
		remaining[section.CollectionName] = section
	}

	for _, name := range order {
		if section, exists := remaining[name]; exists {
			ordered = append(ordered, section)
			delete(remaining, name)
		}
	}

	for _, section := range sections {
		if _, exists := remaining[section.CollectionName]; exists {
			ordered = append(ordered, section)
		}
	}

	return ordered
}

func (s *DashboardService) sortByPriority(sections []DashboardSection) []DashboardSection {
	sorted := make([]DashboardSection, len(sections))
	copy(sorted, sections)

	for i := 0; i < len(sorted)-1; i++ {
		for j := 0; j < len(sorted)-i-1; j++ {
			if sorted[j].Priority > sorted[j+1].Priority {
				sorted[j], sorted[j+1] = sorted[j+1], sorted[j]
			}
		}
	}

	return sorted
}

func (s *DashboardService) getCollectionItemsByQueryType(ctx context.Context, coll database.Collections, userID uuid.UUID, libraryID pgtype.UUID, limit int) ([]database.MediaItems, error) {
	switch coll.QueryType.String {
	case "continue-reading":
		return s.db.GetContinueReadingItems(ctx, database.GetContinueReadingItemsParams{
			UserID:    pgtype.UUID{Bytes: userID, Valid: true},
			LibraryID: libraryID,
			Limit:     pgtype.Int4{Int32: int32(limit), Valid: true},
		})
	case "recently-added":
		return s.db.GetRecentlyAddedItems(ctx, database.GetRecentlyAddedItemsParams{
			LibraryID: libraryID,
			Limit:     pgtype.Int4{Int32: int32(limit), Valid: true},
		})
	case "recently-read":
		return s.db.GetRecentlyReadItems(ctx, database.GetRecentlyReadItemsParams{
			UserID:    pgtype.UUID{Bytes: userID, Valid: true},
			LibraryID: libraryID,
			Limit:     pgtype.Int4{Int32: int32(limit), Valid: true},
		})
	case "not-started":
		return s.db.GetNotStartedItems(ctx, database.GetNotStartedItemsParams{
			UserID:    pgtype.UUID{Bytes: userID, Valid: true},
			LibraryID: libraryID,
			Limit:     pgtype.Int4{Int32: int32(limit), Valid: true},
		})
	case "continue-series":
		rows, err := s.db.GetContinueSeriesItems(ctx, database.GetContinueSeriesItemsParams{
			LibraryID: libraryID,
			UserID:    pgtype.UUID{Bytes: userID, Valid: true},
			Limit:     pgtype.Int4{Int32: int32(limit), Valid: true},
		})
		if err != nil {
			return nil, err
		}
		items := make([]database.MediaItems, 0, len(rows))
		for _, row := range rows {
			items = append(items, continueSeriesRowToMediaItems(row))
		}
		return items, nil
	default:
		return []database.MediaItems{}, nil
	}
}

func (s *DashboardService) getUserCollectionItems(ctx context.Context, coll database.Collections, userID uuid.UUID, libraryID pgtype.UUID, limit int) ([]database.MediaItems, error) {
	collUUID, _ := uuid.FromBytes(coll.ID.Bytes[0:16])

	manualItems, err := s.db.GetCollectionItemsForDashboard(ctx, database.GetCollectionItemsForDashboardParams{
		CollectionID: pgtype.UUID{Bytes: collUUID, Valid: true},
		LibraryID:    libraryID,
		Limit:        pgtype.Int4{Int32: int32(limit), Valid: true},
	})
	if err != nil {
		return nil, err
	}

	var manualNonExcluded []database.MediaItems
	for _, item := range manualItems {
		if !item.Excluded.Valid || !item.Excluded.Bool {
			manualNonExcluded = append(manualNonExcluded, getCollectionItemsRowToMediaItems(item))
		}
	}

	var autoItems []database.MediaItems
	if len(coll.AutoAssignRules) > 0 {
		var rules []Rule
		if err := json.Unmarshal(coll.AutoAssignRules, &rules); err == nil && len(rules) > 0 {
			allLibraryItems, err := s.db.GetLibraryItems(ctx, libraryID)
			if err == nil {
				for _, item := range allLibraryItems {
					alreadyInCollection := false
					for _, manualItem := range manualNonExcluded {
						manualUUID := uuid.UUID(manualItem.ID.Bytes)
						itemUUID := uuid.UUID(item.ID.Bytes)
						if manualUUID == itemUUID {
							alreadyInCollection = true
							break
						}
					}
					if alreadyInCollection {
						continue
					}

					listItem := mediaItemsToListMediaItemsRow(item)
					evaluations := s.collectionService.EvaluateRules(listItem, rules)
					for _, eval := range evaluations {
						if eval.Matches {
							autoItems = append(autoItems, item)
							break
						}
					}
				}
			}
		}
	}

	var finalItems []database.MediaItems
	finalItems = append(finalItems, manualNonExcluded...)
	finalItems = append(finalItems, autoItems...)

	if len(finalItems) > limit {
		finalItems = finalItems[:limit]
	}

	return finalItems, nil
}

func (s *DashboardService) GetDashboardPreferences(ctx context.Context, userID uuid.UUID, libraryID pgtype.UUID) (database.UserDashboardPreferences, error) {
	return s.db.GetDashboardPreferences(ctx, database.GetDashboardPreferencesParams{
		UserID:    pgtype.UUID{Bytes: userID, Valid: true},
		LibraryID: libraryID,
	})
}

func (s *DashboardService) UpsertDashboardPreferences(ctx context.Context, params database.UpsertDashboardPreferencesParams) (database.UserDashboardPreferences, error) {
	return s.db.UpsertDashboardPreferences(ctx, params)
}

func (s *DashboardService) SanitizeDashboardPreferences(hiddenCollections, collectionOrder []string) ([]string, []string) {
	// Deduplicate collection_order while preserving order
	seen := make(map[string]bool)
	var sanitizedOrder []string
	for _, id := range collectionOrder {
		if !seen[id] {
			seen[id] = true
			sanitizedOrder = append(sanitizedOrder, id)
		}
	}

	// Deduplicate hidden_collections
	seen = make(map[string]bool)
	var sanitizedHidden []string
	for _, id := range hiddenCollections {
		if !seen[id] {
			seen[id] = true
			sanitizedHidden = append(sanitizedHidden, id)
		}
	}

	return sanitizedHidden, sanitizedOrder
}

func (s *DashboardService) RestoreSystemCollection(ctx context.Context, userID uuid.UUID, collectionName string, resetType string) error {
	defaultMetadata := map[string]struct {
		Description string
		Icon        string
		Color       string
		Priority    int32
		QueryType   string
	}{
		"Continue Reading": {"Books you're currently reading (0 < progress < 1)", "📖", "#7aa2f7", 1, "continue-reading"},
		"Recently Added":   {"Newly added items to this library", "🆕", "#9ece6a", 2, "recently-added"},
		"Recently Read":    {"Books you've finished (progress >= 1)", "✅", "#e0af68", 3, "recently-read"},
		"Not Started":      {"Books you haven't read yet (progress = 0 or no record)", "📕", "#f7768e", 4, "not-started"},
		"Continue Series":  {"Next book in series you're reading", "📚", "#bb9af7", 5, "continue-series"},
	}

	meta, exists := defaultMetadata[collectionName]
	if !exists {
		return fmt.Errorf("unknown collection: %s", collectionName)
	}

	if resetType == "keep_books" {
		return s.db.ResetSystemCollectionMetadata(ctx, database.ResetSystemCollectionMetadataParams{
			UserID:      pgtype.UUID{Bytes: userID, Valid: true},
			Name:        collectionName,
			Description: pgtype.Text{String: meta.Description, Valid: true},
			Icon:        pgtype.Text{String: meta.Icon, Valid: true},
			Color:       pgtype.Text{String: meta.Color, Valid: true},
			Priority:    pgtype.Int4{Int32: meta.Priority, Valid: true},
		})
	}

	err := s.db.DeleteUserSystemCollection(ctx, database.DeleteUserSystemCollectionParams{
		UserID: pgtype.UUID{Bytes: userID, Valid: true},
		Name:   collectionName,
	})
	if err != nil {
		return err
	}

	_, err = s.db.CreateSystemCollection(ctx, database.CreateSystemCollectionParams{
		UserID:          pgtype.UUID{Bytes: userID, Valid: true},
		Name:            collectionName,
		Description:     pgtype.Text{String: meta.Description, Valid: true},
		Icon:            pgtype.Text{String: meta.Icon, Valid: true},
		Color:           pgtype.Text{String: meta.Color, Valid: true},
		ShowOnDashboard: pgtype.Bool{Bool: true, Valid: true},
		QueryType:       pgtype.Text{String: meta.QueryType, Valid: true},
		Priority:        pgtype.Int4{Int32: meta.Priority, Valid: true},
	})
	return err
}
