package scanner

import (
	"context"
	"errors"
	"fmt"
	"strconv"
	"sync"
	"time"

	"github.com/navidrome/navidrome/core"
	"github.com/navidrome/navidrome/core/artwork"
	"github.com/navidrome/navidrome/log"
	"github.com/navidrome/navidrome/model"
	"github.com/navidrome/navidrome/server/events"
)

type Scanner interface {
	RescanAll(ctx context.Context, fullRescan bool) error
	Status(mediaFolder string) (*StatusInfo, error)
}

type StatusInfo struct {
	MediaFolder string
	Scanning    bool
	LastScan    time.Time
	Count       uint32
	FolderCount uint32
}

var (
	ErrAlreadyScanning = errors.New("already scanning")
	ErrScanError       = errors.New("scan error")
)

type FolderScanner interface {
	// Scan process finds any changes after `lastModifiedSince` and returns the number of changes found
	Scan(ctx context.Context, lastModifiedSince time.Time, progress chan uint32) (int64, error)
}

var isScanning sync.Mutex

type scanner struct {
	folders     map[string]FolderScanner
	status      map[string]*scanStatus
	lock        *sync.RWMutex
	ds          model.DataStore
	pls         core.Playlists
	broker      events.Broker
	cacheWarmer artwork.CacheWarmer
}

type scanStatus struct {
	active      bool
	fileCount   uint32
	folderCount uint32
	lastUpdate  time.Time
}

func New(ds model.DataStore, playlists core.Playlists, cacheWarmer artwork.CacheWarmer, broker events.Broker) Scanner {
	s := &scanner{
		ds:          ds,
		pls:         playlists,
		broker:      broker,
		folders:     map[string]FolderScanner{},
		status:      map[string]*scanStatus{},
		lock:        &sync.RWMutex{},
		cacheWarmer: cacheWarmer,
	}
	s.loadFolders()
	return s
}

func (s *scanner) rescan(ctx context.Context, mediaFolder string, fullRescan bool) error {
	folderScanner := s.folders[mediaFolder]
	start := time.Now()

	s.setStatusStart(mediaFolder)
	defer s.setStatusEnd(mediaFolder, start)

	lastModifiedSince := time.Time{}
	if !fullRescan {
		lastModifiedSince = s.getLastModifiedSince(ctx, mediaFolder)
		log.Debug("Scanning folder", "folder", mediaFolder, "lastModifiedSince", lastModifiedSince)
	} else {
		log.Debug("Scanning folder (full scan)", "folder", mediaFolder)
	}

	progress, cancel := s.startProgressTracker(mediaFolder)
	defer cancel()

	changeCount, err := folderScanner.Scan(ctx, lastModifiedSince, progress)
	if err != nil {
		log.Error("Error importing MediaFolder", "folder", mediaFolder, err)
	}

	if changeCount > 0 {
		log.Debug(ctx, "Detected changes in the music folder. Sending refresh event",
			"folder", mediaFolder, "changeCount", changeCount)
		// Don't use real context, forcing a refresh in all open windows, including the one that triggered the scan
		s.broker.SendMessage(context.Background(), &events.RefreshResource{})
	}

	s.updateLastModifiedSince(mediaFolder, start)
	return err
}

func (s *scanner) startProgressTracker(mediaFolder string) (chan uint32, context.CancelFunc) {
	ctx, cancel := context.WithCancel(context.Background())
	progress := make(chan uint32, 100)
	go func() {
		s.broker.SendMessage(ctx, &events.ScanStatus{Scanning: true, Count: 0, FolderCount: 0})
		defer func() {
			if status, ok := s.getStatus(mediaFolder); ok {
				s.broker.SendMessage(ctx, &events.ScanStatus{
					Scanning:    false,
					Count:       int64(status.fileCount),
					FolderCount: int64(status.folderCount),
				})
			}
		}()
		for {
			select {
			case <-ctx.Done():
				return
			case count := <-progress:
				if count == 0 {
					continue
				}
				totalFolders, totalFiles := s.incStatusCounter(mediaFolder, count)
				s.broker.SendMessage(ctx, &events.ScanStatus{
					Scanning:    true,
					Count:       int64(totalFiles),
					FolderCount: int64(totalFolders),
				})
			}
		}
	}()
	return progress, cancel
}

