// 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 (
	"bytes"
	"fmt"

	"github.com/blugelabs/bluge/numeric"

	"github.com/apache/skywalking-banyandb/pkg/convert"
	"github.com/apache/skywalking-banyandb/pkg/encoding"
	"github.com/apache/skywalking-banyandb/pkg/filter"
	"github.com/apache/skywalking-banyandb/pkg/fs"
	"github.com/apache/skywalking-banyandb/pkg/index"
	"github.com/apache/skywalking-banyandb/pkg/logger"
	pbv1 "github.com/apache/skywalking-banyandb/pkg/pb/v1"
	"github.com/apache/skywalking-banyandb/pkg/pool"
)

// tagFilterOp provides a FilterOp implementation for tag filtering in sidx.
type tagFilterOp struct {
	blockMetadata *blockMetadata
	part          *part
	tagCache      map[string]*tagFilterCache
}

// Filter is an interface for tag filtering.
type Filter interface {
	MightContain(item []byte) bool
	// ContainsAll checks whether all elements in the items slice are present (AND semantics).
	// For array types (StrArr/Int64Arr), it checks if items is a subset of any stored array.
	// For non-array types, it checks whether each item exists as a dictionary value (or might exist, depending on implementation).
	ContainsAll(items [][]byte) bool
}

// tagFilterCache caches tag filter data for a specific tag.
type tagFilterCache struct {
	filter    Filter
	min       []byte
	max       []byte
	valueType pbv1.ValueType
}

// Eq checks if a tag equals a specific value by reading tag data and checking bloom filter.
func (tfo *tagFilterOp) Eq(tagName string, tagValue string) bool {
	if tfo.blockMetadata == nil || tfo.part == nil {
		return false
	}

	// Check if the tag exists in the block
	tagBlock, exists := tfo.blockMetadata.tagsBlocks[tagName]
	if !exists {
		return false
	}

	// Get or create cached tag filter data
	cache, err := tfo.getTagFilterCache(tagName, tagBlock)
	if err != nil {
		logger.Errorf("failed to get tag filter cache for %s: %v", tagName, err)
		return true // Conservative approach - don't filter out
	}

	// Use filter to check if the value might exist
	if cache.filter != nil {
		return cache.filter.MightContain([]byte(tagValue))
	}

	// If no filter, conservatively return true
	return true
}

// Having checks if any of the provided tag values might exist in the filter.
// For non-array types: returns true if ANY value might be contained (OR semantics).
// For array types: returns true if ALL values are a subset of any stored array (AND semantics).
func (tfo *tagFilterOp) Having(tagName string, tagValues []string) bool {
	if tfo.blockMetadata == nil || tfo.part == nil {
		return false
	}

	// Check if the tag exists in the block
	tagBlock, exists := tfo.blockMetadata.tagsBlocks[tagName]
	if !exists {
		return false
	}

	// Get or create cached tag filter data
	cache, err := tfo.getTagFilterCache(tagName, tagBlock)
	if err != nil {
		logger.Errorf("failed to get tag filter cache for %s: %v", tagName, err)
		return true // Conservative approach - don't filter out
	}

	// Use filter to check values
	if cache.filter != nil {
		// For array types (StrArr/Int64Arr), use ContainsAll for optimized subset checking
		if cache.valueType == pbv1.ValueTypeStrArr || cache.valueType == pbv1.ValueTypeInt64Arr {
			items := make([][]byte, len(tagValues))
			for i, v := range tagValues {
				items[i] = []byte(v)
			}
			return cache.filter.ContainsAll(items)
		}

		// For non-array types, check if any value might exist (OR semantics)
		for _, tagValue := range tagValues {
			if cache.filter.MightContain([]byte(tagValue)) {
				return true // Return true as soon as we find a potential match
			}
		}
		// None of the values might exist in the filter
		return false
	}

	// If no filter, conservatively return true
	return true
}

// Range checks if a tag is within a specific range using min/max metadata.
func (tfo *tagFilterOp) Range(tagName string, rangeOpts index.RangeOpts) (bool, error) {
	if tfo.blockMetadata == nil || tfo.part == nil {
		return false, nil
	}

	// Check if the tag exists in the block
	tagBlock, exists := tfo.blockMetadata.tagsBlocks[tagName]
	if !exists {
		return false, nil
	}

	// Get or create cached tag filter data
	cache, err := tfo.getTagFilterCache(tagName, tagBlock)
	if err != nil {
		return false, fmt.Errorf("failed to get tag filter cache for %s: %w", tagName, err)
	}

	// Only perform range check for numeric types with min/max values
	if cache.valueType != pbv1.ValueTypeInt64 || len(cache.min) == 0 || len(cache.max) == 0 {
		return false, nil // Conservative approach for non-numeric or missing min/max - don't skip
	}

	// Check lower bound
	if rangeOpts.Lower != nil {
		lower, ok := rangeOpts.Lower.(*index.FloatTermValue)
		if !ok {
			return false, fmt.Errorf("lower bound is not a float value: %v", rangeOpts.Lower)
		}
		value := convert.Int64ToBytes(numeric.Float64ToInt64(lower.Value))
		if bytes.Compare(cache.max, value) == -1 || (!rangeOpts.IncludesLower && bytes.Equal(cache.max, value)) {
			return true, nil
		}
	}

	// Check upper bound
	if rangeOpts.Upper != nil {
		upper, ok := rangeOpts.Upper.(*index.FloatTermValue)
		if !ok {
			return false, fmt.Errorf("upper bound is not a float value: %v", rangeOpts.Upper)
		}
		value := convert.Int64ToBytes(numeric.Float64ToInt64(upper.Value))
		if bytes.Compare(cache.min, value) == 1 || (!rangeOpts.IncludesUpper && bytes.Equal(cache.min, value)) {
			return true, nil
		}
	}

	return false, nil
}

