package model

import (
	"fmt"
	"time"

	"github.com/tech/sendico/pkg/merrors"
	"github.com/tech/sendico/pkg/model"
	mduration "github.com/tech/sendico/pkg/mutil/duration"
	"github.com/tech/sendico/server/interface/middleware"
	"go.mongodb.org/mongo-driver/v2/bson"
)

type AccountToken struct {
	AccountRef bson.ObjectID
	Login      string
	Name       string
	Locale     string
	ClientID   string
	Expiration time.Time
	Pending    bool
}

func createAccountToken(a *model.Account, expiration int, clientID string) AccountToken {
	return AccountToken{
		AccountRef: *a.GetID(),
		Login:      a.Login,
		Name:       a.Name,
		Locale:     a.Locale,
		ClientID:   clientID,
		Expiration: time.Now().Add(mduration.Param2Duration(expiration, time.Hour)),
		Pending:    false,
	}
}

func getTokenParam(claims middleware.MapClaims, param string) (string, error) {
	id, ok := claims[param].(string)
	if !ok {
		return "", merrors.NoData(fmt.Sprintf("param '%s' not found", param))
	}

	return id, nil
}

const (
	paramNameID         = "id"
	paramNameName       = "name"
	paramNameLocale     = "locale"
	paramNameLogin      = "login"
	paramNameClientID   = "clientId"
	paramNameExpiration = "exp"
	paramNamePending    = "pending"
)

func Claims2Token(claims middleware.MapClaims) (*AccountToken, error) {
	var at AccountToken
	var err error
	var account string
	if account, err = getTokenParam(claims, paramNameID); err != nil {
		return nil, err
	}
	if at.AccountRef, err = bson.ObjectIDFromHex(account); err != nil {
		return nil, err
	}
	if at.Login, err = getTokenParam(claims, paramNameLogin); err != nil {
		return nil, err
	}
	if at.Name, err = getTokenParam(claims, paramNameName); err != nil {
		return nil, err
	}
	if at.Locale, err = getTokenParam(claims, paramNameLocale); err != nil {
		return nil, err
	}
	if clientID, ok := claims[paramNameClientID]; ok {
		if clientIDText, ok := clientID.(string); ok {
			at.ClientID = clientIDText
		}
	}
	if pending, ok := claims[paramNamePending]; ok {
		if pbool, ok := pending.(bool); ok {
			at.Pending = pbool
		}
	}
	if expValue, ok := claims[paramNameExpiration]; ok {
		switch exp := expValue.(type) {
		case time.Time:
			at.Expiration = exp
		case float64:
			at.Expiration = time.Unix(int64(exp), 0)
		case int64:
			at.Expiration = time.Unix(exp, 0)
		default:
			return nil, merrors.InvalidDataType(fmt.Sprintf("expiration param is of invalid type: %T", expValue))
		}
	} else {
		return nil, merrors.InvalidDataType(fmt.Sprintf("expiration param is of invalid type: %T", expValue))
	}
	return &at, nil
}

func Account2Claims(a *model.Account, expiration int) middleware.MapClaims {
	return Account2ClaimsForClient(a, expiration, "")
}

func Account2ClaimsForClient(a *model.Account, expiration int, clientID string) middleware.MapClaims {
	t := createAccountToken(a, expiration, clientID)
	return middleware.MapClaims{
		paramNameID:         t.AccountRef.Hex(),
		paramNameLogin:      t.Login,
		paramNameName:       t.Name,
		paramNameLocale:     t.Locale,
		paramNameClientID:   t.ClientID,
		paramNameExpiration: t.Expiration.Unix(),
		paramNamePending:    t.Pending,
	}
}

func PendingAccount2Claims(a *model.Account, expirationMinutes int) middleware.MapClaims {
	t := createAccountToken(a, expirationMinutes/60, "")
	t.Expiration = time.Now().Add(time.Duration(expirationMinutes) * time.Minute)
	t.Pending = true
	return middleware.MapClaims{
		paramNameID:         t.AccountRef.Hex(),
		paramNameLogin:      t.Login,
		paramNameName:       t.Name,
		paramNameLocale:     t.Locale,
		paramNameExpiration: t.Expiration.Unix(),
		paramNamePending:    t.Pending,
	}
}
