package client

import (
	"context"
	"math"
	"math/rand"
	"sync"
	"time"

	"github.com/pingcap/errors"
)

/*
Pool for efficient reuse of connections.

Usage:
	pool := client.NewPool(log.Debugf, 100, 400, 5, `127.0.0.1:3306`, `username`, `userpwd`, `dbname`)
	...
	conn, _ := pool.GetConn(ctx)
	defer pool.PutConn(conn)
	conn.Execute/conn.Begin/etc...
*/

type (
	Timestamp int64

	LogFunc func(format string, args ...interface{})

	Pool struct {
		logFunc          LogFunc
		minAlive         int
		maxAlive         int
		maxIdle          int
		idleCloseTimeout Timestamp
		idlePingTimeout  Timestamp
		connect          func() (*Conn, error)

		synchro struct {
			sync.Mutex
			idleConnections []Connection
			stats           ConnectionStats
		}

		readyConnection chan Connection
	}

	ConnectionStats struct {
		// Uses internally
		TotalCount int

		// Only for stats
		IdleCount    int
		CreatedCount int64
	}

	Connection struct {
		conn      *Conn
		lastUseAt Timestamp
	}
)

var (
	// MaxIdleTimeoutWithoutPing - If the connection has been idle for more than this time,
	//   then ping will be performed before use to check if it alive
	MaxIdleTimeoutWithoutPing = 10 * time.Second

	// DefaultIdleTimeout - If the connection has been idle for more than this time,
	//   we can close it (but we should remember about Pool.minAlive)
	DefaultIdleTimeout = 30 * time.Second

	// MaxNewConnectionAtOnce - If we need to create new connections,
	//   then we will create no more than this number of connections at a time.
	// This restriction will be ignored on pool initialization.
	MaxNewConnectionAtOnce = 5
)

// NewPool initializes new connection pool and uses params: addr, user, password, dbName and options.
// minAlive specifies the minimum number of open connections that the pool will try to maintain.
// maxAlive specifies the maximum number of open connections (for internal reasons,
// may be greater by 1 inside newConnectionProducer).
// maxIdle specifies the maximum number of idle connections (see DefaultIdleTimeout).
func NewPool(
	logFunc LogFunc,
	minAlive int,
	maxAlive int,
	maxIdle int,
	addr string,
	user string,
	password string,
	dbName string,
	options ...func(conn *Conn),
) *Pool {
	if minAlive > maxAlive {
		minAlive = maxAlive
	}
	if maxIdle > maxAlive {
		maxIdle = maxAlive
	}
	if maxIdle <= minAlive {
		maxIdle = minAlive
	}

	pool := &Pool{
		logFunc:  logFunc,
		minAlive: minAlive,
		maxAlive: maxAlive,
		maxIdle:  maxIdle,

		idleCloseTimeout: Timestamp(math.Ceil(DefaultIdleTimeout.Seconds())),
		idlePingTimeout:  Timestamp(math.Ceil(MaxIdleTimeoutWithoutPing.Seconds())),

		connect: func() (*Conn, error) {
			return Connect(addr, user, password, dbName, options...)
		},

		readyConnection: make(chan Connection),
	}

	pool.synchro.idleConnections = make([]Connection, 0, pool.maxIdle)

	go pool.newConnectionProducer()

	if pool.minAlive > 0 {
		pool.logFunc(`Pool: Setup %d new connections (minimal pool size)...`, pool.minAlive)
		pool.startNewConnections(pool.minAlive)
	}

	go pool.closeOldIdleConnections()

	return pool
}

func (pool *Pool) GetStats(stats *ConnectionStats) {
	pool.synchro.Lock()

	*stats = pool.synchro.stats

	stats.IdleCount = len(pool.synchro.idleConnections)

	pool.synchro.Unlock()
}

// GetConn returns connection from the pool or create new
func (pool *Pool) GetConn(ctx context.Context) (*Conn, error) {
	for {
		connection, err := pool.getConnection(ctx)
		if err != nil {
			return nil, err
		}

		// For long time idle connections, we do a ping check
		if delta := pool.nowTs() - connection.lastUseAt; delta > pool.idlePingTimeout {
			if err := pool.ping(connection.conn); err != nil {
				pool.closeConn(connection.conn)
				continue
			}
		}

		return connection.conn, nil
	}
}

// PutConn returns working connection back to pool
func (pool *Pool) PutConn(conn *Conn) {
	pool.putConnection(Connection{
		conn:      conn,
		lastUseAt: pool.nowTs(),
	})
}

