// Package database provides database client and connection management.
package database

import (
	"context"
	"database/sql"
	"fmt"
	"time"

	"entgo.io/ent/dialect"
	entsql "entgo.io/ent/dialect/sql"
	"git.dcentral.systems/toolz/goplt/internal/ent"
	_ "github.com/lib/pq" // PostgreSQL driver
)

// Client wraps the Ent client with additional functionality.
type Client struct {
	*ent.Client
	db *sql.DB
}

// Config holds database configuration.
type Config struct {
	DSN             string
	Schema          string // Schema name for schema isolation (e.g., "identity", "auth", "authz", "audit")
	MaxConnections  int
	MaxIdleConns    int
	ConnMaxLifetime time.Duration
	ConnMaxIdleTime time.Duration
}

// NewClient creates a new Ent client with connection pooling and schema isolation support.
// If schema is provided, it will be created if it doesn't exist and set as the search path.
func NewClient(cfg Config) (*Client, error) {
	// Open database connection
	db, err := sql.Open("postgres", cfg.DSN)
	if err != nil {
		return nil, fmt.Errorf("failed to open database connection: %w", err)
	}

	// Configure connection pool
	db.SetMaxOpenConns(cfg.MaxConnections)
	db.SetMaxIdleConns(cfg.MaxIdleConns)
	db.SetConnMaxLifetime(cfg.ConnMaxLifetime)
	db.SetConnMaxIdleTime(cfg.ConnMaxIdleTime)

	// Test connection
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	if err := db.PingContext(ctx); err != nil {
		_ = db.Close()
		return nil, fmt.Errorf("failed to ping database: %w", err)
	}

	// Create schema if provided
	if cfg.Schema != "" {
		if err := createSchemaIfNotExists(ctx, db, cfg.Schema); err != nil {
			_ = db.Close()
			return nil, fmt.Errorf("failed to create schema %s: %w", cfg.Schema, err)
		}
		// Set search path to the schema
		if _, err := db.ExecContext(ctx, fmt.Sprintf("SET search_path TO %s", cfg.Schema)); err != nil {
			_ = db.Close()
			return nil, fmt.Errorf("failed to set search path to schema %s: %w", cfg.Schema, err)
		}
	}

	// Create Ent driver
	drv := entsql.OpenDB(dialect.Postgres, db)

	// Create Ent client
	entClient := ent.NewClient(ent.Driver(drv))

	return &Client{
		Client: entClient,
		db:     db,
	}, nil
}

// NewClientWithSchema is a convenience function that creates a client with a specific schema.
func NewClientWithSchema(dsn string, schema string) (*Client, error) {
	return NewClient(Config{
		DSN:             dsn,
		Schema:          schema,
		MaxConnections:  25,
		MaxIdleConns:    5,
		ConnMaxLifetime: 5 * time.Minute,
		ConnMaxIdleTime: 10 * time.Minute,
	})
}

// createSchemaIfNotExists creates a PostgreSQL schema if it doesn't exist.
func createSchemaIfNotExists(ctx context.Context, db *sql.DB, schemaName string) error {
	// Use a transaction to ensure atomicity
	tx, err := db.BeginTx(ctx, nil)
	if err != nil {
		return err
	}
	defer func() {
		_ = tx.Rollback() // Ignore error - if commit succeeded, rollback will error (expected)
	}()

	// Check if schema exists
	var exists bool
	err = tx.QueryRowContext(ctx,
		"SELECT EXISTS(SELECT 1 FROM information_schema.schemata WHERE schema_name = $1)",
		schemaName,
	).Scan(&exists)
	if err != nil {
		return err
	}

	// Create schema if it doesn't exist
	if !exists {
		// Use fmt.Sprintf for schema name since it's a configuration value, not user input
		_, err = tx.ExecContext(ctx, fmt.Sprintf("CREATE SCHEMA IF NOT EXISTS %s", schemaName))
		if err != nil {
			return err
		}
	}

	return tx.Commit()
}

// Close closes the database connection.
func (c *Client) Close() error {
	if err := c.Client.Close(); err != nil {
		return err
	}
	return c.db.Close()
}

// Migrate runs database migrations.
func (c *Client) Migrate(ctx context.Context) error {
	return c.Schema.Create(ctx)
}

// Ping checks database connectivity.
func (c *Client) Ping(ctx context.Context) error {
	return c.db.PingContext(ctx)
}

// DB returns the underlying *sql.DB for advanced operations.
func (c *Client) DB() *sql.DB {
	return c.db
}
