/*
 * # Licensed to the LF AI & Data foundation under one
 * # or more contributor license agreements. See the NOTICE file
 * # distributed with this work for additional information
 * # regarding copyright ownership. The 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 chain

import (
	"github.com/apache/arrow/go/v17/arrow"

	"github.com/milvus-io/milvus-proto/go-api/v3/schemapb"
	"github.com/milvus-io/milvus/internal/util/function/chain/types"
	"github.com/milvus-io/milvus/pkg/v3/util/merr"
)

// =============================================================================
// DataFrame - Immutable Data Container
// =============================================================================

// DataFrame is an immutable data container that stores Milvus data using Arrow Chunked Arrays.
// Each chunk corresponds to a query result (NQ), enabling per-query access.
//
// DataFrame is similar to Arrow Table - it is read-only after creation.
// To create or modify a DataFrame, use DataFrameBuilder.
//
// Memory management: Call Release() exactly once when done with the DataFrame.
//
// Structure:
//
//	DataFrame
//	├── schema: Arrow schema with field metadata
//	├── columns: []arrow.Chunked (one per column, each has NQ chunks)
//	├── chunkSizes: []int64 (row count per chunk, corresponds to Topks)
//	├── fieldTypes: Milvus DataType per column (for export back to protobuf)
//	├── fieldIDs: field ID per column (for export back to protobuf)
//	└── metadata: key-value pairs (e.g., metric_type)
//
// Data layout example (2 columns, 3 chunks/NQ):
//
//	           chunk0(nq0)  chunk1(nq1)  chunk2(nq2)
//	$id        [1,2,3]      [4,5]        [6]
//	$score     [0.9,0.8,0.7] [0.6,0.5]   [0.4]
//	chunkSizes: [3, 2, 1]
type DataFrame struct {
	schema         *arrow.Schema                // Arrow schema with field metadata
	columns        []*arrow.Chunked             // Chunked arrays, one per column
	chunkSizes     []int64                      // Row count per chunk (corresponds to Topks)
	nameIndex      map[string]int               // Column name to index mapping
	fieldTypes     map[string]schemapb.DataType // Preserve Milvus type info for export
	fieldIDs       map[string]int64             // Field IDs for export
	fieldNullables map[string]bool              // Field nullable info for schema creation
	metadata       map[string]string            // Arbitrary key-value metadata (e.g., metric_type)
}

// =============================================================================
// Metadata Methods (Read-only)
// =============================================================================

// NumRows returns the total number of rows across all chunks.
func (df *DataFrame) NumRows() int64 {
	var total int64
	for _, size := range df.chunkSizes {
		total += size
	}
	return total
}

// NumChunks returns the number of chunks (NQ for search results).
func (df *DataFrame) NumChunks() int {
	return len(df.chunkSizes)
}

// NumColumns returns the number of columns.
func (df *DataFrame) NumColumns() int {
	return len(df.columns)
}

// ChunkSizes returns the row count per chunk (same as Topks for search results).
func (df *DataFrame) ChunkSizes() []int64 {
	result := make([]int64, len(df.chunkSizes))
	copy(result, df.chunkSizes)
	return result
}

// Schema returns the Arrow schema.
func (df *DataFrame) Schema() *arrow.Schema {
	return df.schema
}

// =============================================================================
// Column Access Methods (Read-only)
// =============================================================================

// Column returns a column by name.
// Returns nil if the column does not exist.
func (df *DataFrame) Column(name string) *arrow.Chunked {
	idx, exists := df.nameIndex[name]
	if !exists {
		return nil
	}
	return df.columns[idx]
}

// ColumnNames returns all column names in schema order.
func (df *DataFrame) ColumnNames() []string {
	if df.schema == nil {
		return nil
	}
	fields := df.schema.Fields()
	names := make([]string, len(fields))
	for i, f := range fields {
		names[i] = f.Name
	}
	return names
}

// HasColumn checks if a column exists.
func (df *DataFrame) HasColumn(name string) bool {
	_, exists := df.nameIndex[name]
	return exists
}

// =============================================================================
// Field Metadata Methods (Read-only)
// =============================================================================

// FieldType returns the Milvus DataType for a column.
func (df *DataFrame) FieldType(name string) (schemapb.DataType, bool) {
	dt, exists := df.fieldTypes[name]
	return dt, exists
}

// FieldID returns the field ID for a column.
func (df *DataFrame) FieldID(name string) (int64, bool) {
	id, exists := df.fieldIDs[name]
	return id, exists
}

// Metadata returns a metadata value by key.
func (df *DataFrame) Metadata(key string) (string, bool) {
	val, ok := df.metadata[key]
	return val, ok
}

// MetricType returns the metric type from DataFrame metadata.
func (df *DataFrame) MetricType() (string, bool) {
	return df.Metadata(types.MetadataKeyMetricType)
}

// =============================================================================
// Lifecycle Methods
// =============================================================================

// Release releases all Arrow resources held by the DataFrame.
// Call exactly once when done. After Release(), the DataFrame should not be used.
func (df *DataFrame) Release() {
	for _, col := range df.columns {
		if col != nil {
			col.Release()
		}
	}
	df.columns = nil
	df.schema = nil
}

// =============================================================================
// DataFrameBuilder
// =============================================================================

// DataFrameBuilder helps build a DataFrame with proper resource cleanup.
// Use defer builder.Release() right after creation, then call Build() to get the result.
// Build() transfers ownership, making Release() a no-op.
//
// Typical usage pattern:
//
//	builder := NewDataFrameBuilder()
//	defer builder.Release()          // safety net: releases resources if Build() is never called
//	builder.SetChunkSizes(sizes)
//	builder.AddColumnFromChunks("col", chunks)
//	return builder.Build(), nil      // transfers ownership, Release() becomes no-op
//
// Key methods:
//   - SetChunkSizes: set chunk sizes (required)
//   - AddColumnFromChunks: add a column from Arrow Array slices (takes ownership of chunks)
//   - AddColumnFrom: copy a column from another DataFrame (retains and copies metadata)
//   - AddColumns: batch add multiple columns (all-or-nothing with rollback on error)
//   - CopyFieldMetadata: copy field type/ID/nullable from source DataFrame
//   - Build: construct the DataFrame and invalidate the builder
type DataFrameBuilder struct {
	result *DataFrame
	fields []arrow.Field // accumulated fields, schema created in Build()
}

// NewDataFrameBuilder creates a new empty DataFrameBuilder.
func NewDataFrameBuilder() *DataFrameBuilder {
	return &DataFrameBuilder{
		result: &DataFrame{
			columns:        make([]*arrow.Chunked, 0),
			chunkSizes:     make([]int64, 0),
			nameIndex:      make(map[string]int),
			fieldTypes:     make(map[string]schemapb.DataType),
			fieldIDs:       make(map[string]int64),
			fieldNullables: make(map[string]bool),
			metadata:       make(map[string]string),
		},
	}
}

// SetChunkSizes sets the chunk sizes on the result DataFrame.
func (b *DataFrameBuilder) SetChunkSizes(sizes []int64) *DataFrameBuilder {
	if b.result == nil {
		return b
	}
	b.result.chunkSizes = make([]int64, len(sizes))
	copy(b.result.chunkSizes, sizes)
	return b
}

// SetFieldType sets the Milvus data type for a column.
func (b *DataFrameBuilder) SetFieldType(name string, dataType schemapb.DataType) *DataFrameBuilder {
	if b.result == nil {
		return b
	}
	b.result.fieldTypes[name] = dataType
	return b
}

// SetFieldID sets the field ID for a column.
func (b *DataFrameBuilder) SetFieldID(name string, fieldID int64) *DataFrameBuilder {
	if b.result == nil {
		return b
	}
	b.result.fieldIDs[name] = fieldID
	return b
}

// SetFieldNullable sets whether a column is nullable.
func (b *DataFrameBuilder) SetFieldNullable(name string, nullable bool) *DataFrameBuilder {
	if b.result == nil {
		return b
	}
	b.result.fieldNullables[name] = nullable
	return b
}

// SetMetadata sets a metadata key-value pair.
func (b *DataFrameBuilder) SetMetadata(key, value string) *DataFrameBuilder {
	if b.result == nil {
		return b
	}
	b.result.metadata[key] = value
	return b
}

// SetMetricType sets the metric type metadata on the builder.
func (b *DataFrameBuilder) SetMetricType(metricType string) *DataFrameBuilder {
	return b.SetMetadata(types.MetadataKeyMetricType, metricType)
}

// addColumn adds a chunked column to the DataFrame, taking ownership.
// On error, the column is released.
func (b *DataFrameBuilder) addColumn(name string, col *arrow.Chunked) error {
	if b.result == nil {
		if col != nil {
			col.Release()
		}
		return merr.WrapErrServiceInternal("builder already built")
	}

	if _, exists := b.result.nameIndex[name]; exists {
		if col != nil {
			col.Release()
		}
		return merr.WrapErrServiceInternalMsg("column %s already exists", name)
	}

	if col == nil {
		return merr.WrapErrServiceInternalMsg("column %s is nil", name)
	}

	b.addColumnUnchecked(name, col)
	return nil
}

// addColumnUnchecked adds a column without validation (internal use).
func (b *DataFrameBuilder) addColumnUnchecked(name string, col *arrow.Chunked) {
	// Accumulate field for deferred schema creation in Build()
	b.fields = append(b.fields, arrow.Field{Name: name, Type: col.DataType(), Nullable: true})

	// Add column
	b.result.columns = append(b.result.columns, col)
	b.result.nameIndex[name] = len(b.result.columns) - 1
}

// AddColumns adds multiple columns at once, taking ownership of all.
// Either all columns are added successfully, or none are added and all are released.
// This is the preferred method when adding function outputs to avoid partial failures.
func (b *DataFrameBuilder) AddColumns(names []string, cols []*arrow.Chunked) error {
	// Helper to release all columns
	releaseAll := func() {
		for _, c := range cols {
			if c != nil {
				c.Release()
			}
		}
	}

	if b.result == nil {
		releaseAll()
		return merr.WrapErrServiceInternal("builder already built")
	}

	if len(names) != len(cols) {
		releaseAll()
		return merr.WrapErrServiceInternalMsg("names count (%d) != cols count (%d)", len(names), len(cols))
	}

	// Validate all before adding any
	seen := make(map[string]bool, len(names))
	for i, name := range names {
		if _, exists := b.result.nameIndex[name]; exists {
			releaseAll()
			return merr.WrapErrServiceInternalMsg("column %s already exists", name)
		}
		if seen[name] {
			releaseAll()
			return merr.WrapErrServiceInternalMsg("duplicate column name %s in batch", name)
		}
		seen[name] = true
		if cols[i] == nil {
			releaseAll()
			return merr.WrapErrServiceInternalMsg("column %s is nil", name)
		}
	}

	// All validation passed, add all columns
	for i, name := range names {
		b.addColumnUnchecked(name, cols[i])
	}
	return nil
}

// AddColumnFrom copies a column from source DataFrame, including metadata.
// This is a convenience method that combines Retain + addColumn + CopyFieldMetadata.
func (b *DataFrameBuilder) AddColumnFrom(source *DataFrame, colName string) error {
	if b.result == nil {
		return merr.WrapErrServiceInternal("builder already built")
	}

	col := source.Column(colName)
	if col == nil {
		return merr.WrapErrServiceInternalMsg("column %s not found in source", colName)
	}

	col.Retain()
	if err := b.addColumn(colName, col); err != nil {
		return err
	}

	b.CopyFieldMetadata(source, colName)
	return nil
}

// AddColumnFromChunks creates a chunked column from arrays and adds it.
// Takes ownership of chunks - they are released after creating the chunked array.
func (b *DataFrameBuilder) AddColumnFromChunks(name string, chunks []arrow.Array) error {
	if b.result == nil {
		for _, chunk := range chunks {
			if chunk != nil {
				chunk.Release()
			}
		}
		return merr.WrapErrServiceInternal("builder already built")
	}

	if len(chunks) == 0 {
		return nil
	}

	arrowType := chunks[0].DataType()
	chunked := arrow.NewChunked(arrowType, chunks)

	// Release individual arrays after creating chunked
	for _, chunk := range chunks {
		chunk.Release()
	}

	// Infer Milvus type if not set
	if _, exists := b.result.fieldTypes[name]; !exists {
		if milvusType, err := ToMilvusType(arrowType); err == nil {
			b.result.fieldTypes[name] = milvusType
		}
	}

	return b.addColumn(name, chunked)
}

// CopyFieldMetadata copies field type, ID, and nullable from source DataFrame.
func (b *DataFrameBuilder) CopyFieldMetadata(source *DataFrame, colName string) *DataFrameBuilder {
	if b.result == nil {
		return b
	}
	if ft, ok := source.FieldType(colName); ok {
		b.result.fieldTypes[colName] = ft
	}
	if fid, ok := source.FieldID(colName); ok {
		b.result.fieldIDs[colName] = fid
	}
	if nullable, ok := source.fieldNullables[colName]; ok {
		b.result.fieldNullables[colName] = nullable
	}
	return b
}

// CopyAllMetadata copies all metadata entries from source DataFrame.
func (b *DataFrameBuilder) CopyAllMetadata(source *DataFrame) *DataFrameBuilder {
	if b.result == nil || source == nil {
		return b
	}
	for k, v := range source.metadata {
		b.result.metadata[k] = v
	}
	return b
}

// Build returns the constructed DataFrame and invalidates the builder.
// After Build(), Release() becomes a no-op.
func (b *DataFrameBuilder) Build() *DataFrame {
	// Create schema from accumulated fields with correct nullable settings
	if len(b.fields) > 0 {
		finalFields := make([]arrow.Field, len(b.fields))
		for i, f := range b.fields {
			// Look up nullable setting, default to false (Milvus default)
			finalFields[i] = arrow.Field{
				Name:     f.Name,
				Type:     f.Type,
				Nullable: b.result.fieldNullables[f.Name],
			}
		}
		b.result.schema = arrow.NewSchema(finalFields, nil)
	}

	result := b.result
	b.result = nil
	b.fields = nil
	return result
}

// Release releases all resources held by the builder.
// Safe to call multiple times. After Build(), this is a no-op.
func (b *DataFrameBuilder) Release() {
	if b.result != nil {
		// Directly release columns without going through refCount
		// since the DataFrame hasn't been officially "built" yet
		for _, col := range b.result.columns {
			if col != nil {
				col.Release()
			}
		}
		b.result.columns = nil
		b.result.schema = nil
		b.result = nil
	}
	b.fields = nil
}

// =============================================================================
// ChunkCollector
// =============================================================================

// ChunkCollector is a temporary storage with ownership tracking for Arrow arrays
// produced during per-chunk transformations. It solves the problem of safely managing
// N columns × M chunks of intermediate Arrow arrays, ensuring proper cleanup on error.
//
// Workflow:
//
//  1. Create:  collector := NewChunkCollector(colNames, numChunks)
//     defer collector.Release()
//  2. Fill:    collector.Set(colName, chunkIdx, transformedArray)
//  3. Consume: chunks := collector.Consume(colName)  // ownership transfers to caller
//     builder.AddColumnFromChunks(colName, chunks)
//  4. Cleanup: collector.Release()  // releases only non-consumed arrays
//
// On error before all columns are consumed, Release() frees unconsumed arrays
// while consumed arrays are managed by their new owner (typically DataFrameBuilder).
//
// Used by operators that transform data per-chunk: filter, sort, select, limit,
// group_by, merge.
type ChunkCollector struct {
	chunks   map[string][]arrow.Array
	consumed map[string]bool
}

// NewChunkCollector creates a new ChunkCollector.
func NewChunkCollector(colNames []string, numChunks int) *ChunkCollector {
	cc := &ChunkCollector{
		chunks:   make(map[string][]arrow.Array),
		consumed: make(map[string]bool),
	}
	for _, name := range colNames {
		cc.chunks[name] = make([]arrow.Array, numChunks)
	}
	return cc
}

// Set sets the chunk at the given index for a column.
func (cc *ChunkCollector) Set(colName string, chunkIdx int, chunk arrow.Array) {
	cc.chunks[colName][chunkIdx] = chunk
}

// Consume returns the chunks for a column and marks it as consumed.
// Consumed columns will not be released by Release().
// The caller takes ownership of the returned chunks.
func (cc *ChunkCollector) Consume(colName string) []arrow.Array {
	cc.consumed[colName] = true
	return cc.chunks[colName]
}

// Release releases all non-consumed chunks.
// Safe to call multiple times. Consumed columns are not affected.
func (cc *ChunkCollector) Release() {
	for colName, chunks := range cc.chunks {
		if cc.consumed[colName] {
			continue
		}
		for _, chunk := range chunks {
			if chunk != nil {
				chunk.Release()
			}
		}
	}
	// Clear to prevent double-release
	cc.chunks = nil
}
