package streamgate

import (
	"context"
	"errors"
	"sync"
)

// ErrReleaserSerializerConflict is returned when an operation conflicts
// with the current serializer state (e.g., concurrent epoch calls).
var ErrReleaserSerializerConflict = errors.New("streamgate: releaser serializer conflict")

// ErrReleaserEpochConsumed is returned when an epoch has already been fully
// confirmed.
var ErrReleaserEpochConsumed = errors.New("streamgate: releaser epoch consumed")

// ErrReleaserNoEpoch is returned when the given epoch ID does not exist.
var ErrReleaserNoEpoch = errors.New("streamgate: releaser epoch not found")

// ErrReleaserReplaceNotPermitted is returned when replace is called but the
// boundary or tail state does not permit it.
var ErrReleaserReplaceNotPermitted = errors.New("streamgate: replace not permitted")

// AttemptCanceller is the host seam for cancelling a provider attempt. The
// stream releaser calls this for idle and overflow recovery; the actual
// implementation lives in the transport adapter.
type AttemptCanceller interface {
	CancelAttempt(ctx context.Context, attemptID string) error
}

// StreamReleaser serializes EvidenceTail prepare/confirm with CommitBoundary
// release/terminal operations. It ensures zero/partial/full sink progress is
// confirmed correctly, exactly-once terminal semantics, and coordinated
// replacement.
type StreamReleaser struct {
	mu              sync.Mutex
	busy            bool
	tail            *EvidenceTail
	boundary        *CommitBoundary
	canceller       AttemptCanceller
	completedEpochs map[uint64]bool
}

// NewStreamReleaser creates a new StreamReleaser. Both tail and boundary
// must be non-nil. The canceller may be nil if FailPending is not expected
// to be called.
func NewStreamReleaser(tail *EvidenceTail, boundary *CommitBoundary, canceller AttemptCanceller) (*StreamReleaser, error) {
	if tail == nil {
		return nil, errors.New("streamgate: stream releaser tail is required")
	}
	if boundary == nil {
		return nil, errors.New("streamgate: stream releaser boundary is required")
	}
	return &StreamReleaser{
		tail:            tail,
		boundary:        boundary,
		canceller:       canceller,
		completedEpochs: make(map[uint64]bool),
	}, nil
}

func (r *StreamReleaser) isCompletedEpoch(epochID uint64) bool {
	r.mu.Lock()
	defer r.mu.Unlock()
	return r.completedEpochs != nil && r.completedEpochs[epochID]
}

func (r *StreamReleaser) markCompletedEpoch(epochID uint64) {
	r.mu.Lock()
	defer r.mu.Unlock()
	if r.completedEpochs == nil {
		r.completedEpochs = make(map[uint64]bool)
	}
	r.completedEpochs[epochID] = true
}

func (r *StreamReleaser) clearCompletedEpochs() {
	r.mu.Lock()
	defer r.mu.Unlock()
	r.completedEpochs = make(map[uint64]bool)
}

func (r *StreamReleaser) tryAcquireSerializer() error {
	r.mu.Lock()
	defer r.mu.Unlock()
	if r.busy {
		return ErrReleaserSerializerConflict
	}
	r.busy = true
	return nil
}

func (r *StreamReleaser) releaseSerializer() {
	r.mu.Lock()
	defer r.mu.Unlock()
	r.busy = false
}

func (r *StreamReleaser) releaseAndConfirm(ctx context.Context, attemptID string, epochID uint64) (ReleaseProgress, error) {
	prepared, err := r.tail.PrepareRelease(epochID)
	if err != nil {
		return ReleaseProgress{}, err
	}

	releaseEvents := prepared.ReleaseEvents()
	var (
		prog   ReleaseProgress
		relErr error
	)
	if len(releaseEvents) > 0 {
		prog, relErr = r.boundary.ReleaseSafe(ctx, attemptID, releaseEvents)
	}

	confirmErr := r.tail.ConfirmRelease(
		prepared.Token(),
		ReleaseConfirmation{ReleasedEvents: prog.ReleasedEvents()},
	)

	jointErr := errors.Join(relErr, confirmErr)
	if jointErr != nil {
		return prog, jointErr
	}

	r.markCompletedEpoch(epochID)
	return prog, nil
}

