package applicationservice

import (
	"context"
	"encoding/xml"
	"fmt"
	"html"
	"math/rand"
	"strings"
	"time"

	"bitbucket.org/nemt/nemt-portal-api/application/entitymapping"
	"bitbucket.org/nemt/nemt-portal-api/application/third/eligibility/bcbsi"
	"bitbucket.org/nemt/nemt-portal-api/application/viewmodel"
	"bitbucket.org/nemt/nemt-portal-api/domain/entity"
	"bitbucket.org/nemt/nemt-portal-api/domain/service"
	"bitbucket.org/nemt/nemt-portal-api/infra/config"
	"bitbucket.org/nemt/nemt-portal-api/infra/errors"
	"bitbucket.org/nemt/nemt-portal-api/server/validation"
	"googlemaps.github.io/maps"
)

// userService holds methods to user application service
type userService struct {
	svc       *service.Service
	mapEntity *entitymapping.Mapper
	bcbsi     *bcbsi.Service
	cfg       *config.Config
}

// newUserService returns a userService instance
func newUserService(svc *service.Service, mapper *entitymapping.Mapper, bcbsi *bcbsi.Service, cfg *config.Config) *userService {
	return &userService{
		svc:       svc,
		mapEntity: mapper,
		bcbsi:     bcbsi,
		cfg:       cfg,
	}
}

// GetAll returns a list of users
func (s *userService) GetAll(quantity int64, page int64) (retVal []viewmodel.User, err error) {
	users, err := s.svc.Users.GetAll()
	if err != nil {
		return retVal, errors.Wrap(err)
	}
	return s.mapEntity.User.ToUserModelSlice(users), nil
}

// GetByID returns a specific user by its ID
func (s *userService) GetByID(id int64) (retVal viewmodel.User, err error) {
	user, err := s.svc.Users.GetByID(id)
	if err != nil {
		return retVal, errors.Wrap(err)
	}

	return s.mapEntity.User.ToUserModel(user), nil
}

// GetByID returns a specific user by its ID
func (s *userService) GetByUUID(uuid string, profile string) (retVal viewmodel.User, err error) {
	user, err := s.svc.Users.GetByUUID(uuid, profile)
	if err != nil {
		return retVal, errors.Wrap(err)
	}

	return s.mapEntity.User.ToUserModel(user), nil
}

// GetByID returns a specific user by its ID
func (s *userService) GetByMemberID(memberID string) (retVal viewmodel.User, err error) {
	user, err := s.svc.Users.GetByMemberID(memberID)
	if err != nil {
		return retVal, errors.Wrap(err)
	}

	return s.mapEntity.User.ToUserModel(user), nil
}

// GetByEmail returns a specific user by its email
func (s *userService) GetByEmail(email string) (retVal viewmodel.User, err error) {
	user, err := s.svc.Users.GetByEmail(email)
	if err != nil {
		return retVal, errors.Wrap(err)
	}

	return s.mapEntity.User.ToUserModel(user), nil
}

// Login returns a specific user by email and pass
func (s *userService) FullLogin(loginType string, key string, pass string, profile string) (retVal viewmodel.User, err error) {
	user, err := s.svc.Users.FullLogin(loginType, key, pass, profile)
	if err != nil {
		return retVal, errors.Wrap(err)
	}

	return s.mapEntity.User.ToUserModel(user), nil
}

// Login returns a specific user by email and pass
func (s *userService) Login(email string, pass string) (retVal viewmodel.User, err error) {
	user, err := s.svc.Users.Login(email, pass)
	if err != nil {
		return retVal, errors.Wrap(err)
	}

	return s.mapEntity.User.ToUserModel(user), nil
}

func (s *userService) Create(user viewmodel.User, author viewmodel.User) (retVal viewmodel.User, err error) {
	entity := s.mapEntity.User.ToUserEntity(user)
	enAuthor := s.mapEntity.User.ToUserEntity(author)

	for i, _ := range entity.Organizations {
		entity.Organizations[i], err = s.svc.Organization.GetByUUID(entity.Organizations[i].UUID, enAuthor)
		if err != nil {
			return retVal, err
		}
	}

	entity, err = s.svc.Users.Create(entity)
	if err != nil {
		return retVal, errors.Wrap(err)
	}

	return s.mapEntity.User.ToUserModel(entity), nil
}

