package datamysql

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

	"bitbucket.org/nemt/nemt-portal-api/domain/entity"
	"bitbucket.org/nemt/nemt-portal-api/infra/errors"
	"github.com/go-sql-driver/mysql"
	uuid "github.com/satori/go.uuid"
)

// rideRepo maps methods to database
type providerRepo struct {
	conn executor
}

func newProviderRepo(conn executor) *providerRepo {
	return &providerRepo{
		conn: conn,
	}
}

func (c *providerRepo) getProfileQuery(user entity.User) (query string, where string, err error) {
	if len(user.Profiles) > 0 {
		for _, p := range user.Profiles {
			switch p.Key {
			case "AD", "BCBSIAD", "BDCAD", "PLANAD", "VIRPT":
				return
			case "SP", "SPT":
				switch p.Organization.Type.Key {
				case "techsupport", "bcbsi", "bcbsa", "plan":
					return
				case "provider":
					query = ` INNER JOIN     viw_organization_provider c
                        ON          a.provider_id   =   c.provider_id `
					where = fmt.Sprintf(`	AND (c.organization_uuid = '%s' OR c.parent_organization_uuid = '%s') `, p.Organization.UUID, p.Organization.UUID)
					return
				}
			}
		}

		if query == "" && where == "" {
			return "", "", fmt.Errorf("Invalid Query")
		} else {
			return
		}
	} else {
		return "", "", fmt.Errorf("User has no profile to search")
	}
}

func (c *providerRepo) getSelectQueryBaseKey() string {
	return `SELECT  
									a.provider_key_id,
									a.provider_id,
									a.internal_id,
									a.internal_id_suffix,
									a.location_seq_num,
									a.plan_code,
									a.product_id,
									a.treatment_category_code,
									a.active,
									a.enabled,
									a.create_at,
									a.update_at,
									a.created_user
						FROM          tab_provider_key a `
}

func (c *providerRepo) getSelectQueryBase() string {
	return `SELECT DISTINCT
												a.provider_id,
												a.provider_uuid,
												a.provider_internal_id,
												IFNULL(a.provider_internal_id_suffix, '') provider_internal_id_suffix,
												IFNULL(a.provider_muk_id, '') provider_muk_id,
												IFNULL(a.organization_name, '') organization_name,
												IFNULL(a.gender, '') gender,
												IFNULL(a.accept_new_patients, '') accept_new_patients,
												IFNULL(a.provider_name, '') provider_name,
												IFNULL(a.first_name, '') first_name,
												IFNULL(a.last_name, '') last_name,
												IFNULL(a.middle_name, '') middle_name,
												IFNULL(a.provider_title, '') provider_title,
												IFNULL(a.street_name1, '') street_name1,
												IFNULL(a.street_name2, '') street_name2,
												IFNULL(a.city_name, '') city_name,
												IFNULL(a.state, '') state,
												IFNULL(a.zipcode, '') zipcode,
												IFNULL(a.country, '') country,
												IFNULL(a.latitude, 0) latitude,
												IFNULL(a.longitude, 0) longitude,
												IFNULL(a.phone_number, '') phone_number,
												a.create_at,
												a.update_at,
												a.active,
												a.enabled,
												a.created_user,
												(3959 * acos (
														cos ( radians(?) )
														* cos( radians( IFNULL(a.latitude, 0) ) )
														* cos( radians( IFNULL(a.longitude, 0) ) - radians(?) )
														+ sin ( radians(?) )
														* sin( radians( IFNULL(a.latitude, 0) ) )
												)) AS distance_in_miles
								FROM 
																				tab_provider     a
								INNER JOIN     tab_provider_key b
								ON          a.provider_id   =   b.provider_id `
}

func (c *providerRepo) GetAll(user entity.User) ([]entity.Provider, error) {
	lat := 41.886406
	long := -87.624225

	query, where, err := c.getProfileQuery(user)
	if err != nil {
		return nil, err
	}

	query = c.getSelectQueryBase() + query + " WHERE 1 = 1 " + where

	providers, err := c.parseSet(c.conn.Query(query, lat, long, lat))
	if err != nil {
		return nil, err
	}

	return c.getKeys("", "", providers)
}