// ReleaseEpoch serializes a full release cycle: prepare from tail, release
// through boundary, and confirm sink progress back to tail.
func (r *StreamReleaser) ReleaseEpoch(ctx context.Context, attemptID string, epochID uint64) (ReleaseProgress, error) {
	if err := r.tryAcquireSerializer(); err != nil {
		return ReleaseProgress{}, err
	}
	defer r.releaseSerializer()

	return r.releaseAndConfirm(ctx, attemptID, epochID)
}

// ReleasePassThroughEvent releases an unbuffered event directly through the boundary under serializer lock.
func (r *StreamReleaser) ReleasePassThroughEvent(ctx context.Context, attemptID string, ev NormalizedEvent) (ReleaseProgress, error) {
	if err := r.tryAcquireSerializer(); err != nil {
		return ReleaseProgress{}, err
	}
	defer r.releaseSerializer()

	relEv, err := normalizedToReleaseEvent(ev)
	if err != nil {
		return ReleaseProgress{}, err
	}

	return r.boundary.ReleaseSafe(ctx, attemptID, []ReleaseEvent{relEv})
}

// releaseReplacementEvents releases an ordered replacement payload in place of
// the pending content captured by the given epoch. The tail validates the
// payload against that epoch before any tail or sink mutation, so an unknown
// epoch, a foreign channel, or a non-releasable kind fails closed with the
// stream untouched. Prepare, the single ordered boundary release, and confirm
// all run inside one serializer reservation: the original payload can never
// reach the sink, and confirm reflects exactly the zero/partial/full progress
// the boundary reported back into the committed look-behind and cursor. Confirm
// runs even when the release failed, so a partially accepted replacement never
// leaves the tail ahead of or behind downstream reality.
func (r *StreamReleaser) releaseReplacementEvents(ctx context.Context, attemptID string, epochID uint64, events []NormalizedEvent) (ReleaseProgress, error) {
	if err := r.tryAcquireSerializer(); err != nil {
		return ReleaseProgress{}, err
	}
	defer r.releaseSerializer()

	prepared, err := r.tail.prepareReplacement(epochID, events)
	if err != nil {
		return ReleaseProgress{}, err
	}

	prog, relErr := r.boundary.ReleaseSafe(ctx, attemptID, prepared.ReleaseEvents())

	confirmErr := r.tail.confirmReplacement(
		prepared.Token(),
		ReleaseConfirmation{ReleasedEvents: prog.ReleasedEvents()},
	)
	if confirmErr == nil {
		// The target epoch is settled: its original content is consumed and can
		// never be released, so a later terminal must not re-release it.
		r.markCompletedEpoch(epochID)
	}

	return prog, errors.Join(relErr, confirmErr)
}

// CommitTerminalWithoutEpoch commits a terminal result without requiring epoch tail prepare/confirm.
func (r *StreamReleaser) CommitTerminalWithoutEpoch(ctx context.Context, attemptID string, result TerminalResult) error {
	if err := r.tryAcquireSerializer(); err != nil {
		return err
	}
	defer r.releaseSerializer()

	if !result.Success() {
		_, err := r.failPendingLocked(ctx, attemptID, result)
		return err
	}
	return r.boundary.CommitTerminal(ctx, attemptID, result)
}

