package oauthutil

import (
	"sync"
	"sync/atomic"

	"github.com/rclone/rclone/fs"
)

// Renew allows tokens to be renewed on expiry if uploads are in progress.
type Renew struct {
	name     string       // name to use in logs
	ts       *TokenSource // token source that needs renewing
	uploads  atomic.Int32 // number of uploads in progress
	run      func() error // a transaction to run to renew the token on
	done     chan any     // channel to end the go routine
	shutdown sync.Once
}

// NewRenew creates a new Renew struct and starts a background process
// which renews the token whenever it expires.  It uses the run() call
// to run a transaction to do this.
//
// It will only renew the token if the number of uploads > 0
func NewRenew(name string, ts *TokenSource, run func() error) *Renew {
	r := &Renew{
		name: name,
		ts:   ts,
		run:  run,
		done: make(chan any),
	}
	go r.renewOnExpiry()
	return r
}

// renewOnExpiry renews the token whenever it expires.  Useful when there
// are lots of uploads in progress and the token doesn't get renewed.
// Amazon seem to cancel your uploads if you don't renew your token
// for 2hrs.
func (r *Renew) renewOnExpiry() {
	expiry := r.ts.OnExpiry()
	for {
		select {
		case <-expiry:
		case <-r.done:
			return
		}
		uploads := r.uploads.Load()
		if uploads != 0 {
			fs.Debugf(r.name, "Token expired - %d uploads in progress - refreshing", uploads)
			// Do a transaction
			err := r.run()
			if err == nil {
				fs.Debugf(r.name, "Token refresh successful")
			} else {
				fs.Errorf(r.name, "Token refresh failed: %v", err)
			}
		} else {
			fs.Debugf(r.name, "Token expired but no uploads in progress - doing nothing")
		}
	}
}

// Start should be called before starting an upload
func (r *Renew) Start() {
	r.uploads.Add(1)
}

// Stop should be called after finishing an upload
func (r *Renew) Stop() {
	r.uploads.Add(-1)
}

// Invalidate invalidates the token source
func (r *Renew) Invalidate() {
	r.ts.Invalidate()
}

// Expire expires the token source
func (r *Renew) Expire() error {
	return r.ts.Expire()
}

// Shutdown stops the timer and no more renewal will take place.
func (r *Renew) Shutdown() {
	if r == nil {
		return
	}
	// closing a channel can only be done once
	r.shutdown.Do(func() {
		if r.ts != nil {
			r.ts.expiryTimer.Stop()
		}
		close(r.done)
	})
}