func (c *providerRepo) GetByUUID(providerUUID string, user entity.User) (entity.Provider, error) {
	lat := 41.886406
	long := -87.624225

	query, where, err := c.getProfileQuery(user)
	if err != nil {
		return entity.Provider{}, err
	}

	query = c.getSelectQueryBase() + query + " WHERE a.provider_uuid = ? " + where

	return c.parseEntity(c.conn.QueryRow(query, lat, long, lat, providerUUID))
}

func (c *providerRepo) GetByID(providerID int64, user entity.User) (entity.Provider, error) {
	lat := 41.886406
	long := -87.624225

	query, where, err := c.getProfileQuery(user)
	if err != nil {
		return entity.Provider{}, err
	}

	query = c.getSelectQueryBase() + query + " WHERE a.provider_id = ? " + where

	return c.parseEntity(c.conn.QueryRow(query, lat, long, lat, providerID))
}

func (c *providerRepo) GetByMukID(mukID string, user entity.User) (entity.Provider, error) {
	lat := 41.886406
	long := -87.624225

	query, where, err := c.getProfileQuery(user)
	if err != nil {
		return entity.Provider{}, err
	}

	query = c.getSelectQueryBase() + query + " WHERE a.provider_muk_id = ? " + where

	return c.parseEntity(c.conn.QueryRow(query, lat, long, lat, mukID))
}

func (c *providerRepo) GetByNPI(NPI string, user entity.User) (entity.Provider, error) {
	lat := 41.886406
	long := -87.624225

	query, where, err := c.getProfileQuery(user)
	if err != nil {
		return entity.Provider{}, err
	}

	query = c.getSelectQueryBase() + query + " WHERE a.provider_internal_id = ? " + where

	return c.parseEntity(c.conn.QueryRow(query, lat, long, lat, NPI))
}

func (c *providerRepo) Get(query string, lat float64, long float64, distance int64, planCode string, productID string, mukID string, internalID string, sort string, user entity.User) ([]entity.Provider, error) {
	filter := " WHERE 1 = 1 "
	params := make([]interface{}, 0)

	if lat == 0 && long == 0 {
		lat = 41.886406
		long = -87.624225
	}
	params = append(params, lat, long, lat)

	if query != "" && (mukID == "" && internalID == "") {
		filter += " AND (a.provider_name LIKE ? OR a.street_name1 LIKE ? OR a.street_name2 LIKE ? OR a.zipcode LIKE ?) "
		// MATCH(a.provider_internal_id, a.provider_internal_id_suffix, a.provider_muk_id, a.provider_name, a.street_name1, a.street_name2, a.city_name, a.state, a.zipcode, a.country, a.phone_number) AGAINST (?)

		query = "%" + query + "%"
		params = append(params, query, query, query, query)
	}

	if planCode != "" {
		filter += " AND b.plan_code = ? "
		params = append(params, planCode)
	}

	if productID != "" {
		filter += " AND b.product_id = ? "
		params = append(params, productID)
	}

	if mukID != "" {
		filter += " AND a.provider_muk_id = ? "
		params = append(params, mukID)
	}

	if internalID != "" {
		filter += " AND a.provider_internal_id = ? "
		params = append(params, internalID)
	}

	final := " HAVING distance_in_miles < ? "
	params = append(params, distance)

	if sort == "name" {
		final += " ORDER BY a.provider_name ASC "
	} else if sort == "distance" {
		final += " ORDER BY distance_in_miles ASC "
	}

	query, where, err := c.getProfileQuery(user)
	if err != nil {
		return nil, err
	}

	query = c.getSelectQueryBase() + query + filter + where + final

	providers, err := c.parseSet(c.conn.Query(query, params...))
	if err != nil {
		return nil, err
	}
	if len(providers) > 0 {
		return c.getKeys(planCode, productID, providers)
	} else {
		return providers, nil
	}
}

