//
//  Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
//
//  Licensed 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 infinity

import (
	"context"
	"errors"
	"fmt"
	"os"
	"ragflow/internal/common"
	"ragflow/internal/server/config"
	"reflect"
	"strconv"
	"strings"
	"sync/atomic"
	"time"

	"github.com/apache/thrift/lib/go/thrift"

	"ragflow/internal/server"

	infinity "github.com/infiniflow/infinity-go-sdk"
	"go.uber.org/zap"
)

type infinityClient struct {
	pool        *infinity.ConnectionPool
	poolMaxOpen int
	dbName      string

	getDatabaseSeq      atomic.Uint64
	getDatabaseInflight atomic.Int64

	// Original URI from config, used by RunSQL to extract the host.
	hostURI string

	// Port for psql wire-protocol listener (default 5432).
	postgresPort int

	// JSON file (under conf/) with the field-name alias map.
	mappingFileName string
}

// defaultPoolMaxSize is the hard upper bound (MaxOpen) for the Go Infinity
// connection pool. Invalid or missing INFINITY_POOL_MAX_SIZE values fall back
// to this rather than becoming unbounded.
const defaultPoolMaxSize = 4

// defaultMaxIdleConnections caps how many established connections the pool
// keeps idle for reuse, independent of the pool's hard open cap (MaxOpen).
// It bounds idle socket retention on the Infinity server when MaxOpen is large.
const defaultMaxIdleConnections = 2

// defaultOperationTimeout bounds the total duration of a single pool operation
// (applied via ensureDeadline when the caller context has no deadline).
const defaultOperationTimeout = 30 * time.Second

// defaultSocketTimeout bounds individual thrift read/write waits on a checked-out
// connection.
const defaultSocketTimeout = 30 * time.Second

// resolvePoolMaxSize reads INFINITY_POOL_MAX_SIZE and falls back to defaultPoolMaxSize. A value < 1 is
// rejected so the pool never becomes unbounded.
func resolvePoolMaxSize() int {
	raw := os.Getenv("INFINITY_POOL_MAX_SIZE")
	if raw == "" {
		return defaultPoolMaxSize
	}
	v, err := strconv.Atoi(raw)
	if err != nil || v < 1 {
		common.Warn("INFINITY_POOL_MAX_SIZE must be a positive integer; using default",
			zap.String("value", raw), zap.Int("default", defaultPoolMaxSize))
		return defaultPoolMaxSize
	}
	return v
}

// NewInfinityClient creates a new Infinity client using the SDK
type poolExhaustedError struct {
	caller string
}

func (e *poolExhaustedError) Error() string {
	return fmt.Sprintf("Infinity connection pool exhausted while %s", e.caller)
}

func (e *poolExhaustedError) Code() common.ErrorCode {
	return common.CodeConnectionPoolExhausted
}

func (e *poolExhaustedError) Message() string {
	return e.Error()
}

func ensureDeadline(ctx context.Context, timeout time.Duration) (context.Context, context.CancelFunc) {
	if ctx == nil {
		return context.WithTimeout(context.Background(), timeout)
	}
	if _, ok := ctx.Deadline(); ok {
		return ctx, func() {}
	}
	return context.WithTimeout(ctx, timeout)
}

func NewInfinityClient(cfg config.InfinityConfig) (*infinityClient, error) {
	// Parse URI like "localhost:23817" to get IP and port
	host := "127.0.0.1"
	port := 23817

	if cfg.URI != "" {
		parts := strings.Split(cfg.URI, ":")
		if len(parts) == 2 {
			host = parts[0]
			if p, err := strconv.Atoi(parts[1]); err == nil {
				port = p
			}
		}
	}

	// Build the connection pool object. Connections are created lazily on
	// first use (InitialSize=0), so this does not verify reachability; the
	// actual connectivity check happens later in WaitForHealthy. The retry
	// loop only re-attempts pool construction against transient config errors.
	common.Info("Connecting to Infinity")
	uri := infinity.NetworkAddress{IP: host, Port: port}
	maxOpen := resolvePoolMaxSize()
	var pool *infinity.ConnectionPool
	var err error
	for i := 0; i < 24; i++ {
		// Cap idle connections retained for reuse; never exceed the pool's
		// hard open cap (MaxOpen).
		maxIdle := maxOpen
		if maxIdle > defaultMaxIdleConnections {
			maxIdle = defaultMaxIdleConnections
		}
		poolCfg := infinity.DefaultConnectionPoolConfig(uri)
		// Don't pre-open connections at process startup (InitialSize=0);
		// connections are created lazily on first use. Keep MaxIdle so that
		// established connections are retained for reuse instead of being
		// reopened on every request.
		poolCfg.InitialSize = 0
		poolCfg.MaxOpen = maxOpen
		poolCfg.MaxIdle = maxIdle
		pool, err = infinity.NewConnectionPool(poolCfg, func(u infinity.URI) (*infinity.InfinityConnection, error) {
			networkAddress, ok := u.(infinity.NetworkAddress)
			if !ok {
				return nil, fmt.Errorf("unexpected URI type: %T", u)
			}
			return infinity.NewInfinityConnectionWithConfig(networkAddress, &thrift.TConfiguration{
				ConnectTimeout: server.DefaultConnectTimeout,
				SocketTimeout:  defaultSocketTimeout,
			})
		})
		if err == nil {
			break
		}
		if i < 23 {
			time.Sleep(5 * time.Second)
		}
	}
	if err != nil {
		return nil, fmt.Errorf("Failed to connect to Infinity after 120s: %w", err)
	}

	client := &infinityClient{
		pool:            pool,
		poolMaxOpen:     maxOpen,
		dbName:          cfg.DBName,
		hostURI:         cfg.URI,
		postgresPort:    cfg.PostgresPort,
		mappingFileName: cfg.MappingFileName,
	}

	return client, nil
}

