package controllers

import (
	"context"
	"crypto/sha256"
	"encoding/base64"
	"errors"
	"fmt"
	"log/slog"
	"net/http"
	"strings"
	"time"

	"github.com/emochka2007/block-accounting/internal/interface/rest/domain"
	"github.com/emochka2007/block-accounting/internal/interface/rest/presenters"
	"github.com/emochka2007/block-accounting/internal/pkg/bip39"
	"github.com/emochka2007/block-accounting/internal/pkg/ctxmeta"
	"github.com/emochka2007/block-accounting/internal/pkg/hdwallet"
	"github.com/emochka2007/block-accounting/internal/usecase/interactors/jwt"
	"github.com/emochka2007/block-accounting/internal/usecase/interactors/organizations"
	"github.com/emochka2007/block-accounting/internal/usecase/interactors/users"
	"github.com/emochka2007/block-accounting/internal/usecase/repository/auth"
	"github.com/go-chi/chi/v5"
)

var (
	ErrorAuthInvalidMnemonic = errors.New("invalid mnemonic")
	ErrorTokenRequired       = errors.New("token required")
)

type AuthController interface {
	Join(w http.ResponseWriter, req *http.Request) ([]byte, error)
	JoinWithInvite(w http.ResponseWriter, req *http.Request) ([]byte, error)
	Login(w http.ResponseWriter, req *http.Request) ([]byte, error)
	Invite(w http.ResponseWriter, req *http.Request) ([]byte, error)
	Refresh(w http.ResponseWriter, req *http.Request) ([]byte, error)
	InviteGet(w http.ResponseWriter, req *http.Request) ([]byte, error)
}

type authController struct {
	log             *slog.Logger
	presenter       presenters.AuthPresenter
	usersInteractor users.UsersInteractor
	jwtInteractor   jwt.JWTInteractor
	repo            auth.Repository
	orgInteractor   organizations.OrganizationsInteractor
}

func NewAuthController(
	log *slog.Logger,
	presenter presenters.AuthPresenter,
	usersInteractor users.UsersInteractor,
	jwtInteractor jwt.JWTInteractor,
	repo auth.Repository,
	orgInteractor organizations.OrganizationsInteractor,
) AuthController {
	return &authController{
		log:             log,
		presenter:       presenter,
		usersInteractor: usersInteractor,
		jwtInteractor:   jwtInteractor,
		repo:            repo,
		orgInteractor:   orgInteractor,
	}
}

func (c *authController) Join(w http.ResponseWriter, req *http.Request) ([]byte, error) {
	request, err := presenters.CreateRequest[domain.JoinRequest](req)
	if err != nil {
		return nil, fmt.Errorf("error create join request. %w", err)
	}

	c.log.Debug("join request", slog.String("mnemonic", request.Mnemonic))

	if !bip39.IsMnemonicValid(request.Mnemonic) {
		return nil, fmt.Errorf("error invalid mnemonic. %w", ErrorAuthInvalidMnemonic)
	}

	ctx, cancel := context.WithTimeout(req.Context(), 3*time.Second)
	defer cancel()

	user, err := c.usersInteractor.Create(ctx, users.CreateParams{
		Name:     request.Name,
		Email:    request.Credentals.Email,
		Phone:    request.Credentals.Phone,
		Tg:       request.Credentals.Telegram,
		Mnemonic: request.Mnemonic,
		Activate: true,
		Owner:    true,
		Admin:    true,
	})
	if err != nil {
		return nil, fmt.Errorf("error create new user. %w", err)
	}

	c.log.Debug("join request", slog.String("user id", user.ID.String()))

	return c.presenter.ResponseJoin(user)
}

// NIT: wrap with idempotent action handler
func (c *authController) Login(w http.ResponseWriter, req *http.Request) ([]byte, error) {
	request, err := presenters.CreateRequest[domain.LoginRequest](req)
	if err != nil {
		return nil, fmt.Errorf("error create login request. %w", err)
	}

	c.log.Debug("login request", slog.String("mnemonic", request.Mnemonic))

	ctx, cancel := context.WithTimeout(req.Context(), 3*time.Second)
	defer cancel()

	seed, err := hdwallet.NewSeedFromMnemonic(request.Mnemonic)
	if err != nil {
		return nil, fmt.Errorf("error create seed from mnemonic. %w", err)
	}

	users, err := c.usersInteractor.Get(ctx, users.GetParams{
		Seed: seed,
	})
	if err != nil {
		return nil, fmt.Errorf("error fetch user by seed. %w", err)
	}

	if len(users) == 0 {
		return nil, fmt.Errorf("error empty users set")
	}

	c.log.Debug("login request", slog.String("user id", users[0].ID.String()))

	return c.presenter.ResponseLogin(users[0])
}

