package services

import (
	"bookhoard/internal/database"
	"context"
	"log"
	"time"

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

// HashBackfillService is a one-time self-heal pass that computes and stores the
// SHA-256 for media items imported before hashing existed (file_sha256 IS
// NULL). It runs once shortly after startup, independently of auto-scan, and
// also performs a final conflict sweep that flags any content-duplicate groups
// (same library + SHA-256 at different paths) on the admin Hash Conflicts page.
//
// The sweep runs after the per-item pass because during the pass only one side
// of a preexisting duplicate pair may be hashed at a time - the group only
// becomes visible once every item has its hash.
type HashBackfillService struct {
	db     *database.Queries
	libSvc *LibraryService
}

// NewHashBackfillService creates a backfill service.
func NewHashBackfillService(db *database.Queries) *HashBackfillService {
	return &HashBackfillService{db: db, libSvc: NewLibraryService(db)}
}

// Run performs the backfill pass followed by the conflict sweep. It logs
// progress and never returns an error - failures on individual items are
// skipped so one unreadable file cannot block the rest.
func (s *HashBackfillService) Run(ctx context.Context) {
	items, err := s.db.ListMediaItemsMissingHash(ctx)
	if err != nil {
		log.Printf("[HASH-BACKFILL] failed to list items missing hash: %v", err)
		return
	}
	if len(items) == 0 {
		log.Printf("[HASH-BACKFILL] all media items already hashed, nothing to do")
		s.sweepConflicts(ctx)
		return
	}

	log.Printf("[HASH-BACKFILL] computing SHA-256 for %d unhashed media items", len(items))
	started := time.Now()
	hashed, failed := 0, 0

	for _, item := range items {
		if ctx.Err() != nil {
			log.Printf("[HASH-BACKFILL] cancelled after %d items", hashed)
			return
		}

		path, err := s.libSvc.ResolveMediaPath(ctx, item.LibraryID, item.FilePath)
		if err != nil {
			log.Printf("[HASH-BACKFILL] could not resolve path for %q: %v", item.FilePath, err)
			failed++
			continue
		}

		sha, err := computeFileSHA256(path)
		if err != nil {
			log.Printf("[HASH-BACKFILL] could not hash %q: %v", path, err)
			failed++
			continue
		}

		_, err = s.db.UpdateMediaItemIdentifiers(ctx, database.UpdateMediaItemIdentifiersParams{
			ID:             item.ID,
			FileSha256:     pgtype.Text{String: sha, Valid: true},
			HashConfidence: pgtype.Text{String: "sha256_full", Valid: true},
		})
		if err != nil {
			log.Printf("[HASH-BACKFILL] could not store hash for %q: %v", item.FilePath, err)
			failed++
			continue
		}
		hashed++

		if hashed%25 == 0 {
			log.Printf("[HASH-BACKFILL] progress: %d/%d hashed", hashed, len(items))
		}
	}

	log.Printf("[HASH-BACKFILL] done in %s: %d hashed, %d failed (of %d)",
		time.Since(started).Round(time.Second), hashed, failed, len(items))

	s.sweepConflicts(ctx)
}

// sweepConflicts flags every content-duplicate group (same library + SHA-256,
// more than one item) as a pending hash conflict. The upsert is a no-op for
// groups that are already tracked or resolved, so admins who chose "keep both"
// are never re-prompted.
func (s *HashBackfillService) sweepConflicts(ctx context.Context) {
	groups, err := s.db.FindHashConflictGroups(ctx)
	if err != nil {
		log.Printf("[HASH-BACKFILL] conflict sweep failed: %v", err)
		return
	}
	if len(groups) == 0 {
		return
	}

	flagged := 0
	for _, g := range groups {
		if err := s.db.CreateHashConflict(ctx, database.CreateHashConflictParams{
			LibraryID:  g.LibraryID,
			FileSha256: g.FileSha256.String,
		}); err != nil {
			log.Printf("[HASH-BACKFILL] could not record conflict group: %v", err)
			continue
		}
		flagged++
	}
	log.Printf("[HASH-BACKFILL] flagged %d content-duplicate group(s) for admin review", flagged)
}