// WaitForHealthy blocks until Infinity is healthy or timeout
func (c *infinityClient) WaitForHealthy(ctx context.Context, timeout time.Duration) error {
	common.Info("Waiting for Infinity to be healthy")
	deadline := time.Now().Add(timeout)
	for time.Now().Before(deadline) {
		select {
		case <-ctx.Done():
			return ctx.Err()
		default:
		}

		conn, release, err := c.checkoutConn(ctx, "WaitForHealthy")
		if err != nil {
			time.Sleep(5 * time.Second)
			continue
		}
		res, err := conn.ShowCurrentNode()
		release()
		if err != nil {
			time.Sleep(5 * time.Second)
			continue
		}
		// Use reflection to access ErrorCode and ServerStatus fields
		// since ShowCurrentNodeResponse is in an internal package
		v := reflect.ValueOf(res)
		if v.Kind() != reflect.Ptr {
			common.Warn("Infinity health check returned unexpected response kind",
				zap.String("type", fmt.Sprintf("%T", res)),
				zap.String("kind", v.Kind().String()),
			)
			time.Sleep(5 * time.Second)
			continue
		}
		v = v.Elem()
		errorCode := v.FieldByName("ErrorCode")
		serverStatus := v.FieldByName("ServerStatus")
		if !errorCode.IsValid() || !serverStatus.IsValid() {
			common.Warn("Infinity health check response missing expected fields",
				zap.String("type", fmt.Sprintf("%T", res)),
				zap.Bool("has_error_code", errorCode.IsValid()),
				zap.Bool("has_server_status", serverStatus.IsValid()),
			)
			time.Sleep(5 * time.Second)
			continue
		}
		// ErrorCode 0 means OK, ServerStatus "started" or "alive" means healthy
		if errorCode.Int() == 0 {
			status := serverStatus.String()
			if status == "started" || status == "alive" {
				common.Info("Infinity is healthy")
				return nil
			}
		}
		time.Sleep(5 * time.Second)
	}
	return fmt.Errorf("Infinity not healthy after %v", timeout)
}

func (c *infinityClient) checkoutConn(ctx context.Context, caller string) (*infinity.InfinityConnection, func(), error) {
	if c == nil || c.pool == nil {
		return nil, nil, fmt.Errorf("Infinity client not initialized")
	}
	ctx, cancel := ensureDeadline(ctx, defaultOperationTimeout)
	conn, err := c.pool.GetContext(ctx)
	if err != nil {
		cancel()
		if (errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled)) && c.poolMaxOpen > 0 {
			stats := c.pool.Stats()
			if stats.TotalConnections >= c.poolMaxOpen && stats.AvailableConnections == 0 {
				return nil, nil, &poolExhaustedError{caller: caller}
			}
		}
		return nil, nil, err
	}
	released := false
	release := func() {
		if released {
			return
		}
		released = true
		defer cancel()
		if err := c.pool.Put(conn); err != nil {
			common.Warn("Infinity connection release failed",
				zap.String("caller", caller),
				zap.String("conn_ptr", fmt.Sprintf("%p", conn)),
				zap.Error(err),
			)
		}
	}
	return conn, release, nil
}

