package domain

import (
	"context"
	"errors"
	"fmt"
	"time"

	"git.vdhsn.com/barretthousen/barretthousen/src/auth/internal/data"
	"git.vdhsn.com/barretthousen/barretthousen/src/lib/kernel"
)

type (
	Domain struct {
		Storage
		PasswordHasher
	}

	HashedPassword []byte

	PasswordHasher interface {
		Hash(string) HashedPassword
		Compare(string, HashedPassword) bool
	}

	Storage interface {
		RegisterAccount(context.Context, data.RegisterAccountInput) (int, error)
		GetAccountBySession(context.Context, int) (data.AccountResult, error)
		GetAccount(context.Context, data.GetAccountInput) (data.AccountResult, error)
		CreateSession(context.Context, data.CreateSessionInput) (data.Session, error)
		VerifyAccount(context.Context, data.VerifyAccountInput) error
	}
)

type (
	CreateAccountCommand struct {
		Email    string
		Password string
		Role     Role
	}
	AccountCreated struct {
		Account
	}
)

func (d *Domain) CreateAccount(ctx context.Context, in CreateAccountCommand) (out AccountCreated, err error) {
	if in.Email == "" {
		err = errors.New("email cannot be empty")
		return
	}

	if in.Password == "" {
		err = errors.New("password cannot be empty")
		return
	}

	if in.Role == EmptyRole {
		in.Role = UserRole
	}

	var accountID int
	if accountID, err = d.Storage.RegisterAccount(ctx, data.RegisterAccountInput{
		Email:        in.Email,
		PasswordHash: in.Password,
		Role:         in.Role.String(),
	}); err != nil {
		err = fmt.Errorf("could not register account: %w", err)
		return
	}

	var ar data.AccountResult
	if ar, err = d.Storage.GetAccount(ctx, data.GetAccountInput{
		Email:     in.Email,
		AccountID: accountID,
	}); err != nil {
		err = fmt.Errorf("could not find account by ID: %w", err)
		return
	}

	out = AccountCreated{
		Account: Account{
			ID:           ar.ID,
			Created:      ar.Created,
			Verified:     ar.Verified,
			Email:        ar.Email,
			PasswordHash: ar.PasswordHash,
			Role:         Role(ar.Role),
			Enabled:      ar.Enabled,
		},
	}

	return
}

type (
	LoginCommand struct {
		Email    string
		Password string
	}
	LoggedIn struct {
		Account
		SessionID int
		Created   time.Time
	}
)

var (
	ErrInvalidLogin   = errors.New("invalid credentials provided")
	ErrInvalidSession = errors.New("invalid session id provided")
)

func (d *Domain) Login(ctx context.Context, in LoginCommand) (out LoggedIn, err error) {
	kernel.TraceLog.Printf("%q login attempt", in.Email)
	var sess data.Session
	if sess, err = d.Storage.CreateSession(ctx, data.CreateSessionInput{
		Email:        in.Email,
		PasswordHash: in.Password,
	}); errors.Is(err, data.ErrCouldNotCreateSession) {
		err = ErrInvalidLogin
		return
	} else if err != nil {
		kernel.ErrorLog.Printf("Error creating session in storage for %q: %w", in.Email, err)
		err = ErrInvalidLogin
		return
	}

	var ar data.AccountResult
	if ar, err = d.Storage.GetAccount(ctx, data.GetAccountInput{
		AccountID: sess.AccountID,
	}); err != nil {
		err = fmt.Errorf("could not find account by ID: %w", err)
		return
	}

	out = LoggedIn{
		Account: Account{
			ID:       ar.ID,
			Created:  ar.Created,
			Verified: ar.Verified,
			Email:    ar.Email,
			Role:     Role(ar.Role),
			Enabled:  ar.Enabled,
		},
		Created:   sess.Created,
		SessionID: sess.ID,
	}

	return
}

type (
	GetAccountCommand struct {
		SessionID int
	}
)

func (d *Domain) GetAccount(ctx context.Context, in GetAccountCommand) (out Account, err error) {
	var ar data.AccountResult
	if ar, err = d.Storage.GetAccountBySession(ctx, in.SessionID); errors.Is(err, data.ErrSessionNotFound) {
		err = ErrInvalidSession
		return
	} else if err != nil {
		kernel.ErrorLog.Printf("Error getting account by session id: %w", err)
		err = fmt.Errorf("could not get account: %w", err)
		return
	}

	out = Account{
		ID:       ar.ID,
		Created:  ar.Created,
		Verified: ar.Verified,
		Email:    ar.Email,
		Role:     Role(ar.Role),
		Enabled:  ar.Enabled,
	}
	return
}

type (
	VerifyAccountCommand struct{}
	AccountVerified      struct{}
)

func (d *Domain) VerifyAccount(ctx context.Context, in VerifyAccountCommand) (out AccountVerified, err error) {
	err = errors.New("Unimplemented")
	return
}