// ReleaseTerminalEpoch commits a terminal result for a buffered epoch.
func (r *StreamReleaser) ReleaseTerminalEpoch(ctx context.Context, attemptID string, epochID uint64, result TerminalResult) (ReleaseProgress, error) {
	if err := r.tryAcquireSerializer(); err != nil {
		return ReleaseProgress{}, err
	}
	defer r.releaseSerializer()

	if !result.Success() {
		return r.failPendingLocked(ctx, attemptID, result)
	}

	if r.isCompletedEpoch(epochID) {
		return ReleaseProgress{}, r.boundary.CommitTerminal(ctx, attemptID, result)
	}

	prog, err := r.releaseAndConfirm(ctx, attemptID, epochID)
	if err != nil {
		return prog, err
	}

	return prog, r.boundary.CommitTerminal(ctx, attemptID, result)
}

// ReplaceUncommittedAttempt coordinates replacing the current attempt.
func (r *StreamReleaser) ReplaceUncommittedAttempt(ctx context.Context, oldAttemptID, newAttemptID string) error {
	if err := r.tryAcquireSerializer(); err != nil {
		return err
	}
	defer r.releaseSerializer()

	if err := r.boundary.ReplaceUncommittedAttempt(oldAttemptID, newAttemptID); err != nil {
		return err
	}

	r.tail.ResetForReplace()
	r.clearCompletedEpochs()
	return nil
}

// ContinueOpenAttempt coordinates transferring attempt identity when continuing an open stream.
func (r *StreamReleaser) ContinueOpenAttempt(ctx context.Context, oldAttemptID, newAttemptID string) error {
	if err := r.tryAcquireSerializer(); err != nil {
		return err
	}
	defer r.releaseSerializer()

	return r.boundary.ContinueOpenAttempt(oldAttemptID, newAttemptID)
}

// FailPending discards all pending tail state, calls the canceller, and
// commits an error terminal through the boundary.
func (r *StreamReleaser) FailPending(ctx context.Context, attemptID string, result TerminalResult) (ReleaseProgress, error) {
	if err := r.tryAcquireSerializer(); err != nil {
		return ReleaseProgress{}, err
	}
	defer r.releaseSerializer()
	return r.failPendingLocked(ctx, attemptID, result)
}

// DiscardPendingAndCommitTerminal discards pending tail state without calling attempt canceller
// (when previous attempt was already aborted/closed) and commits terminal through the boundary.
func (r *StreamReleaser) DiscardPendingAndCommitTerminal(ctx context.Context, attemptID string, result TerminalResult) error {
	if err := r.tryAcquireSerializer(); err != nil {
		return err
	}
	defer r.releaseSerializer()

	r.tail.DiscardPendingForTerminal()
	return r.boundary.CommitTerminal(ctx, attemptID, result)
}

func (r *StreamReleaser) failPendingLocked(ctx context.Context, attemptID string, result TerminalResult) (ReleaseProgress, error) {
	r.tail.DiscardPendingForTerminal()

	if r.canceller != nil {
		if err := r.canceller.CancelAttempt(ctx, attemptID); err != nil {
			if result.Error() {
				causes := result.FailureCauses()
				newCause, cerr := NewFailureCause("releaser", "attempt_cancel_failed", "", "", "")
				if cerr == nil {
					causes, _ = causes.Append(newCause)
				}
				newResult, rerr := NewErrorTerminalResult(
					result.Channel(),
					*result.ExternalDesc(),
					causes,
					result.OccurredAt(),
				)
				if rerr == nil {
					return ReleaseProgress{}, r.boundary.CommitTerminal(ctx, attemptID, newResult)
				}
			}
			return ReleaseProgress{}, r.boundary.CommitTerminal(ctx, attemptID, result)
		}
	}

	return ReleaseProgress{}, r.boundary.CommitTerminal(ctx, attemptID, result)
}

// PrepareForReplace prepares a release for the given epoch without executing.
func (r *StreamReleaser) PrepareForReplace(ctx context.Context, attemptID string, epochID uint64) (PreparedRelease, error) {
	if err := r.tryAcquireSerializer(); err != nil {
		return PreparedRelease{}, err
	}
	defer r.releaseSerializer()
	return r.tail.PrepareRelease(epochID)
}