func (c *infinityClient) checkoutDatabase(ctx context.Context, caller string) (*infinity.Database, func(), error) {
	conn, release, err := c.checkoutConn(ctx, caller)
	if err != nil {
		return nil, nil, err
	}
	reqID := c.getDatabaseSeq.Add(1)
	inflight := c.getDatabaseInflight.Add(1)
	start := time.Now()
	stats := c.pool.Stats()
	common.Info("Infinity GetDatabase begin",
		zap.Uint64("req_id", reqID),
		zap.String("caller", caller),
		zap.String("db_name", c.dbName),
		zap.Bool("is_connected", conn.IsConnected()),
		zap.Int64("inflight", inflight),
		zap.String("conn_ptr", fmt.Sprintf("%p", conn)),
		zap.Int("pool_in_use", stats.InUseConnections),
		zap.Int("pool_available", stats.AvailableConnections),
	)
	db, err := conn.GetDatabase(c.dbName)
	if err != nil {
		inflight = c.getDatabaseInflight.Add(-1)
		elapsed := time.Since(start)
		common.Warn("Infinity GetDatabase failed",
			zap.Uint64("req_id", reqID),
			zap.String("caller", caller),
			zap.String("db_name", c.dbName),
			zap.Bool("is_connected", conn.IsConnected()),
			zap.Int64("inflight", inflight),
			zap.Duration("elapsed", elapsed),
			zap.Error(err),
		)
		release()
		return nil, nil, err
	}
	wrappedRelease := func() {
		inflight := c.getDatabaseInflight.Add(-1)
		elapsed := time.Since(start)
		common.Info("Infinity GetDatabase done",
			zap.Uint64("req_id", reqID),
			zap.String("caller", caller),
			zap.String("db_name", c.dbName),
			zap.Bool("is_connected", conn.IsConnected()),
			zap.Int64("inflight", inflight),
			zap.Duration("elapsed", elapsed),
		)
		release()
	}
	return db, wrappedRelease, nil
}

// Engine Infinity engine implementation using Go SDK
type Engine struct {
	config                 config.InfinityConfig
	client                 *infinityClient
	mappingFileName        string
	docMetaMappingFileName string
}

// NewEngine creates an Infinity engine
func NewEngine(infinityConfig config.InfinityConfig) (*Engine, error) {

	client, err := NewInfinityClient(infinityConfig)
	if err != nil {
		return nil, err
	}

	mappingFileName := infinityConfig.MappingFileName
	if mappingFileName == "" {
		mappingFileName = "infinity_mapping.json"
	}
	docMetaMappingFileName := infinityConfig.DocMetaMappingFileName
	if docMetaMappingFileName == "" {
		docMetaMappingFileName = "doc_meta_infinity_mapping.json"
	}

	engine := &Engine{
		config:                 infinityConfig,
		client:                 client,
		mappingFileName:        mappingFileName,
		docMetaMappingFileName: docMetaMappingFileName,
	}

	// Wait for Infinity to be healthy
	if err = client.WaitForHealthy(context.Background(), 120*time.Second); err != nil {
		return nil, fmt.Errorf("Infinity not healthy: %w", err)
	}

	// MigrateDB creates the database if it doesn't exist
	if err = engine.MigrateDB(context.Background()); err != nil {
		return nil, fmt.Errorf("failed to migrate database: %w", err)
	}

	return engine, nil
}

// GetType returns the engine type
func (e *Engine) GetType() string {
	return "infinity"
}

// SupportsPageRank returns false because Infinity does not support pagerank.
func (e *Engine) SupportsPageRank() bool {
	return false
}

// Ping checks if Infinity is accessible
func (e *Engine) Ping(ctx context.Context) error {
	if e.client == nil || e.client.pool == nil {
		return fmt.Errorf("Infinity client not initialized")
	}
	conn, release, err := e.client.checkoutConn(ctx, "Ping")
	if err != nil {
		return err
	}
	defer release()
	if !conn.IsConnected() {
		return fmt.Errorf("Infinity not connected")
	}
	return nil
}

// Close closes the Infinity connection
func (e *Engine) Close() error {
	if e.client != nil && e.client.pool != nil {
		return e.client.pool.Close()
	}
	return nil
}

// MigrateDB creates the database if it doesn't exist
func (e *Engine) MigrateDB(ctx context.Context) error {
	conn, release, err := e.client.checkoutConn(ctx, "MigrateDB")
	if err != nil {
		return fmt.Errorf("failed to get connection: %w", err)
	}
	defer release()
	_, err = conn.CreateDatabase(e.client.dbName, infinity.ConflictTypeIgnore, "")
	if err != nil {
		return fmt.Errorf("failed to create database: %w", err)
	}
	return nil
}
