package authentication_usecase

import (
	"chonho/const"
	"chonho/internal/api-v1/entity"
	"chonho/internal/api-v1/repository/address_repository"
	"chonho/internal/api-v1/repository/fmc_repository"
	"chonho/internal/api-v1/repository/store_repository"
	"chonho/internal/api-v1/repository/user_repository"
	_error "chonho/pkg/c_error"
	"chonho/pkg/jwt"
	"chonho/pkg/model"
	"chonho/pkg/sha"
	"context"
	"encoding/json"
	"errors"
	firebase "firebase.google.com/go"
	"firebase.google.com/go/auth"
	"strings"

	"github.com/google/uuid"
	"gorm.io/gorm"

	"time"

	"google.golang.org/api/option"
)

type UseCase interface {
	ParseJWTToken(token string, ctx context.Context) (*model.User, error)
	RegisterUserByDevice(request *entity.RegisterByDeviceRequest, ctx context.Context) (*entity.UserData, error)
	RegisterUser(request *entity.RegisterRequest, ctx context.Context) (*entity.UserData, error)
	Login(request *entity.LoginRequest, deviceId string, ctx context.Context) (*entity.UserData, error)
	LoginByToken(token string, ctx context.Context) (*entity.UserData, error)
	CheckPhoneNumber(phone string, ctx context.Context) error
}

type useCase struct {
	jwtKey                           string
	tokenLifeTime                    int64
	userRepo                         user_repository.UserRepository
	addressRepo                      address_repository.AddressRepository
	serviceAccountCredentialPathFile string
	app                              *firebase.App
	storeRepo                        store_repository.StoreRepository
	fcmTokenRepo                     fmc_repository.FCMRepository
}

func NewAuthenticationUseCase(
	jwtKey string,
	tokenLifeTime int64,
	userRepo user_repository.UserRepository,
	addressRepo address_repository.AddressRepository,
	credentialFile string,
	storeRepo store_repository.StoreRepository,
	fcmTokenRepo fmc_repository.FCMRepository,
) *useCase {
	opt := option.WithCredentialsFile(credentialFile)

	app, err := firebase.NewApp(context.Background(), nil, opt)
	if err != nil {
		panic("can not connect with firebase project")
	}

	return &useCase{
		jwtKey:                           jwtKey,
		tokenLifeTime:                    tokenLifeTime,
		userRepo:                         userRepo,
		addressRepo:                      addressRepo,
		serviceAccountCredentialPathFile: credentialFile,
		app:                              app,
		storeRepo:                        storeRepo,
		fcmTokenRepo:                     fcmTokenRepo,
	}
}

func (uc *useCase) CheckPhoneNumber(phone string, ctx context.Context) error {

	_, err := uc.userRepo.First(user_repository.UserFilter{
		Phone: phone,
	})

	if err != nil {
		if err == gorm.ErrRecordNotFound {
			return nil
		}
		return _error.New(_const.InternalServerErr, err.Error())
	}

	return _error.New(_const.UserAlreadyExist, "phone is used")

}
func (uc *useCase) ParseJWTToken(token string, ctx context.Context) (*model.User, error) {

	data, err := jwt.Decode(token, uc.jwtKey)
	if err != nil {
		return nil, err
	}

	var user model.User
	err = json.Unmarshal(data, &user)
	if err != nil {
		return nil, err
	}

	if user.Id == 0 {
		return nil, errors.New("invalid token")
	}

	return &user, nil
}

func (uc *useCase) createUserData(user *model.User, address *model.UserShippingAddress, isSeller bool) (*entity.UserData, error) {

	// remove sensitive info
	user.Password = ""
	user.Salt = ""
	user.FireBaseId = nil

	returnData := &entity.UserData{
		User:     user,
		Address:  address,
		IsSeller: isSeller,
	}

	encodeUser := &model.User{
		Id:     user.Id,
		Status: user.Status,
		Phone:  user.Phone,
	}
	userByteData, _ := json.Marshal(encodeUser)
	token, err := jwt.Generate(userByteData, uc.jwtKey, time.Now().Add(time.Duration(uc.tokenLifeTime)*time.Hour))
	if err != nil {
		return nil, _error.New(_const.InternalServerErr, err.Error())
	}
	returnData.Token = token

	return returnData, nil
}

func (uc *useCase) RegisterUserByDevice(request *entity.RegisterByDeviceRequest, ctx context.Context) (*entity.UserData, error) {
	user, err := uc.userRepo.First(user_repository.UserFilter{
		DeviceId: request.DeviceId,
		Status:   _const.UserStatusAnonymous,
	})

	if err != nil && gorm.ErrRecordNotFound != err {
		return nil, _error.New(_const.InternalServerErr, err.Error())
	}

	if user == nil {
		user = &model.User{
			DeviceId: request.DeviceId,
			Status:   _const.UserStatusAnonymous,
		}
		err = uc.userRepo.Save(user)
		if err != nil {
			return nil, _error.New(_const.InternalServerErr, err.Error())
		}
	}

	return uc.createUserData(user, nil, false)
}