// DropConn closes the connection without any checks
func (pool *Pool) DropConn(conn *Conn) {
	pool.closeConn(conn)
}

func (pool *Pool) putConnection(connection Connection) {
	pool.synchro.Lock()
	defer pool.synchro.Unlock()

	// If someone is already waiting for a connection, then we return it to him
	select {
	case pool.readyConnection <- connection:
		return
	default:
	}

	// Nobody needs this connection

	pool.putConnectionUnsafe(connection)
}

func (pool *Pool) nowTs() Timestamp {
	return Timestamp(time.Now().Unix())
}

func (pool *Pool) getConnection(ctx context.Context) (Connection, error) {
	pool.synchro.Lock()

	connection := pool.getIdleConnectionUnsafe()
	if connection.conn != nil {
		pool.synchro.Unlock()
		return connection, nil
	}
	pool.synchro.Unlock()

	// No idle connections are available

	select {
	case connection := <-pool.readyConnection:
		return connection, nil

	case <-ctx.Done():
		return Connection{}, ctx.Err()
	}
}

func (pool *Pool) putConnectionUnsafe(connection Connection) {
	if len(pool.synchro.idleConnections) == cap(pool.synchro.idleConnections) {
		pool.synchro.stats.TotalCount--
		_ = connection.conn.Close() // Could it be more effective to close older connections?
	} else {
		pool.synchro.idleConnections = append(pool.synchro.idleConnections, connection)
	}
}

func (pool *Pool) newConnectionProducer() {
	var connection Connection
	var err error

	for {
		connection.conn = nil

		pool.synchro.Lock()

		connection = pool.getIdleConnectionUnsafe()
		if connection.conn == nil {
			if pool.synchro.stats.TotalCount >= pool.maxAlive {
				// Can't create more connections
				pool.synchro.Unlock()
				time.Sleep(10 * time.Millisecond)
				continue
			}
			pool.synchro.stats.TotalCount++ // "Reserving" new connection
		}

		pool.synchro.Unlock()

		if connection.conn == nil {
			connection, err = pool.createNewConnection()
			if err != nil {
				pool.synchro.Lock()
				pool.synchro.stats.TotalCount-- // Bad luck, should try again
				pool.synchro.Unlock()

				time.Sleep(time.Duration(10+rand.Intn(90)) * time.Millisecond)
				continue
			}
		}

		pool.readyConnection <- connection
	}
}

func (pool *Pool) createNewConnection() (Connection, error) {
	var connection Connection
	var err error

	connection.conn, err = pool.connect()
	if err != nil {
		return Connection{}, errors.Errorf(`Could not connect to mysql: %s`, err)
	}
	connection.lastUseAt = pool.nowTs()

	pool.synchro.Lock()
	pool.synchro.stats.CreatedCount++
	pool.synchro.Unlock()

	return connection, nil
}

func (pool *Pool) getIdleConnectionUnsafe() Connection {
	cnt := len(pool.synchro.idleConnections)
	if cnt == 0 {
		return Connection{}
	}

	last := cnt - 1
	connection := pool.synchro.idleConnections[last]
	pool.synchro.idleConnections[last].conn = nil
	pool.synchro.idleConnections = pool.synchro.idleConnections[:last]

	return connection
}

func (pool *Pool) closeOldIdleConnections() {
	var toPing []Connection

	ticker := time.NewTicker(5 * time.Second)

	for range ticker.C {
		toPing = pool.getOldIdleConnections(toPing[:0])
		if len(toPing) == 0 {
			continue
		}
		pool.recheckConnections(toPing)

		if !pool.spawnConnectionsIfNeeded() {
			pool.closeIdleConnectionsIfCan()
		}
	}
}

func (pool *Pool) getOldIdleConnections(dst []Connection) []Connection {
	dst = dst[:0]

	pool.synchro.Lock()

	synchro := &pool.synchro

	idleCnt := len(synchro.idleConnections)
	checkBefore := pool.nowTs() - pool.idlePingTimeout

	for i := idleCnt - 1; i >= 0; i-- {
		if synchro.idleConnections[i].lastUseAt > checkBefore {
			continue
		}

		dst = append(dst, synchro.idleConnections[i])

		last := idleCnt - 1
		if i < last {
			// Removing an item from the middle of a slice
			synchro.idleConnections[i], synchro.idleConnections[last] = synchro.idleConnections[last], synchro.idleConnections[i]
		}

		synchro.idleConnections[last].conn = nil
		synchro.idleConnections = synchro.idleConnections[:last]
		idleCnt--
	}

	pool.synchro.Unlock()

	return dst
}