func (c *providerRepo) getKeys(planCode string, productID string, providers []entity.Provider) ([]entity.Provider, error) {
	var params []interface{}
	var queryParams = " WHERE a.provider_id IN ("
	for _, p := range providers {
		queryParams += "?,"
		params = append(params, p.ProviderID)
	}
	queryParams = queryParams[0 : len(queryParams)-1]
	queryParams += ") "

	if planCode != "" {
		queryParams += " AND a.plan_code = ? "
		params = append(params, planCode)
	}

	if productID != "" {
		queryParams += " AND a.product_id = ? "
		params = append(params, productID)
	}

	keys, err := c.parseKeySet(c.conn.Query(c.getSelectQueryBaseKey()+queryParams, params...))
	if err != nil {
		return nil, err
	}
	sortedResults := make(map[string][]entity.ProviderKey)
	for _, k := range keys {
		sortedKey := fmt.Sprintf("provider%v", k.Provider.ProviderID)
		sortedResults[sortedKey] = append(sortedResults[sortedKey], k)
	}

	for i := range providers {
		sortedKey := fmt.Sprintf("provider%v", providers[i].ProviderID)
		providers[i].Keys = sortedResults[sortedKey]
	}

	return providers, nil
}

// parseSet parses a result set result to an entity array
func (c *providerRepo) parseKeySet(rows *sql.Rows, err error) ([]entity.ProviderKey, error) {
	if err != nil {
		return nil, errors.Wrap(err)
	}
	result := make([]entity.ProviderKey, 0)
	for rows.Next() {
		entity, err := c.parseKeyEntity(rows)
		if err != nil {
			return nil, errors.Wrap(err)
		}
		result = append(result, entity)
	}
	return result, nil
}

// parseEntity parses a result to an entity
func (c *providerRepo) parseKeyEntity(row scanner) (retVal entity.ProviderKey, err error) {
	err = row.Scan(
		&retVal.ProviderKeyID, &retVal.Provider.ProviderID, &retVal.InternalID, &retVal.InternalSuffixID, &retVal.LocationSeqNumber, &retVal.PlanCode, &retVal.ProductID, &retVal.TreatmentCategoryCode, &retVal.Active, &retVal.Enabled, &retVal.CreateDate, &retVal.UpdateDate, &retVal.CreatedUser.ID)

	return retVal, errors.Wrap(err)
}

// parseSet parses a result set result to an entity array
func (c *providerRepo) parseSet(rows *sql.Rows, err error) ([]entity.Provider, error) {
	if err != nil {
		return nil, errors.Wrap(err)
	}
	result := make([]entity.Provider, 0)
	for rows.Next() {
		entity, err := c.parseEntity(rows)
		if err != nil {
			return nil, errors.Wrap(err)
		}
		result = append(result, entity)
	}
	return result, nil
}

// parseEntity parses a result to an entity
func (c *providerRepo) parseEntity(row scanner) (retVal entity.Provider, err error) {
	err = row.Scan(
		&retVal.ProviderID, &retVal.ProviderUUID, &retVal.InternalID, &retVal.InternalSuffixID, &retVal.MukID, &retVal.OrganizatioName, &retVal.Gender, &retVal.AcceptNewPatients, &retVal.Name, &retVal.FirstName, &retVal.LastName, &retVal.MiddleName, &retVal.Title, &retVal.Address.StreetAddress1, &retVal.Address.StreetAddress2, &retVal.Address.CityName, &retVal.Address.State, &retVal.Address.ZipCode, &retVal.Address.Country, &retVal.Address.Latitude, &retVal.Address.Longitude, &retVal.Address.PhoneNumber, &retVal.CreateDate, &retVal.UpdateDate, &retVal.Active, &retVal.Enabled, &retVal.CreatedUser.ID, &retVal.Distance)

	if err == sql.ErrNoRows {
		return retVal, nil
	} else {
		return retVal, errors.Wrap(err)
	}
}

func (c *providerRepo) Save(providers []entity.ProviderResponse, user entity.User) ([]entity.Provider, error) {
	var savedProviders = make([]entity.Provider, 0)
	for _, p := range providers {
		provider, err := c.saveProvider(p, user)
		if err != nil {
			if driverErr, ok := err.(*mysql.MySQLError); ok {
				if driverErr.Number != 1062 {
					return nil, err
				}
			} else {
				return nil, err
			}
		} else {
			savedProviders = append(savedProviders, provider)
		}
	}

	return savedProviders, nil
}