func (c *authController) Refresh(w http.ResponseWriter, req *http.Request) ([]byte, error) {
	request, err := presenters.CreateRequest[domain.RefreshRequest](req)
	if err != nil {
		return nil, fmt.Errorf("error create refresh request. %w", err)
	}

	c.log.Debug(
		"refresh request",
		slog.String("token", request.Token),
		slog.String("refresh_token", request.RefreshToken),
	)

	ctx, cancel := context.WithTimeout(req.Context(), 3*time.Second)
	defer cancel()

	newTokens, err := c.jwtInteractor.RefreshToken(ctx, request.Token, request.RefreshToken)
	if err != nil {
		return nil, fmt.Errorf("error refresh access token. %w", err)
	}

	return c.presenter.ResponseRefresh(newTokens)
}

// const mnemonicEntropyBitSize int = 256

func (c *authController) Invite(w http.ResponseWriter, r *http.Request) ([]byte, error) {
	request, err := presenters.CreateRequest[domain.NewInviteLinkRequest](r)
	if err != nil {
		return nil, fmt.Errorf("error create refresh request. %w", err)
	}

	organizationID, err := ctxmeta.OrganizationId(r.Context())
	if err != nil {
		return nil, fmt.Errorf("error fetch organization id from context. %w", err)
	}

	user, err := ctxmeta.User(r.Context())
	if err != nil {
		return nil, fmt.Errorf("error fetch user from context. %w", err)
	}

	c.log.Debug(
		"invite request",
		slog.Int("exp at", request.ExpirationDate),
		slog.String("org id", organizationID.String()),
		slog.String("inviter id", user.Id().String()),
	)

	ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second)
	defer cancel()

	linkHash := sha256.New()

	linkHash.Write([]byte(
		user.Id().String() + organizationID.String() + time.Now().String(),
	))

	linkHashString := strings.ReplaceAll(
		strings.ReplaceAll(
			strings.ReplaceAll(
				base64.StdEncoding.EncodeToString(linkHash.Sum(nil)),
				"/", "$",
			),
			"?", "@",
		),
		"&", "#",
	)

	createdAt := time.Now()
	expDate := createdAt.Add(time.Hour * 24 * 7)

	if request.ExpirationDate > 0 {
		expDate = time.UnixMilli(int64(request.ExpirationDate))
	}

	c.log.Debug(
		"",
		slog.String("link", linkHashString),
	)

	if err := c.repo.AddInvite(ctx, auth.AddInviteParams{
		LinkHash:       linkHashString,
		OrganizationID: organizationID,
		CreatedBy:      *user,
		CreatedAt:      createdAt,
		ExpiredAt:      expDate,
	}); err != nil {
		return nil, fmt.Errorf("error add new invite link. %w", err)
	}

	return c.presenter.ResponseNewInvite(ctx, organizationID, linkHashString)
}

func (c *authController) JoinWithInvite(w http.ResponseWriter, r *http.Request) ([]byte, error) {
	c.log.Debug("join with link request")

	request, err := presenters.CreateRequest[domain.JoinRequest](r)
	if err != nil {
		return nil, fmt.Errorf("error create join request. %w", err)
	}

	c.log.Debug("join with invite request", slog.Any("request", request))

	if !bip39.IsMnemonicValid(request.Mnemonic) {
		return nil, fmt.Errorf("error invalid mnemonic. %w", ErrorAuthInvalidMnemonic)
	}

	hash := chi.URLParam(r, "hash")

	if hash == "" {
		return nil, fmt.Errorf("error fetch invite hash from request")
	}

	usedAt := time.Now()

	ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
	defer cancel()

	organizationID, err := c.repo.MarkAsUsedLink(ctx, hash, usedAt)
	if err != nil {
		return nil, fmt.Errorf("error mark invite link as used. %w", err)
	}

	user, err := c.usersInteractor.Create(ctx, users.CreateParams{
		Name:     request.Name,
		Email:    request.Credentals.Email,
		Phone:    request.Credentals.Phone,
		Tg:       request.Credentals.Telegram,
		Mnemonic: request.Mnemonic,
		Activate: true,
	})
	if err != nil {
		return nil, fmt.Errorf("error create new user with invire link. %w", err)
	}

	if err = c.orgInteractor.AddUser(ctx, organizations.AddUserParams{
		User:           user,
		OrganizationID: organizationID,
		SkipRights:     true,
	}); err != nil {
		c.log.Error(
			"error add user into organization",
			slog.String("organization id", organizationID.String()),
			slog.String("user id", user.Id().String()),
			slog.String("invire hash", hash),
		)

		return nil, fmt.Errorf("error add user into organization. %w", err)
	}

	return c.presenter.ResponseJoin(user)
}

func (c *authController) InviteGet(w http.ResponseWriter, r *http.Request) ([]byte, error) {
	return presenters.ResponseOK()
}