func (pool *Pool) recheckConnections(connections []Connection) {
	const workerCnt = 2 // Heuristic :)

	queue := make(chan Connection, len(connections))
	for _, connection := range connections {
		queue <- connection
	}
	close(queue)

	var wg sync.WaitGroup
	wg.Add(workerCnt)
	for worker := 0; worker < workerCnt; worker++ {
		go func() {
			defer wg.Done()
			for connection := range queue {
				if err := pool.ping(connection.conn); err != nil {
					pool.closeConn(connection.conn)
				} else {
					pool.putConnection(connection)
				}
			}
		}()
	}

	wg.Wait()
}

// spawnConnectionsIfNeeded creates new connections if there are not enough of them and returns true in this case
func (pool *Pool) spawnConnectionsIfNeeded() bool {
	pool.synchro.Lock()
	totalCount := pool.synchro.stats.TotalCount
	idleCount := len(pool.synchro.idleConnections)
	needSpanNew := pool.minAlive - totalCount
	pool.synchro.Unlock()

	if needSpanNew <= 0 {
		return false
	}

	// Не хватает соединений, нужно создать еще

	if needSpanNew > MaxNewConnectionAtOnce {
		needSpanNew = MaxNewConnectionAtOnce
	}

	pool.logFunc(`Pool: Setup %d new connections (total: %d idle: %d)...`, needSpanNew, totalCount, idleCount)
	pool.startNewConnections(needSpanNew)

	return true
}

func (pool *Pool) closeIdleConnectionsIfCan() {
	pool.synchro.Lock()

	canCloseCnt := pool.synchro.stats.TotalCount - pool.minAlive
	canCloseCnt-- // -1 to account for an open but unused connection (pool.readyConnection <- connection in newConnectionProducer)

	idleCnt := len(pool.synchro.idleConnections)

	inFly := pool.synchro.stats.TotalCount - idleCnt

	// We can close no more than 10% connections at a time, but at least 1, if possible
	idleCanCloseCnt := idleCnt / 10
	if idleCanCloseCnt == 0 {
		idleCanCloseCnt = 1
	}
	if canCloseCnt > idleCanCloseCnt {
		canCloseCnt = idleCanCloseCnt
	}
	if canCloseCnt <= 0 {
		pool.synchro.Unlock()
		return
	}

	closeFromIdx := idleCnt - canCloseCnt
	if closeFromIdx < 0 {
		// If there are enough requests in the "flight" now, then we can close all unnecessary
		closeFromIdx = 0
	}

	toClose := append([]Connection{}, pool.synchro.idleConnections[closeFromIdx:]...)

	for i := closeFromIdx; i < idleCnt; i++ {
		pool.synchro.idleConnections[i].conn = nil
	}
	pool.synchro.idleConnections = pool.synchro.idleConnections[:closeFromIdx]

	pool.synchro.Unlock()

	pool.logFunc(`Pool: Close %d idle connections (in fly %d)`, len(toClose), inFly)
	for _, connection := range toClose {
		pool.closeConn(connection.conn)
	}
}

func (pool *Pool) closeConn(conn *Conn) {
	pool.synchro.Lock()
	pool.synchro.stats.TotalCount--
	pool.synchro.Unlock()

	_ = conn.Close() // Closing is not an instant action, so do it outside the lock
}

func (pool *Pool) startNewConnections(count int) {
	connections := make([]Connection, 0, count)
	for i := 0; i < count; i++ {
		if conn, err := pool.createNewConnection(); err == nil {
			pool.synchro.Lock()
			pool.synchro.stats.TotalCount++
			pool.synchro.Unlock()
			connections = append(connections, conn)
		}
	}

	pool.synchro.Lock()
	for _, connection := range connections {
		pool.putConnectionUnsafe(connection)
	}
	pool.synchro.Unlock()
}

func (pool *Pool) ping(conn *Conn) error {
	deadline := time.Now().Add(100 * time.Millisecond)
	_ = conn.SetDeadline(deadline)
	err := conn.Ping()
	if err != nil {
		pool.logFunc(`Pool: ping query fail: %s`, err.Error())
	} else {
		_ = conn.SetDeadline(time.Time{})
	}
	return err
}