func (s *scanner) RescanAll(ctx context.Context, fullRescan bool) error {
	if !isScanning.TryLock() {
		log.Debug("Scanner already running, ignoring request for rescan.")
		return ErrAlreadyScanning
	}
	defer isScanning.Unlock()

	var hasError bool
	for folder := range s.folders {
		err := s.rescan(ctx, folder, fullRescan)
		hasError = hasError || err != nil
	}
	if hasError {
		log.Error("Errors while scanning media. Please check the logs")
		core.WriteAfterScanMetrics(ctx, s.ds, false)
		return ErrScanError
	}
	core.WriteAfterScanMetrics(ctx, s.ds, true)
	return nil
}

func (s *scanner) getStatus(folder string) (scanStatus, bool) {
	s.lock.RLock()
	defer s.lock.RUnlock()
	status, ok := s.status[folder]
	return *status, ok
}

func (s *scanner) incStatusCounter(folder string, numFiles uint32) (totalFolders uint32, totalFiles uint32) {
	s.lock.Lock()
	defer s.lock.Unlock()
	if status, ok := s.status[folder]; ok {
		status.fileCount += numFiles
		status.folderCount++
		totalFolders = status.folderCount
		totalFiles = status.fileCount
	}
	return
}

func (s *scanner) setStatusStart(folder string) {
	s.lock.Lock()
	defer s.lock.Unlock()
	if status, ok := s.status[folder]; ok {
		status.active = true
		status.fileCount = 0
		status.folderCount = 0
	}
}

func (s *scanner) setStatusEnd(folder string, lastUpdate time.Time) {
	s.lock.Lock()
	defer s.lock.Unlock()
	if status, ok := s.status[folder]; ok {
		status.active = false
		status.lastUpdate = lastUpdate
	}
}

func (s *scanner) Status(mediaFolder string) (*StatusInfo, error) {
	status, ok := s.getStatus(mediaFolder)
	if !ok {
		return nil, errors.New("mediaFolder not found")
	}
	return &StatusInfo{
		MediaFolder: mediaFolder,
		Scanning:    status.active,
		LastScan:    status.lastUpdate,
		Count:       status.fileCount,
		FolderCount: status.folderCount,
	}, nil
}

func (s *scanner) getLastModifiedSince(ctx context.Context, folder string) time.Time {
	ms, err := s.ds.Property(ctx).Get(model.PropLastScan + "-" + folder)
	if err != nil {
		return time.Time{}
	}
	if ms == "" {
		return time.Time{}
	}
	i, _ := strconv.ParseInt(ms, 10, 64)
	return time.Unix(0, i*int64(time.Millisecond))
}

func (s *scanner) updateLastModifiedSince(folder string, t time.Time) {
	millis := t.UnixNano() / int64(time.Millisecond)
	if err := s.ds.Property(context.TODO()).Put(model.PropLastScan+"-"+folder, fmt.Sprint(millis)); err != nil {
		log.Error("Error updating DB after scan", err)
	}
}

func (s *scanner) loadFolders() {
	ctx := context.TODO()
	fs, _ := s.ds.MediaFolder(ctx).GetAll()
	for _, f := range fs {
		log.Info("Configuring Media Folder", "name", f.Name, "path", f.Path)
		s.folders[f.Path] = s.newScanner(f)
		s.status[f.Path] = &scanStatus{
			active:      false,
			fileCount:   0,
			folderCount: 0,
			lastUpdate:  s.getLastModifiedSince(ctx, f.Path),
		}
	}
}

func (s *scanner) newScanner(f model.MediaFolder) FolderScanner {
	return NewTagScanner(f.Path, s.ds, s.pls, s.cacheWarmer)
}