func (s *userService) CreateBulk(users []viewmodel.User, author viewmodel.User) (retVal []viewmodel.User, err error) {
	entities := s.mapEntity.User.ToUserEntitySlice(users)
	enAuthor := s.mapEntity.User.ToUserEntity(author)
	organizations := make([]entity.Organization, 0)
	for i, _ := range entities {
		if i == 0 {
			for o, _ := range entities[i].Organizations {
				org, err := s.svc.Organization.GetByUUID(entities[i].Organizations[o].UUID, enAuthor)
				if err != nil {
					return nil, err
				}
				organizations = append(organizations, org)
			}
		}
		entities[i].Organizations = organizations
	}

	entities, err = s.svc.Users.CreateBulk(entities)
	if err != nil {
		return nil, errors.Wrap(err)
	}

	return s.mapEntity.User.ToUserModelSlice(entities), nil
}

// GetUsersByProfile returns a list of users by profile
func (s *userService) GetUsersByProfile(profile string) (retVal []viewmodel.User, err error) {
	users, err := s.svc.Users.GetUsersByProfile(profile)
	if err != nil {
		return retVal, errors.Wrap(err)
	}

	list := s.mapEntity.User.ToUserModelSlice(users)

	return list, nil
}

func (s *userService) RemoveAddress(addressUUID string) error {
	return s.svc.Users.RemoveAddress(addressUUID)
}

func (s *userService) UpdateLogin(user viewmodel.User) error {
	eUser := s.mapEntity.User.ToUserEntity(user)
	return s.svc.Users.UpdateLogin(eUser)
}

func (s *userService) SaveAddress(address viewmodel.Address) (retVal viewmodel.Address, err error) {
	entity := s.mapEntity.Address.ToAddressEntity(address)
	entity, err = s.svc.Users.SaveAddress(entity)
	if err != nil {
		return retVal, errors.Wrap(err)
	}

	return s.mapEntity.Address.ToAddressModel(entity), err
}

func (s *userService) GetContactType() (retVal []viewmodel.ContactType, err error) {
	entity, err := s.svc.Users.GetContactType()
	if err != nil {
		return retVal, errors.Wrap(err)
	}

	return s.mapEntity.User.ToContactTypeModelSlice(entity), nil
}

func (s *userService) SaveContact(contact viewmodel.Contact) (retVal viewmodel.Contact, err error) {
	entity := s.mapEntity.User.ToContactEntity(contact)
	entity, err = s.svc.Users.SaveContact(entity)
	if err != nil {
		return retVal, errors.Wrap(err)
	}

	return s.mapEntity.User.ToContactModel(entity), err
}

func (s *userService) RemoveContact(contact viewmodel.Contact) (retVal viewmodel.Contact, err error) {
	entity := s.mapEntity.User.ToContactEntity(contact)
	entity, err = s.svc.Users.RemoveContact(entity)
	if err != nil {
		return retVal, errors.Wrap(err)
	}

	return s.mapEntity.User.ToContactModel(entity), err
}

func (s *userService) rangeIn(low, hi int) string {
	result := low + rand.Intn(hi-low)
	return fmt.Sprintf("%v", result)
}

func (s *userService) generatePassword(n int) string {
	const (
		charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
	)

	return s.stringWithCharset(n, charset)
}

func (s *userService) stringWithCharset(length int, charset string) string {
	b := make([]byte, length)
	var seededRand *rand.Rand = rand.New(
		rand.NewSource(time.Now().UnixNano()))
	for i := range b {
		b[i] = charset[seededRand.Intn(len(charset))]
	}
	return string(b)
}

