package library

import (
	"strconv"
	"strings"
	"sync"
	"time"
)

// ScanMetrics holds timing and count data collected during a library scan.
// Worker-pool fields are protected by a mutex; DB-writer fields are
// single-threaded and use plain addition.
type ScanMetrics struct {
	mu sync.Mutex

	// Top-level phases (wall-clock).
	Total               time.Duration `json:"total"`
	LoadExisting        time.Duration `json:"loadExisting"`
	WalkDuration        time.Duration `json:"walkDuration"`
	ExtractionWallClock time.Duration `json:"extractionWallClock"`
	DBWritesWallClock   time.Duration `json:"dbWritesWallClock"`
	OrphanCleanup       time.Duration `json:"orphanCleanup"`

	// Per-format extraction (cumulative across workers).
	FormatExtraction map[string]int64 `json:"formatExtraction"`
	FormatCount      map[string]int64 `json:"formatCount"`

	// Sub-operation cumulative times (across workers).
	TagExtraction      time.Duration `json:"tagExtraction"`
	DurationExtraction time.Duration `json:"durationExtraction"`

	// DB sub-operations (cumulative, single-threaded DB writer).
	BatchCommits time.Duration `json:"batchCommits"`
	CoverArtSave time.Duration `json:"coverArtSave"`

	// Thumbnail generation (async worker pool).
	ThumbnailWallClock  time.Duration `json:"thumbnailWallClock"`
	ThumbnailGeneration time.Duration `json:"thumbnailGeneration"`
	ThumbnailSmall      time.Duration `json:"thumbnailSmall"`
	ThumbnailMedium     time.Duration `json:"thumbnailMedium"`
	ThumbnailLarge      time.Duration `json:"thumbnailLarge"`

	// Full-rescan-specific phases.
	ClearQueue      time.Duration `json:"clearQueue"`
	ClearDatabase   time.Duration `json:"clearDatabase"`
	ClearCoverFiles time.Duration `json:"clearCoverFiles"`

	// File counts.
	Added   int64 `json:"added"`
	Updated int64 `json:"updated"`
	Skipped int64 `json:"skipped"`
	Removed int64 `json:"removed"`

	// Cancelled is true when the scan was stopped via CancelScan.
	Cancelled bool `json:"cancelled"`

	// Library identification.
	LibraryID   int64  `json:"libraryId"`   // library that was scanned
	LibraryName string `json:"libraryName"` // display name of scanned library

	// Non-fatal issues encountered during scanning.
	Warnings []ScanWarning `json:"warnings"`

	// onWarning, when set, receives each warning as it is recorded so
	// the scan's job log can show problems live instead of only in the
	// completion payload.  Invoked outside the metrics lock.
	onWarning func(ScanWarning) `json:"-"`
}

// ScanProgress is the payload emitted periodically during a scan to
// report live progress to the frontend.
type ScanProgress struct {
	Phase       string `json:"phase"`     // "counting", "scanning", "orphans", "thumbnails"
	Total       int64  `json:"total"`     // total audio files from pre-walk count
	Processed   int64  `json:"processed"` // added + skipped + updated so far
	Added       int64  `json:"added"`
	Skipped     int64  `json:"skipped"`
	Updated     int64  `json:"updated"`
	LibraryID   int64  `json:"libraryId"`   // library being scanned
	LibraryName string `json:"libraryName"` // display name of library being scanned
	QueuedCount int    `json:"queuedCount"` // number of libraries still queued after this one
}

// ScanWarning represents a non-fatal issue encountered during scanning.
type ScanWarning struct {
	FilePath string `json:"filePath"`
	Phase    string `json:"phase"`
	Err      string `json:"err"`
}

