package controller

import (
	"database/sql"
	"fmt"
	"sync"

	"github.com/pingcap/log"
	"github.com/pingcap/tep/pkg/dbconn"
	"github.com/pingcap/tep/pkg/local"
	"github.com/pingcap/tep/pkg/operator"
	"github.com/pingcap/tidb-operator/pkg/apis/pingcap/v1alpha1"
	"go.uber.org/zap"
)

type dbConnControl struct {
	mu  sync.Mutex
	dbs map[string]DBConn
}

func newDBConnControl() *dbConnControl {
	return &dbConnControl{dbs: map[string]DBConn{}}
}

func dbKey(namespace, clusterName string) string {
	return fmt.Sprintf("%s.%s", clusterName, namespace)
}

func (c *Controller) newDB(tc *v1alpha1.TidbCluster) (db *sql.DB, conf dbconn.Config, err error) {
	ns := tc.GetNamespace()
	var password string
	password, err = c.getTiDBSuperUserPassword(ns)
	if err != nil {
		return
	}
	host, port := try2ForwardTiDBServer(c.Forwarder, ns)

	conf = dbconn.Config{
		User:     operator.TiDBSuperUser,
		Password: password,
		Host:     host,
		Port:     port,
		Database: operator.TiDBDatabase,
	}

	db, err = dbconn.NewDatabaseHandle(conf)
	return
}

func (c *Controller) GetDBConn(tc *v1alpha1.TidbCluster) (DBConn, error) {
	c.dcc.mu.Lock()
	defer c.dcc.mu.Unlock()

	ns := tc.GetNamespace()
	tcName := tc.GetName()
	key := dbKey(ns, tcName)
	if _, ok := c.dcc.dbs[key]; !ok {
		db, conf, err := c.newDB(tc)
		if err != nil {
			return nil, err
		}
		c.dcc.dbs[key] = newDBConn(db, conf)
	}

	return c.dcc.dbs[key], nil
}

type DBConn interface {
	GetDB() *sql.DB
	ProbeQuery() error
}

// dbConn implements DBConn interface
type dbConn struct {
	db   *sql.DB
	conf dbconn.Config
}

func newDBConn(db *sql.DB, conf dbconn.Config) DBConn {
	return &dbConn{
		db:   db,
		conf: conf,
	}
}

func (dc *dbConn) GetDB() *sql.DB {
	return dc.db
}

func (dc *dbConn) ProbeQuery() error {
	var count int
	rows, err := dc.db.Query(ProbeSQL)
	if err != nil {
		return fmt.Errorf("database query[%s] error, %v", ProbeSQL, err)
	}
	defer rows.Close()

	for rows.Next() {
		err = rows.Scan(&count)
		if err != nil {
			return fmt.Errorf("scan result sets of query[%s] error, %v", ProbeSQL, err)
		}
	}

	err = rows.Err()
	if err != nil {
		return fmt.Errorf("retrieve result sets of query[%s] error, %v", ProbeSQL, err)
	}

	log.Info("probe query", zap.String("host", dc.conf.Host), zap.Uint16("Port", dc.conf.Port), zap.String("database", dc.conf.Database), zap.String("query", ProbeSQL), zap.Int("result", count))
	return nil
}

func try2ForwardTiDBServer(portForwarder *local.PortForwarder, namespace string) (string, uint16) {
	if portForwarder != nil {
		localPort, err := portForwarder.ForwardServicePort(namespace, fmt.Sprintf("%s-tidb", namespace), 4000)
		if err != nil {
			log.Warn("forward tidb failed", zap.Error(err))
		} else {
			log.Info("forwarded tidb", zap.String("namespace", namespace), zap.Uint16("port", localPort))
			return "localhost", localPort
		}
	}
	return GetTiDBSvcDNSName(namespace), operator.TiDBPort
}