func (s *userService) CheckAndCreateMember(user viewmodel.User, provider viewmodel.ProviderResp, authorUser viewmodel.User) (viewmodel.User, error) {
	if validationErrors := validation.ValidateEligibility(&user); len(validationErrors) > 0 {
		return viewmodel.User{}, &viewmodel.ValidationError{Message: "Eligibility validation failed", Errors: validationErrors}
	}

	entityUser := s.mapEntity.User.ToUserEntity(user)
	entityAuthorUser := s.mapEntity.User.ToUserEntity(authorUser)
	entityProvider := s.mapEntity.Provider.ToProviderRespEntity(provider)

	newEmail := entityUser.Email
	newPhoneNumber := entityUser.PhoneNumber

	var err error

	if entityUser.UUID != "" {
		entityUser, err = s.svc.Users.GetByUUID(entityUser.UUID, "US")
		if err != nil {
			return viewmodel.User{}, &viewmodel.ValidationError{Message: fmt.Sprintf("Error finding user by UUID: %s", err.Error())}
		}
	} else {
		entityUser, err = s.svc.Users.GetByMemberID(entityUser.Member)
		if err != nil {
			return viewmodel.User{}, &viewmodel.ValidationError{Message: fmt.Sprintf("Error finding user by Subscriber ID: %s", err.Error())}
		}
	}

	if entityUser.UUID == "" {
		entityUser = s.mapEntity.User.ToUserEntity(user)
	}

	var eligibility viewmodel.Eligibility
	loc, _ := time.LoadLocation("America/Chicago")
	eligibility.TrackingID = s.rangeIn(1000000, 9999999)
	eligibility.ServiceInfo.DateOfService = time.Now().In(loc)
	eligibility.ServiceInfo.ServiceTypeCodes = []string{"30"}

	if entityProvider.InternalID == "" && entityProvider.ProviderUUID != "" {
		entityProvider, err = s.svc.Provider.GetByUUID(entityProvider.ProviderUUID, entityAuthorUser)
		if err != nil {
			return viewmodel.User{}, &viewmodel.ValidationError{Message: fmt.Sprintf("Error finding provider by UUID: %s", err.Error())}
		}
	} else if entityProvider.InternalID != "" {
		entityProvider, err = s.svc.Provider.GetByNPI(entityProvider.InternalID, entityAuthorUser)
		if err != nil {
			return viewmodel.User{}, &viewmodel.ValidationError{Message: fmt.Sprintf("Error finding provider by NPI: %s", err.Error())}
		}
	}

	if entityProvider.InternalID == "" {
		eligibility.Provider.ProviderNPI = "1699849786"
		eligibility.Provider.ProviderName = "LITHOLINK CORPORATION"
	} else {
		eligibility.Provider.ProviderNPI = entityProvider.InternalID
		eligibility.Provider.ProviderName = entityProvider.OrganizatioName
	}

	plan, err := s.svc.Plans.GetByAlphaPrefix(entityUser.Member[:3])
	if err != nil {
		return viewmodel.User{}, &viewmodel.ValidationError{Message: fmt.Sprintf("Error finding Plan: %s", err.Error())}
	}
	eligibility.Payer.PayerID = fmt.Sprintf("%v", plan.PayerID)
	eligibility.Payer.PayerName = plan.PayerName

	eligibility.Subscriber.SubscriberID = entityUser.Member
	eligibility.Subscriber.PatientType = entityUser.Type
	eligibility.Subscriber.Name.First = strings.Split(entityUser.Name, " ")[0]
	eligibility.Subscriber.Name.Last = strings.Split(entityUser.Name, " ")[1]
	eligibility.Subscriber.DemographicInfo.DateOfBirth = entityUser.BirthDate
	eligibility.Subscriber.DemographicInfo.Gender = entityUser.Gender

	ret, err := s.bcbsi.BXE.CheckEligibility(eligibility)
	if err != nil {
		return viewmodel.User{}, &viewmodel.ValidationError{Message: fmt.Sprintf("Error checking eligibility: %s", err.Error())}
	}

	if ret.QueryResult.QueryResultSummary.BenefitsFound {
		xmlString := html.UnescapeString(ret.QueryResult.HIPPA271.T271)
		xmlReader := strings.NewReader(xmlString)

		var f viewmodel.Interchange271
		err = xml.NewDecoder(xmlReader).Decode(&f)
		if err != nil {
			return viewmodel.User{}, &viewmodel.ValidationError{Message: fmt.Sprintf("Error unmarshaling 271 response: %s", err.Error())}
		}

		header := f.Division.HealthCareEligibilityResponse.LoopHL0030[0].HL_0460[0].HL_0890[0].NM1_0920[0].N3_0950
		body := f.Division.HealthCareEligibilityResponse.LoopHL0030[0].HL_0460[0].HL_0890[0].NM1_0920[0].N4_0960
		zipCode := strings.TrimSpace(body.N403)

		if len(strings.TrimSpace(zipCode)) > 5 {
			zipCode = strings.TrimSpace(zipCode)[:5]
		}

		if !s.cfg.App.DisableZipValidation {
			_, err = s.svc.Zipcodes.GetByParticipatingZipcode(zipCode)
			if err != nil {
				return viewmodel.User{}, &viewmodel.ValidationError{Message: fmt.Sprintf("Member's Home zipcode, %s, is not currently eligible for participation in this program", zipCode)}
			}
		}

		if entityUser.ID == 0 {
			profile, err := s.svc.Profile.GetByKey("US")
			if err != nil {
				return viewmodel.User{}, &viewmodel.ValidationError{Message: fmt.Sprintf("Error getting user profile: %s", err.Error())}
			}
			entityUser.Profiles = append(entityUser.Profiles, profile)

			if len(entityUser.PhoneNumber) == 10 && !strings.Contains(entityUser.PhoneNumber, "+1") {
				phoneNumber := fmt.Sprintf("+1%s", entityUser.PhoneNumber)
				entityUser.PhoneNumber = phoneNumber
			}

			entityUser.Test = false
			entityUser, err = s.svc.Users.Create(entityUser)
			if err != nil {
				return viewmodel.User{}, &viewmodel.ValidationError{Message: fmt.Sprintf("Error creating user: %s", err.Error())}
			}
		} else {
			if len(newEmail) > 0 && newEmail != entityUser.Email {
				entityUser.Email = newEmail
			}

			if len(newPhoneNumber) > 0 && len(newPhoneNumber) == 10 && !strings.Contains(newPhoneNumber, "+1") {
				newPhoneNumber = fmt.Sprintf("+1%s", newPhoneNumber)
			}

			if len(newPhoneNumber) > 0 && len(newPhoneNumber) == 12 && strings.Contains(newPhoneNumber, "+1") {
				entityUser.PhoneNumber = newPhoneNumber
			}

			err = s.svc.Users.UpdateLogin(entityUser)
			if err != nil {
				return viewmodel.User{}, &viewmodel.ValidationError{Message: fmt.Sprintf("Error updating login data: %s", err.Error())}
			}
		}

		address := entity.Address{}
		address.AddressType = entity.Params{
			Key:  "home",
			Name: "Home",
		}
		address.Name = fmt.Sprintf("%s, %s", header.N301, body.N401)
		address.Address = fmt.Sprintf("%s, %s (%s)", header.N301, body.N401, zipCode)
		address.CreatedUser = entityAuthorUser
		address.User = entityUser
		address.Origin = entity.Params{
			Key:  "provider",
			Name: "Provider",
		}

		googleMapsAPI, err := maps.NewClient(maps.WithClientIDAndSignature("gme-bluecrossandblue1", "msqgD-jdqCyR0M_1u5C1HION5iI="))
		if err != nil {
			return viewmodel.User{}, &viewmodel.ValidationError{Message: fmt.Sprintf("Error instanciating google maps: %s", err.Error())}
		}

		r := &maps.GeocodingRequest{
			Address: address.Address + " " + body.N402 + ", " + zipCode,
		}

		result, err := googleMapsAPI.Geocode(context.Background(), r)
		if err != nil {
			return viewmodel.User{}, &viewmodel.ValidationError{Message: fmt.Sprintf("Error getting geocode info: %s", err.Error())}
		}

		if len(result) > 0 {
			address.Latitude = result[0].Geometry.Location.Lat
			address.Longitude = result[0].Geometry.Location.Lng
		}

		if len(user.Addresses) > 0 {
			for _, a := range user.Addresses {
				if a.AddressType == "home" {
					err := s.svc.Users.RemoveAddress(a.UUID)
					if err != nil {
						return viewmodel.User{}, &viewmodel.ValidationError{Message: fmt.Sprintf("Error removing old address: %s", err.Error())}
					}
				}
			}
		}

		address, err = s.svc.Users.SaveAddress(address)
		if err != nil {
			return viewmodel.User{}, &viewmodel.ValidationError{Message: fmt.Sprintf("Error saving new address: %s", err.Error())}
		}
		entityUser.Addresses = append(entityUser.Addresses, address)

		return s.mapEntity.User.ToUserModel(entityUser), nil
	} else {
		return viewmodel.User{}, &viewmodel.ValidationError{Message: "No benefits found for the member"}
	}

	return viewmodel.User{}, nil
}
