package datamysql

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

	"bitbucket.org/nemt/nemt-portal-api/domain/contract"
	"bitbucket.org/nemt/nemt-portal-api/infra/config"
	"bitbucket.org/nemt/nemt-portal-api/infra/errors"
	"github.com/go-sql-driver/mysql"
)

var (
	instance *Conn
	once     sync.Once
	connErr  error
)

// Conn is the MySQL connection manager
type Conn struct {
	db            *sql.DB
	users         *userRepo
	rides         *rideRepo
	visit         *visitRepo
	provider      *providerRepo
	notification  *notificationRepo
	profile       *profileRepo
	organization  *organizationRepo
	zipcodes      *zipcodeRepo
	plan          *planRepo
	passwordReset *passwordResetRepo
}

// Begin starts a transaction
func (c *Conn) Begin() (contract.TransactionManager, error) {
	tx, err := c.db.Begin()
	if err != nil {
		return nil, errors.Wrap(err)
	}

	return newTransaction(tx), nil
}

// Close closes the db connection
func (c *Conn) Close() (err error) {
	return c.db.Close()
}

// Users returns the users set
func (c *Conn) Users() contract.UserRepo {
	return c.users
}

// Rides returns the rides set
func (c *Conn) Rides() contract.RideRepo {
	return c.rides
}

// Visits returns the rides set
func (c *Conn) Visits() contract.VisitRepo {
	return c.visit
}

// Provider returns the rides set
func (c *Conn) Provider() contract.ProviderRepo {
	return c.provider
}

// Provider returns the rides set
func (c *Conn) Profile() contract.ProfileRepo {
	return c.profile
}

// Provider returns the rides set
func (c *Conn) Notification() contract.NotificationRepo {
	return c.notification
}

// Provider returns the rides set
func (c *Conn) Organization() contract.OrganizationRepo {
	return c.organization
}

func (c *Conn) Zipcodes() contract.ZipcodeRepo {
	return c.zipcodes
}

func (c *Conn) Plans() contract.PlanRepo {
	return c.plan
}

func (c *Conn) PasswordReset() contract.PasswordResetRepo {
	return c.passwordReset
}

// Instance returns an instance of a DataManager
func Instance(cfg *config.Config) (contract.DataManager, error) {
	once.Do(func() {

		db, err := sql.Open("mysql", getMySQLConfig(cfg).FormatDSN())
		if err != nil {
			connErr = errors.Wrap(err)
			return
		}

		err = db.Ping()
		if err != nil {
			connErr = errors.Wrap(err)
			return
		}

		connMaxLifetime := time.Duration(cfg.DB.MaxLifeInMinutes) * time.Minute
		db.SetConnMaxLifetime(connMaxLifetime)
		db.SetMaxIdleConns(cfg.DB.MaxIdleConns)
		db.SetMaxOpenConns(cfg.DB.MaxOpenConns)

		instance = new(Conn)
		instance.db = db

		instance.users = newUserRepo(db)
		instance.rides = newRideRepo(db)
		instance.visit = newVisitRepo(db)
		instance.provider = newProviderRepo(db)
		instance.notification = newNotificationRepo(db)
		instance.profile = newProfileRepo(db)
		instance.organization = newOrganizationRepo(db)
		instance.zipcodes = newZipcodeRepo(db)
		instance.plan = newPlanRepo(db)
		instance.passwordReset = newPasswordResetRepo(db)
	})

	return instance, connErr
}

// getMySQLConfig returns the configuration for MySQL driver
func getMySQLConfig(cfg *config.Config) *mysql.Config {
	loc, _ := time.LoadLocation("UTC")

	return &mysql.Config{
		Net:       "tcp",
		Addr:      fmt.Sprintf("%s:%d", cfg.DB.Host, cfg.DB.Port),
		DBName:    cfg.DB.Name,
		User:      cfg.DB.User,
		Passwd:    cfg.DB.Pass,
		ParseTime: true,
		Strict:    true,
		Loc:       loc,
	}
}
