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.
//
// All public methods are safe for concurrent use. A single serializer mutex
// prevents overlapping PrepareRelease→ReleaseSafe→ConfirmRelease sequences.
type StreamReleaser struct {
	mu        sync.Mutex
	tail      *EvidenceTail
	boundary  *CommitBoundary
	canceller AttemptCanceller
}

// 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,
	}, nil
}

// ReleaseEpoch serializes a full release cycle: prepare from tail, release
// through boundary, and confirm sink progress back to tail. It returns the
// number of events confirmed through the entire pipeline.
//
// The serializer guarantees:
//   - PrepareRelease→ReleaseSafe→ConfirmRelease runs atomically under the
//     serializer lock.
//   - Only events the boundary accepted are confirmed to the tail.
func (r *StreamReleaser) ReleaseEpoch(ctx context.Context, attemptID string, epochID uint64) (ReleaseProgress, error) {
	r.mu.Lock()
	defer r.mu.Unlock()

	// Prepare release from tail.
	prepared, err := r.tail.PrepareRelease(epochID)
	if err != nil {
		return ReleaseProgress{}, err
	}

	releaseEvents := prepared.ReleaseEvents()

	// Release through boundary.
	prog, err := r.boundary.ReleaseSafe(ctx, attemptID, releaseEvents)
	if err != nil {
		return prog, err
	}

	// Confirm exactly the number the boundary accepted.
	confirmation := ReleaseConfirmation{ReleasedEvents: prog.ReleasedEvents()}
	if err := r.tail.ConfirmRelease(prepared.Token(), confirmation); err != nil {
		return prog, err
	}

	return prog, nil
}

// ReleaseTerminalEpoch commits a terminal result. For error terminals it
// discards pending, cancels the attempt, and commits the error terminal.
// For success terminals it first releases any remaining epoch, then commits
// the terminal through the boundary.
func (r *StreamReleaser) ReleaseTerminalEpoch(ctx context.Context, attemptID string, epochID uint64, result TerminalResult) (ReleaseProgress, error) {
	r.mu.Lock()
	defer r.mu.Unlock()

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

	// Success terminal: try to release remaining epoch first.
	prepared, err := r.tail.PrepareRelease(epochID)
	if err == nil {
		releaseEvents := prepared.ReleaseEvents()
		if len(releaseEvents) > 0 {
			prog, relErr := r.boundary.ReleaseSafe(ctx, attemptID, releaseEvents)
			if relErr == nil {
				confirmation := ReleaseConfirmation{ReleasedEvents: prog.ReleasedEvents()}
				relErr = r.tail.ConfirmRelease(prepared.Token(), confirmation)
			}
			if relErr == nil {
				return prog, r.boundary.CommitTerminal(ctx, attemptID, result)
			}
			// Release failed; proceed with terminal anyway.
		}
	}

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

// ReplaceUncommittedAttempt coordinates replacing the current attempt. It
// calls boundary.ReplaceUncommittedAttempt and tail.ResetForReplace together
// so that both the boundary state and the tail pending/prepared tokens are
// reset atomically.
func (r *StreamReleaser) ReplaceUncommittedAttempt(ctx context.Context, oldAttemptID, newAttemptID string) error {
	r.mu.Lock()
	defer r.mu.Unlock()

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

	// Reset tail: discards pending entries, prepared tokens, and the token
	// nonce so that a new generation starts cleanly.
	r.tail.ResetForReplace()

	return nil
}

// FailPending discards all pending tail state, calls the canceller, and
// commits an error terminal through the boundary. This is the S04/S19
// recovery path: no partial release, no start release, cancel then terminal.
//
// If cancel fails, the failure cause is appended to the terminal result and
// the single terminal is still committed.
func (r *StreamReleaser) FailPending(ctx context.Context, attemptID string, result TerminalResult) (ReleaseProgress, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	return r.failPendingLocked(ctx, attemptID, result)
}

// failPendingLocked is the shared implementation for FailPending and the
// error path of ReleaseTerminalEpoch. Caller must hold r.mu.
func (r *StreamReleaser) failPendingLocked(ctx context.Context, attemptID string, result TerminalResult) (ReleaseProgress, error) {
	// Discard all pending state.
	r.tail.DiscardPendingForTerminal()

	// Cancel the attempt if a canceller is configured.
	if r.canceller != nil {
		if err := r.canceller.CancelAttempt(ctx, attemptID); err != nil {
			// Append bounded cause to the error terminal.
			if result.Error() {
				causes := result.FailureCauses()
				newCause, cerr := NewFailureCause("releaser", "attempt_cancel_failed", "", "", attemptID)
				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)
				}
			}
			// If we couldn't build the result, commit the original terminal.
			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
// the full release pipeline. This is used in tests to simulate a prepared
// but unconfirmed release before replacement.
func (r *StreamReleaser) PrepareForReplace(ctx context.Context, attemptID string, epochID uint64) (PreparedRelease, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	return r.tail.PrepareRelease(epochID)
}