// timingBreakdown renders the full per-phase timing profile as plain
// text for the job log.  This replaces the metrics table that used to
// live on the settings page: same numbers, but attached to the scan
// that produced them and copyable from the job's output pane.
func (m *ScanMetrics) timingBreakdown() string {
	var b strings.Builder

	line := func(label string, d time.Duration) {
		b.WriteString(label)
		b.WriteString(": ")
		b.WriteString(d.Round(time.Millisecond).String())
		b.WriteString("\n")
	}

	line("Total", m.Total)

	// Full-rescan-only phases; zero on an incremental scan.
	if m.ClearQueue > 0 || m.ClearDatabase > 0 || m.ClearCoverFiles > 0 {
		line("  Clear queue", m.ClearQueue)
		line("  Clear database", m.ClearDatabase)
		line("  Clear cover files", m.ClearCoverFiles)
	}

	line("  Load existing files", m.LoadExisting)
	line("  Directory walk", m.WalkDuration)
	line("  Metadata extraction (wall clock)", m.ExtractionWallClock)
	line("    Tag extraction (cumulative)", m.TagExtraction)
	line("    Duration extraction (cumulative)", m.DurationExtraction)

	for format, ms := range m.FormatExtraction {
		b.WriteString("      ")
		b.WriteString(format)
		b.WriteString(" (")
		b.WriteString(strconv.FormatInt(m.FormatCount[format], 10))
		b.WriteString(" files): ")
		b.WriteString((time.Duration(ms) * time.Millisecond).String())
		b.WriteString("\n")
	}

	line("  DB writes (wall clock)", m.DBWritesWallClock)
	line("    Batch commits", m.BatchCommits)
	line("    Save cover originals", m.CoverArtSave)
	line("  Thumbnails (wall clock)", m.ThumbnailWallClock)
	line("    Cumulative CPU time", m.ThumbnailGeneration)
	line("      Small", m.ThumbnailSmall)
	line("      Medium", m.ThumbnailMedium)
	line("      Large", m.ThumbnailLarge)
	line("  Orphan cleanup", m.OrphanCleanup)

	return b.String()
}

func newScanMetrics() *ScanMetrics {
	return &ScanMetrics{
		FormatExtraction: make(map[string]int64),
		FormatCount:      make(map[string]int64),
	}
}

// addExtraction records per-file extraction timing from a worker
// goroutine.  It is safe for concurrent use.
func (m *ScanMetrics) addExtraction(
	fileType string,
	tagTime, durationTime time.Duration,
) {
	m.mu.Lock()
	defer m.mu.Unlock()

	total := tagTime + durationTime
	m.FormatExtraction[fileType] += total.Milliseconds()
	m.FormatCount[fileType]++
	m.TagExtraction += tagTime
	m.DurationExtraction += durationTime
}

// addCoverArtSave records the time spent saving an original cover
// art file.  Called from the single-threaded DB writer.
func (m *ScanMetrics) addCoverArtSave(d time.Duration) {
	m.CoverArtSave += d
}

// addWarning records a non-fatal scan issue.  Safe for concurrent use.
func (m *ScanMetrics) addWarning(filePath, phase string, err error) {
	warning := ScanWarning{
		FilePath: filePath,
		Phase:    phase,
		Err:      err.Error(),
	}

	m.mu.Lock()
	m.Warnings = append(m.Warnings, warning)
	notify := m.onWarning
	m.mu.Unlock()

	// Called outside the lock: the job registry takes its own locks and
	// must never be able to deadlock against a scan worker.
	if notify != nil {
		notify(warning)
	}
}

// addThumbnailTier records the time spent generating a single
// thumbnail tier.  Safe for concurrent use from the thumbnail
// worker pool.
func (m *ScanMetrics) addThumbnailTier(
	suffix string,
	d time.Duration,
) {
	m.mu.Lock()
	defer m.mu.Unlock()

	m.ThumbnailGeneration += d

	switch suffix {
	case "_sm":
		m.ThumbnailSmall += d
	case "_md":
		m.ThumbnailMedium += d
	case "_lg":
		m.ThumbnailLarge += d
	}
}
