package updater

import (
	"sort"
	"sync"
	"time"

	"github.com/safing/portbase/utils"
)

// Registry States.
const (
	StateReady       = "ready"       // Default idle state.
	StateChecking    = "checking"    // Downloading indexes.
	StateDownloading = "downloading" // Downloading updates.
	StateFetching    = "fetching"    // Fetching a single file.
)

// RegistryState describes the registry state.
type RegistryState struct {
	sync.Mutex
	reg *ResourceRegistry

	// ID holds the ID of the state the registry is currently in.
	ID string

	// Details holds further information about the current state.
	Details any

	// Updates holds generic information about the current status of pending
	// and recently downloaded updates.
	Updates UpdateState

	// operationLock locks the operation of any state changing operation.
	// This is separate from the registry lock, which locks access to the
	// registry struct.
	operationLock sync.Mutex
}

// StateDownloadingDetails holds details of the downloading state.
type StateDownloadingDetails struct {
	// Resources holds the resource IDs that are being downloaded.
	Resources []string

	// FinishedUpTo holds the index of Resources that is currently being
	// downloaded. Previous resources have finished downloading.
	FinishedUpTo int
}

// UpdateState holds generic information about the current status of pending
// and recently downloaded updates.
type UpdateState struct {
	// LastCheckAt holds the time of the last update check.
	LastCheckAt *time.Time
	// LastCheckError holds the error of the last check.
	LastCheckError error
	// PendingDownload holds the resources that are pending download.
	PendingDownload []string

	// LastDownloadAt holds the time when resources were downloaded the last time.
	LastDownloadAt *time.Time
	// LastDownloadError holds the error of the last download.
	LastDownloadError error
	// LastDownload holds the resources that we downloaded the last time updates
	// were downloaded.
	LastDownload []string

	// LastSuccessAt holds the time of the last successful update (check).
	LastSuccessAt *time.Time
}

// GetState returns the current registry state.
// The returned data must not be modified.
func (reg *ResourceRegistry) GetState() RegistryState {
	reg.state.Lock()
	defer reg.state.Unlock()

	return RegistryState{
		ID:      reg.state.ID,
		Details: reg.state.Details,
		Updates: reg.state.Updates,
	}
}

// StartOperation starts an operation.
func (s *RegistryState) StartOperation(id string) bool {
	defer s.notify()

	s.operationLock.Lock()

	s.Lock()
	defer s.Unlock()

	s.ID = id
	return true
}

// UpdateOperationDetails updates the details of an operation.
// The supplied struct should be a copy and must not be changed after calling
// this function.
func (s *RegistryState) UpdateOperationDetails(details any) {
	defer s.notify()

	s.Lock()
	defer s.Unlock()

	s.Details = details
}

// EndOperation ends an operation.
func (s *RegistryState) EndOperation() {
	defer s.notify()
	defer s.operationLock.Unlock()

	s.Lock()
	defer s.Unlock()

	s.ID = StateReady
	s.Details = nil
}

// ReportUpdateCheck reports an update check to the registry state.
func (s *RegistryState) ReportUpdateCheck(pendingDownload []string, failed error) {
	defer s.notify()

	sort.Strings(pendingDownload)

	s.Lock()
	defer s.Unlock()

	now := time.Now()
	s.Updates.LastCheckAt = &now
	s.Updates.LastCheckError = failed
	s.Updates.PendingDownload = pendingDownload

	if failed == nil {
		s.Updates.LastSuccessAt = &now
	}
}

// ReportDownloads reports downloaded updates to the registry state.
func (s *RegistryState) ReportDownloads(downloaded []string, failed error) {
	defer s.notify()

	sort.Strings(downloaded)

	s.Lock()
	defer s.Unlock()

	now := time.Now()
	s.Updates.LastDownloadAt = &now
	s.Updates.LastDownloadError = failed
	s.Updates.LastDownload = downloaded

	// Remove downloaded resources from the pending list.
	if len(s.Updates.PendingDownload) > 0 {
		newPendingDownload := make([]string, 0, len(s.Updates.PendingDownload))
		for _, pending := range s.Updates.PendingDownload {
			if !utils.StringInSlice(downloaded, pending) {
				newPendingDownload = append(newPendingDownload, pending)
			}
		}
		s.Updates.PendingDownload = newPendingDownload
	}

	if failed == nil {
		s.Updates.LastSuccessAt = &now
	}
}

func (s *RegistryState) notify() {
	switch {
	case s.reg == nil:
		return
	case s.reg.StateNotifyFunc == nil:
		return
	}

	s.reg.StateNotifyFunc(s)
}