func (uc *useCase) RegisterUser(request *entity.RegisterRequest, ctx context.Context) (*entity.UserData, error) {
	// verify token first
	fbId, err := uc.verifyToken(request.FirebaseIdToken, ctx)
	if err != nil {
		return nil, err
	}

	user, _ := uc.userRepo.First(user_repository.UserFilter{
		Phone: request.Phone,
	})
	if user != nil {
		return nil, _error.New(_const.UserAlreadyExist, "phone is already register for a user")
	}

	user, err = uc.userRepo.First(user_repository.UserFilter{
		DeviceId: request.DeviceId,
		Status:   _const.UserStatusAnonymous,
	})

	if err != nil && err != gorm.ErrRecordNotFound {
		return nil, _error.New(_const.InternalServerErr, err.Error())
	}

	if user == nil {
		user = &model.User{
			DeviceId: request.DeviceId,
		}
	}

	user.Status = _const.UserStatusVerified
	user.Name = request.Name
	user.Avatar = request.Avatar
	user.Salt = uuid.New().String()
	user.Password = sha.Decode(request.Password, user.Salt)
	user.FireBaseId = &fbId.UID
	user.Phone = &request.Phone

	err = uc.userRepo.Save(user)
	if err != nil {
		return nil, _error.New(_const.InternalServerErr, err.Error())
	}
	uc.updateFcmToken(request.DeviceId, user.Id)
	return uc.createUserData(user, nil, false)
}

func (uc *useCase) updateFcmToken(deviceId string, userId int64) {
	token, _ := uc.fcmTokenRepo.FirstToken(fmc_repository.TokenFilter{
		DeviceId: deviceId,
	})
	if token != nil {
		token.UserId = userId
		uc.fcmTokenRepo.Save(token)
	}
}

func (uc *useCase) verifyToken(token string, ctx context.Context) (*auth.Token, error) {

	client, err := uc.app.Auth(ctx)
	if err != nil {
		return nil, _error.New(_const.UserVerifyUserWithFireBaseFailed, err.Error())
	}

	verifyToken, err := client.VerifyIDToken(ctx, token)
	if err != nil {
		return nil, _error.New(_const.UserVerifyUserWithFireBaseFailed, err.Error())
	}

	return verifyToken, nil
}

func (uc *useCase) Login(request *entity.LoginRequest, deviceID string, ctx context.Context) (*entity.UserData, error) {
	user, err := uc.userRepo.First(user_repository.UserFilter{
		Phone: request.Phone,
	})
	if err != nil {
		if err == gorm.ErrRecordNotFound {
			return nil, _error.New(_const.UserNotFound, "user not found")
		}
		return nil, _error.New(_const.InternalServerErr, err.Error())
	}

	sendHashedPass := sha.Decode(request.Password, user.Salt)
	if sendHashedPass != user.Password {
		return nil, _error.New(_const.UserWrongPassword, "incorrect password")
	}
	isMain := true
	address, _ := uc.addressRepo.First(address_repository.AddressFilter{
		UserId:        user.Id,
		IsMainAddress: &isMain,
	})

	uc.updateFcmToken(deviceID, user.Id)
	return uc.createUserData(user, address, uc.IsSeller(user.Id))
}

func (uc *useCase) LoginByToken(token string, ctx context.Context) (*entity.UserData, error) {
	// verify token first
	fbToken, err := uc.verifyToken(token, ctx)
	if err != nil {
		return nil, err
	}

	// get phone from token
	identifies := fbToken.Firebase.Identities

	if phones, has := identifies["phone"]; has {
		if phoneArr, ok := phones.([]interface{}); ok {
			if len(phoneArr) > 0 {
				phoneInEb16 := phoneArr[0].(string)
				phoneNumber := strings.Replace(phoneInEb16, "+84", "0", 1)
				user, err := uc.userRepo.First(user_repository.UserFilter{
					Phone: phoneNumber,
				})
				if err != nil {
					if err == gorm.ErrRecordNotFound {
						return nil, _error.New(_const.UserNotFound, "user not found")
					}
					return nil, _error.New(_const.InternalServerErr, err.Error())
				}
				isMain := true
				address, _ := uc.addressRepo.First(address_repository.AddressFilter{
					UserId:        user.Id,
					IsMainAddress: &isMain,
				})
				return uc.createUserData(user, address, uc.IsSeller(user.Id))
			}
		}
	}
	return nil, _error.New(_const.UserNotFound, "user not found")
}

func (uc *useCase) IsSeller(userId int64) bool {
	store, _ := uc.storeRepo.FirstStore(store_repository.StoreFilter{
		UserId: userId,
	})
	return store != nil
}
