// Licensed to Apache Software Foundation (ASF) under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Apache Software Foundation (ASF) licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied.  See the License for the
// specific language governing permissions and limitations
// under the License.

package sidx

import (
	"context"
	"fmt"
	"sync/atomic"

	"github.com/apache/skywalking-banyandb/pkg/logger"
	"github.com/apache/skywalking-banyandb/pkg/run"
)

// partWrapper provides thread-safe reference counting for parts.
// It enables safe concurrent access to parts while managing their lifecycle.
// When the reference count reaches zero, the underlying part is cleaned up.
type partWrapper struct {
	p         *part
	mp        *memPart
	ref       int32
	removable atomic.Bool
}

// newPartWrapper creates a new partWrapper with an initial reference count of 1.
// The part starts in the active state.
func newPartWrapper(mp *memPart, p *part) *partWrapper {
	pw := &partWrapper{
		mp:  mp,
		p:   p,
		ref: 1,
	}

	return pw
}

// acquire increments the reference count atomically.
// Returns true if the reference was successfully acquired (part is still active),
// false if the part is being removed or has been removed.
func (pw *partWrapper) acquire() bool {
	// Try to increment reference count
	for {
		oldRef := atomic.LoadInt32(&pw.ref)
		if oldRef <= 0 {
			// Reference count is already zero or negative, cannot acquire
			return false
		}

		// Try to atomically increment
		if atomic.CompareAndSwapInt32(&pw.ref, oldRef, oldRef+1) {
			return true
		}
		// Retry if CAS failed
	}
}

// release decrements the reference count atomically.
// When the reference count reaches zero, the part is cleaned up.
// This method is safe to call multiple times.
func (pw *partWrapper) release() {
	newRef := atomic.AddInt32(&pw.ref, -1)
	if newRef > 0 {
		return
	}

	if newRef < 0 {
		// This shouldn't happen in correct usage, but log it for debugging
		logger.GetLogger().Warn().
			Int32("ref", newRef).
			Str("part", pw.String()).
			Msg("partWrapper reference count went negative")
		return
	}

	// Reference count reached zero, perform cleanup
	pw.cleanup()
}

// cleanup performs the actual cleanup when reference count reaches zero.
// This includes closing the part and potentially removing files from disk.
func (pw *partWrapper) cleanup() {
	// Release memory part if it exists
	if pw.mp != nil {
		ReleaseMemPart(pw.mp)
		pw.mp = nil
	}

	if pw.p == nil {
		return
	}

	// Close the part to release file handles
	pw.p.close()

	// Remove from disk if marked as removable
	if pw.removable.Load() && pw.p.fileSystem != nil {
		partPath := pw.p.path
		fileSystem := pw.p.fileSystem
		l := logger.GetLogger("storage-part-removal")
		run.GoOrDie(context.Background(), "storage-part-removal", l, func(_ context.Context) {
			// Use a goroutine for potentially slow disk operations
			// to avoid blocking the caller
			if rmFS, ok := fileSystem.(interface{ MustRMAll(string) }); ok {
				rmFS.MustRMAll(partPath)
			}
		})
	}
}

// markForRemoval marks the part as removable and transitions it to the removing state.
// Once marked for removal, no new references can be acquired.
// The part will be physically removed when the reference count reaches zero.
func (pw *partWrapper) markForRemoval() {
	// Mark as removable for cleanup
	pw.removable.Store(true)
}

// ID returns the unique identifier of the part.
func (pw *partWrapper) ID() uint64 {
	return pw.p.partMetadata.ID
}

// refCount returns the current reference count.
// This is primarily for testing and debugging.
func (pw *partWrapper) refCount() int32 {
	return atomic.LoadInt32(&pw.ref)
}

// isMemPart returns true if this wrapper contains a memory part.
func (pw *partWrapper) isMemPart() bool {
	// A memory part typically has no file system path or is stored in memory
	return pw.mp != nil
}

// String returns a string representation of the partWrapper.
func (pw *partWrapper) String() string {
	refCount := pw.refCount()

	if pw.p == nil && pw.mp == nil {
		return fmt.Sprintf("partWrapper{id=nil,  ref=%d}", refCount)
	}

	if pw.mp != nil {
		var id uint64
		if pw.mp.partMetadata != nil {
			id = pw.mp.partMetadata.ID
		}
		return fmt.Sprintf("partWrapper{id=%d, ref=%d, memPart=true}",
			id, refCount)
	}

	if pw.p == nil {
		return fmt.Sprintf("partWrapper{id=nil, ref=%d, part=nil}", refCount)
	}

	// Handle case where p.partMetadata might be nil after cleanup
	var id uint64
	var path string
	if pw.p.partMetadata != nil {
		id = pw.p.partMetadata.ID
	}
	if pw.p.path != "" {
		path = pw.p.path
	} else {
		path = "unknown"
	}

	return fmt.Sprintf("partWrapper{id=%d, ref=%d, path=%s}",
		id, refCount, path)
}

// overlapsKeyRange checks if the part overlaps with the given key range.
// Returns true if there is any overlap between the part's key range and the query range.
// Uses part metadata to perform efficient range filtering without I/O.
func (pw *partWrapper) overlapsKeyRange(minKey, maxKey int64) bool {
	if pw.p == nil {
		return false
	}
	if minKey > maxKey {
		return false
	}
	if pw.p.partMetadata == nil {
		return true
	}
	pm := pw.p.partMetadata
	if pm.MaxKey < minKey || pm.MinKey > maxKey {
		return false
	}
	return true
}

// overlapsTimestampRange checks if the part overlaps with the given timestamp range.
// Returns true when part has no timestamp metadata (fallback) or when ranges overlap.
func (pw *partWrapper) overlapsTimestampRange(minTS, maxTS int64) bool {
	if pw.p == nil {
		return false
	}
	if pw.p.partMetadata == nil {
		return true
	}
	pm := pw.p.partMetadata
	if pm.MinTimestamp == nil || pm.MaxTimestamp == nil {
		return true
	}
	if *pm.MaxTimestamp < minTS || *pm.MinTimestamp > maxTS {
		return false
	}
	return true
}
