package memorywatcher

import (
	"errors"
	"time"

	"github.com/go-git/go-git/v5/plumbing/transport"
)

// FailureReason classifies bounded synchronization errors without exposing raw
// transport details through runtime status.
func FailureReason(err error, fallback StatusReason) StatusReason {
	if errors.Is(err, transport.ErrAuthenticationRequired) || errors.Is(err, transport.ErrAuthorizationFailed) {
		return StatusReasonAuthFailed
	}
	return fallback
}

// Status returns the current runtime-only synchronization snapshot.
func (w *Watcher) Status() Status {
	if w == nil {
		return Status{State: StatusDegraded, Reason: StatusReasonWatcherFailed}
	}
	w.statusMu.RLock()
	defer w.statusMu.RUnlock()
	return w.status
}

// RetrySync requests immediate recovery through the same single-writer Git
// pipeline used by commits. A failed local commit is attempted again; all
// other states retry the push. Multiple push retries coalesce.
func (w *Watcher) RetrySync() {
	if w == nil {
		return
	}
	status := w.Status()
	if status.State == StatusDegraded && status.Reason == StatusReasonCommitFailed {
		w.requestCommitRetry()
		return
	}
	w.requestPushRetry(0)
}

// RecordFinalPush publishes the result of the dispatcher's teardown push after
// the watcher has stopped. A failure remains pending for the next attach or an
// explicit retry; it does not schedule a goroutine after Close.
func (w *Watcher) RecordFinalPush(err error) {
	if w == nil {
		return
	}
	w.mu.Lock()
	commitFailed := w.commitFailed
	w.mu.Unlock()
	if commitFailed {
		w.publishStatus(Status{State: StatusDegraded, Reason: StatusReasonCommitFailed})
		return
	}
	if err == nil || errors.Is(err, ErrNothingToPush) {
		w.publishStatus(Status{State: StatusReady})
		return
	}
	w.publishStatus(Status{State: StatusPendingSync, Reason: FailureReason(err, StatusReasonPushFailed)})
}

func (w *Watcher) publishStatus(next Status) {
	listener, changed := w.setStatus(next)
	if changed && listener != nil {
		listener(next)
	}
}

func (w *Watcher) setStatus(next Status) (StatusListener, bool) {
	w.statusMu.Lock()
	changed := w.status != next
	w.status = next
	w.statusMu.Unlock()
	return w.opts.Status, changed
}

func (w *Watcher) markPushReady() {
	w.mu.Lock()
	w.retryGen++
	w.retryAttempt = 0
	if w.retryTimer != nil {
		w.retryTimer.Stop()
		w.retryTimer = nil
	}
	commitFailed := w.commitFailed
	w.mu.Unlock()
	if commitFailed {
		w.publishStatus(Status{State: StatusDegraded, Reason: StatusReasonCommitFailed})
		return
	}
	w.publishStatus(Status{State: StatusReady})
}

func (w *Watcher) markPushPending(reason StatusReason) {
	w.mu.Lock()
	if w.stopped {
		w.mu.Unlock()
		return
	}
	delay := w.retryDelayLocked()
	w.retryAttempt++
	retryAt := time.Now().UTC().Add(delay)
	w.retryGen++
	generation := w.retryGen
	if w.retryTimer != nil {
		w.retryTimer.Stop()
	}
	status := Status{State: StatusPendingSync, Reason: reason, RetryAt: retryAt}
	if w.commitFailed {
		status = Status{State: StatusDegraded, Reason: StatusReasonCommitFailed}
	}
	listener, changed := w.setStatus(status)
	w.mu.Unlock()
	if changed && listener != nil {
		listener(status)
	}
	w.mu.Lock()
	if w.stopped || generation != w.retryGen {
		w.mu.Unlock()
		return
	}
	w.retryTimer = time.AfterFunc(delay, func() { w.retryPush(generation) })
	w.mu.Unlock()
}

func (w *Watcher) requestPushRetry(delay time.Duration) {
	if w == nil || w.opts.Push == nil {
		return
	}
	w.mu.Lock()
	if w.stopped {
		w.mu.Unlock()
		return
	}
	if w.retryRunning {
		w.mu.Unlock()
		return
	}
	if w.retryTimer != nil {
		w.retryTimer.Stop()
	}
	w.retryGen++
	generation := w.retryGen
	w.retryTimer = time.AfterFunc(delay, func() { w.retryPush(generation) })
	w.mu.Unlock()
}

func (w *Watcher) requestCommitRetry() {
	w.mu.Lock()
	if w.stopped {
		w.mu.Unlock()
		return
	}
	w.hasPending = true
	if w.debounce != nil {
		w.debounce.Stop()
	}
	w.debounce = time.AfterFunc(0, w.flushCommit)
	w.mu.Unlock()
}

func (w *Watcher) retryPush(generation uint64) {
	w.mu.Lock()
	if w.stopped || generation != w.retryGen {
		w.mu.Unlock()
		return
	}
	w.retryTimer = nil
	if w.retryRunning {
		w.mu.Unlock()
		return
	}
	w.retryRunning = true
	w.mu.Unlock()

	w.commitLock.Lock()
	defer func() {
		w.commitLock.Unlock()
		w.mu.Lock()
		w.retryRunning = false
		w.mu.Unlock()
	}()

	status := w.Status()
	if status.State == StatusPendingSync && !status.RetryAt.IsZero() {
		status.RetryAt = time.Time{}
		w.publishStatus(status)
	}
	w.pushNow("")
}

func (w *Watcher) retryDelayLocked() time.Duration {
	minDelay := w.opts.RetryMin
	if minDelay <= 0 {
		minDelay = defaultRetryMin
	}
	maxDelay := w.opts.RetryMax
	if maxDelay <= 0 {
		maxDelay = defaultRetryMax
	}
	if maxDelay < minDelay {
		maxDelay = minDelay
	}
	delay := minDelay
	for attempt := 0; attempt < w.retryAttempt && delay < maxDelay; attempt++ {
		if delay > maxDelay/2 {
			return maxDelay
		}
		delay *= 2
	}
	if delay > maxDelay {
		return maxDelay
	}
	return delay
}