func (c *providerRepo) saveProvider(provider entity.ProviderResponse, user entity.User) (entity.Provider, error) {
	const (
		query = `INSERT INTO tab_provider(provider_uuid,provider_internal_id,provider_internal_id_suffix,provider_muk_id,organization_name,gender,accept_new_patients,provider_name,first_name,last_name,middle_name,provider_title,street_name1,street_name2,city_name,state,zipcode,country,latitude,longitude,phone_number,created_user)
							VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);`
	)

	UUID, _ := uuid.NewV4()
	lat, _ := strconv.ParseFloat(provider.Latitude, 64)
	long, _ := strconv.ParseFloat(provider.Longitude, 64)

	p := entity.Provider{
		ProviderUUID:      UUID.String(),
		InternalID:        provider.FivePartKeyGroups[0].ProviderNum,
		InternalSuffixID:  provider.FivePartKeyGroups[0].ProviderNumSuffix,
		MukID:             provider.MukID,
		OrganizatioName:   provider.OrgName,
		Gender:            provider.Gender,
		AcceptNewPatients: provider.AcceptNewPatients,
		Name:              provider.ProviderName,
		FirstName:         provider.FirstName,
		MiddleName:        provider.MiddleName,
		LastName:          provider.LastName,
		Title:             provider.ProviderTitle,
		Address: entity.ProviderAddress{
			StreetAddress1: provider.StreetName1,
			StreetAddress2: provider.StreetName2,
			CityName:       provider.CityName,
			State:          provider.State,
			ZipCode:        provider.ZipCode,
			Country:        provider.Country,
			Latitude:       lat,
			Longitude:      long,
			PhoneNumber:    provider.PhoneNumber,
		},
		CreateDate: time.Now(),
		UpdateDate: time.Now(),
		Active:     true,
		Enabled:    true,
	}

	result, err := c.conn.Exec(query, p.ProviderUUID, p.InternalID, p.InternalSuffixID, p.MukID, p.OrganizatioName, p.Gender, p.AcceptNewPatients, p.Name, p.FirstName, p.LastName, p.MiddleName, p.Title, p.Address.StreetAddress1, p.Address.StreetAddress2, p.Address.CityName, p.Address.State, p.Address.ZipCode, p.Address.Country, p.Address.Latitude, p.Address.Longitude, p.Address.PhoneNumber, 1)
	if err != nil {
		if driverErr, ok := err.(*mysql.MySQLError); ok {
			if driverErr.Number != 1062 {
				return entity.Provider{}, err
			} else {
				tempProvider, err := c.GetByMukID(p.MukID, user)
				if err != nil {
					return entity.Provider{}, err
				}
				p.ProviderID = tempProvider.ProviderID
			}
		}
	} else {
		p.ProviderID, err = result.LastInsertId()
		if err != nil {
			return entity.Provider{}, err
		}
	}

	var providerKeys []entity.ProviderKey
	for _, fk := range provider.FivePartKeyGroups {
		key, err := c.saveKeys(p.ProviderID, fk)
		if err != nil {
			return p, err
		}
		providerKeys = append(providerKeys, key)
	}
	p.Keys = providerKeys

	return p, nil
}

func (c *providerRepo) saveKeys(ProviderID int64, fk entity.PartKeyGroup) (entity.ProviderKey, error) {
	const (
		query = `INSERT INTO tab_provider_key (provider_id, internal_id, internal_id_suffix, location_seq_num, plan_code, product_id, treatment_category_code, created_user)
						 VALUES (?, ?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE provider_id = ?, internal_id = ?, internal_id_suffix = ?, location_seq_num = ?, plan_code = ?, product_id = ?, treatment_category_code = ?, created_user = ?;`
	)

	key := entity.ProviderKey{
		InternalID:            fk.ProviderNum,
		InternalSuffixID:      fk.ProviderNumSuffix,
		TreatmentCategoryCode: fk.TreatmentCategoryCode,
		ProductID:             fk.ProductID,
		PlanCode:              fk.PlanCode,
		LocationSeqNumber:     fk.LocationSeqNum,
		Provider: entity.Provider{
			ProviderID: ProviderID,
		},
	}

	result, err := c.conn.Exec(query, key.Provider.ProviderID, key.InternalID, key.InternalSuffixID, key.LocationSeqNumber, key.PlanCode, key.ProductID, key.TreatmentCategoryCode, 1, key.Provider.ProviderID, key.InternalID, key.InternalSuffixID, key.LocationSeqNumber, key.PlanCode, key.ProductID, key.TreatmentCategoryCode, 1)
	if err != nil {
		return key, err
	}

	key.ProviderKeyID, err = result.LastInsertId()
	if err != nil {
		return key, err
	}

	return key, nil
}