// getTagFilterCache retrieves or creates cached tag filter data.
func (tfo *tagFilterOp) getTagFilterCache(tagName string, tagBlock dataBlock) (*tagFilterCache, error) {
	// Check cache first
	if cache, exists := tfo.tagCache[tagName]; exists {
		return cache, nil
	}

	// Read tag metadata to get filter information
	tagMetadata, err := tfo.readTagMetadata(tagName, tagBlock)
	if err != nil {
		return nil, fmt.Errorf("failed to read tag metadata: %w", err)
	}
	defer releaseTagMetadata(tagMetadata)

	// Create cache entry
	cache := &tagFilterCache{
		valueType: tagMetadata.valueType,
		min:       make([]byte, len(tagMetadata.min)),
		max:       make([]byte, len(tagMetadata.max)),
	}
	copy(cache.min, tagMetadata.min)
	copy(cache.max, tagMetadata.max)

	// Read filter data
	if tagMetadata.filterBlock.size > 0 {
		bf, err := tfo.readBloomFilter(tagName, tagMetadata.filterBlock)
		if err != nil {
			return nil, fmt.Errorf("failed to read bloom filter: %w", err)
		}
		cache.filter = bf
	} else {
		tagDataReader, _ := tfo.part.getTagDataReader(tagName)
		encodeTypeBuf := make([]byte, 1)
		fs.MustReadData(tagDataReader, int64(tagMetadata.dataBlock.offset), encodeTypeBuf)
		encodeType := encoding.EncodeType(encodeTypeBuf[0])
		if encodeType == encoding.EncodeTypeDictionary {
			tagData := make([]byte, tagMetadata.dataBlock.size)
			fs.MustReadData(tagDataReader, int64(tagMetadata.dataBlock.offset), tagData)
			dictValues, err := encoding.DecodeDictionaryValues(tagData[1:])
			if err != nil {
				logger.Panicf("failed to extract dictionary values for tag %s: %v", tagName, err)
			}
			df := generateDictionaryFilter()
			df.Set(dictValues, tagMetadata.valueType)
			cache.filter = df
		}
	}

	// Cache the result
	tfo.tagCache[tagName] = cache

	return cache, nil
}

// readTagMetadata reads tag metadata from the tag metadata files.
func (tfo *tagFilterOp) readTagMetadata(tagName string, tagBlock dataBlock) (*tagMetadata, error) {
	if tagBlock.size == 0 {
		return nil, fmt.Errorf("empty tag block for %s", tagName)
	}

	// Get tag metadata reader
	metaReader, exists := tfo.part.getTagMetadataReader(tagName)
	if !exists {
		return nil, fmt.Errorf("no metadata reader for tag %s", tagName)
	}

	data := make([]byte, tagBlock.size)
	fs.MustReadData(metaReader, int64(tagBlock.offset), data)

	// Unmarshal tag metadata
	tm, err := unmarshalTagMetadata(data)
	if err != nil {
		return nil, fmt.Errorf("failed to unmarshal tag metadata: %w", err)
	}

	return tm, nil
}

// readBloomFilter reads and decodes a bloom filter from filter data.
func (tfo *tagFilterOp) readBloomFilter(tagName string, filterBlock dataBlock) (*filter.BloomFilter, error) {
	if filterBlock.size == 0 {
		return nil, fmt.Errorf("empty filter block")
	}

	// Get filter reader
	filterReader, exists := tfo.part.getTagFilterReader(tagName)
	if !exists {
		return nil, fmt.Errorf("no filter reader for tag %s", tagName)
	}

	// Read filter data
	filterData := make([]byte, filterBlock.size)
	fs.MustReadData(filterReader, int64(filterBlock.offset), filterData)

	return decodeBloomFilter(filterData)
}

// reset resets the tagFilterOp for reuse.
func (tfo *tagFilterOp) reset() {
	tfo.blockMetadata = nil
	tfo.part = nil
	for key, cache := range tfo.tagCache {
		if cache.filter != nil {
			switch f := cache.filter.(type) {
			case *filter.BloomFilter:
				releaseBloomFilter(f)
			case *filter.DictionaryFilter:
				releaseDictionaryFilter(f)
			}
		}
		delete(tfo.tagCache, key)
	}
}

// generateTagFilterOp gets a tagFilterOp from pool or creates new.
func generateTagFilterOp(bm *blockMetadata, p *part) *tagFilterOp {
	v := tagFilterOpPool.Get()
	if v == nil {
		return &tagFilterOp{
			blockMetadata: bm,
			part:          p,
			tagCache:      make(map[string]*tagFilterCache),
		}
	}
	tfo := v
	tfo.blockMetadata = bm
	tfo.part = p
	return tfo
}

// releaseTagFilterOp returns tagFilterOp to pool after reset.
func releaseTagFilterOp(tfo *tagFilterOp) {
	tfo.reset()
	tagFilterOpPool.Put(tfo)
}

var tagFilterOpPool = pool.Register[*tagFilterOp]("sidx-tagFilterOp")
