package users_service

import (
	"context"
	"errors"
	"evening_detective_server/internal/modules/email_sender"
	"evening_detective_server/internal/modules/password_generator"
	"evening_detective_server/internal/modules/processor_jwt"
	"evening_detective_server/internal/modules/roles"
	"evening_detective_server/internal/repos"
	"evening_detective_server/internal/repos/refresh_tokens_repo"
	"evening_detective_server/internal/repos/user_agreements_repo"
	"evening_detective_server/internal/repos/users_repo"
	"fmt"
	"log"
	"net/mail"
	"slices"
	"strings"
	"time"

	"golang.org/x/crypto/bcrypt"
)

// Версии документов, акцепт которых фиксируется. Должны совпадать с
// редакциями, опубликованными по постоянным URL (/api/terms и /api/privacy,
// rpc GetTerms/GetPrivacy, текст встраивается из пакета docs): при изменении
// текста документа версия ОБЯЗАНА увеличиваться, иначе повторный акцепт
// не будет зафиксирован как согласие с новой редакцией
// (UNIQUE (user_id, agreement_type, version)).
const (
	termsVersion   = "1.0"
	privacyVersion = "1.0"
)

// usersRepository — граница доступа к данным пользователей, реализуемая
// *users_repo.UsersRepo; интерфейс позволяет тестировать сервис без БД.
type usersRepository interface {
	AddUserWithAgreements(
		ctx context.Context,
		username string,
		email string,
		passwordHash string,
		roles []string,
		agreements []user_agreements_repo.Agreement,
	) (int, error)
	UpdateUserPassword(ctx context.Context, email string, passwordHash string) error
	GetUserByEmail(ctx context.Context, email string) (*repos.User, error)
	GetUserByID(ctx context.Context, id int) (*repos.User, error)
	GetUsers(ctx context.Context) ([]*repos.User, error)
	AddUserRole(ctx context.Context, userId int, role string) error
	DeleteUserRole(ctx context.Context, userId int, role string) error
	DeleteUser(ctx context.Context, userId int) error
}

type UsersService struct {
	usersRepo         usersRepository
	passwordGenerator password_generator.IPasswordGenerator
	emailSender       email_sender.IEmailSender
	processorJWT      processor_jwt.IProcessorJWT
	refreshTokensRepo *refresh_tokens_repo.RefreshTokensRepo
	// appBaseURL — публичный адрес фронтенда для ссылки «Войти в игру» в
	// письмах; пустое значение — ссылка не добавляется.
	appBaseURL string
}

func NewUsersService(
	usersRepo usersRepository,
	passwordGenerator password_generator.IPasswordGenerator,
	emailSender email_sender.IEmailSender,
	processorJWT processor_jwt.IProcessorJWT,
	refreshTokensRepo *refresh_tokens_repo.RefreshTokensRepo,
	appBaseURL string,
) *UsersService {
	return &UsersService{
		usersRepo:         usersRepo,
		passwordGenerator: passwordGenerator,
		emailSender:       emailSender,
		processorJWT:      processorJWT,
		refreshTokensRepo: refreshTokensRepo,
		appBaseURL:        loginURL(appBaseURL),
	}
}

// ErrTermsNotAccepted — регистрация невозможна без акцепта соглашений.
var ErrTermsNotAccepted = errors.New("Необходимо принять Пользовательское соглашение и Политику конфиденциальности")

func (s *UsersService) AddUser(
	ctx context.Context,
	username string,
	email string,
	acceptTerms bool,
	acceptPrivacy bool,
	ip string,
	userAgent string,
) error {
	if !acceptTerms || !acceptPrivacy {
		return ErrTermsNotAccepted
	}

	email = normalizeEmail(email)
	if err := validateEmail(email); err != nil {
		return err
	}

	password, err := s.passwordGenerator.Generate()
	if err != nil {
		return err
	}
	hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
	if err != nil {
		return err
	}
	// Создание пользователя и фиксация акцептов — в одной транзакции:
	// без записей о согласиях пользователь не существует.
	id, err := s.usersRepo.AddUserWithAgreements(
		ctx,
		username,
		email,
		string(hashedPassword),
		[]string{"user"},
		[]user_agreements_repo.Agreement{
			{
				AgreementType: user_agreements_repo.AgreementTypeTerms,
				Version:       termsVersion,
				IP:            ip,
				UserAgent:     userAgent,
			},
			{
				AgreementType: user_agreements_repo.AgreementTypePrivacy,
				Version:       privacyVersion,
				IP:            ip,
				UserAgent:     userAgent,
			},
		},
	)
	if err != nil {
		return err
	}

	// Консистентность ответа и состояния БД: если письмо с паролем не
	// ушло, пользователь не должен оставаться в системе «полусозданным»
	// (клиент получит ошибку и повторит регистрацию). Компенсация —
	// удаление созданного пользователя. Выполняется вне отменённого
	// контекста: письмо могло упасть именно из-за истечения таймаута ctx,
	// и тогда тот же ctx не дал бы выполнить удаление.
	err = s.emailSender.Send(ctx, email_sender.Message{
		To:      email,
		Subject: welcomeSubject,
		Body:    welcomePlain(username, email, password, s.appBaseURL),
		HTML:    welcomeHTML(username, email, password, s.appBaseURL),
	})
	if err != nil {
		if delErr := s.usersRepo.DeleteUser(context.WithoutCancel(ctx), id); delErr != nil {
			log.Printf("email: отправка не удалась (%v) и компенсация не выполнена: %v", err, delErr)
		}
		return fmt.Errorf("email send: %w", err)
	}

	return nil
}

// DeleteAccount удаляет учётную запись и все персональные данные пользователя
// (ст. 21 152-ФЗ). Требует подтверждения паролем: обладатель украденного
// access-токена не должен мочь удалить чужой аккаунт.
func (s *UsersService) DeleteAccount(
	ctx context.Context,
	userId int,
	password string,
) error {
	user, err := s.usersRepo.GetUserByID(ctx, userId)
	if err != nil {
		return err
	}
	if err = bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password)); err != nil {
		return errors.New("Неверный пароль")
	}
	return s.usersRepo.DeleteUser(ctx, userId)
}

func (s *UsersService) RefreshPassword(
	ctx context.Context,
	email string,
) error {
	email = normalizeEmail(email)
	if err := validateEmail(email); err != nil {
		return err
	}

	// Анти-enumeration: ответ одинаков для существующего и несуществующего
	// адреса, письмо отправляется только реальному пользователю.
	user, err := s.usersRepo.GetUserByEmail(ctx, email)
	if err != nil {
		if errors.Is(err, users_repo.ErrUserNotFound) {
			return nil
		}
		return err
	}

	password, err := s.passwordGenerator.Generate()
	if err != nil {
		return err
	}
	hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
	if err != nil {
		return err
	}

	// Письмо отправляется ДО смены пароля: при сбое отправки пароль
	// остаётся прежним и аккаунт не блокируется (иначе старый пароль уже
	// не работает, а новый пользователь так и не узнал).
	err = s.emailSender.Send(ctx, email_sender.Message{
		To:      user.Email,
		Subject: resetSubject,
		Body:    resetPlain(user.Username, user.Email, password, s.appBaseURL),
		HTML:    resetHTML(user.Username, user.Email, password, s.appBaseURL),
	})
	if err != nil {
		return fmt.Errorf("email send: %w", err)
	}

	return s.usersRepo.UpdateUserPassword(ctx, user.Email, string(hashedPassword))
}

// normalizeEmail приводит адрес к каноническому виду (без пробелов, нижний
// регистр): "User@Example.COM " и "user@example.com" — один аккаунт.
func normalizeEmail(email string) string {
	return strings.ToLower(strings.TrimSpace(email))
}

// validateEmail проверяет, что строка является одиночным email-адресом.
func validateEmail(email string) error {
	if email == "" {
		return errors.New("Email не указан")
	}
	addr, err := mail.ParseAddress(email)
	if err != nil {
		return errors.New("Некорректный email")
	}
	// Отклоняем форму "Имя <a@b.c>" и всё, что не является чистым адресом.
	if !strings.EqualFold(addr.Address, email) {
		return errors.New("Некорректный email")
	}
	return nil
}

func (s *UsersService) Login(
	ctx context.Context,
	email string,
	password string,
) (string, string, error) {
	// Email нормализуется так же, как при регистрации: иначе пользователь,
	// зарегистрировавшийся как user@example.com, не войдёт с User@Example.COM.
	user, err := s.usersRepo.GetUserByEmail(
		ctx,
		normalizeEmail(email),
	)
	if err != nil {
		return "", "", err
	}

	if err = bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password)); err != nil {
		return "", "", errors.New("Не верен email или пароль")
	}

	accessToken, err := s.processorJWT.GenerateAccessToken(user.ID, user.Email, user.Roles)
	if err != nil {
		return "", "", errors.New("Не получилось получить доступ")
	}

	refreshToken, err := s.processorJWT.GenerateRefreshToken(user.ID)
	if err != nil {
		return "", "", errors.New("Не получилось получить доступ")
	}
	refreshTokenClaims, err := s.processorJWT.GetClaims(refreshToken)
	if err != nil {
		return "", "", errors.New("Не получилось получить доступ")
	}

	if err := s.refreshTokensRepo.AddRefreshToken(ctx, user.ID, refreshTokenClaims.ExpiresAt.Time, refreshTokenClaims.ID); err != nil {
		return "", "", err
	}

	return accessToken, refreshToken, nil
}

func (s *UsersService) Refresh(
	ctx context.Context,
	token string,
) (string, string, error) {
	claims, err := s.processorJWT.GetClaims(token)
	if err != nil {
		return "", "", errors.New("Пользователь не найден")
	}

	now := time.Now()
	ok, err := s.refreshTokensRepo.ExistsRefreshToken(ctx, claims.UserID, claims.ID, now)
	if err != nil {
		return "", "", err
	}
	if !ok {
		return "", "", errors.New("Пользователь не найден")
	}
	if err := s.refreshTokensRepo.DeleteRefreshTokenByJti(ctx, claims.ID); err != nil {
		return "", "", err
	}

	user, err := s.usersRepo.GetUserByID(ctx, claims.UserID)
	if err != nil {
		return "", "", err
	}

	accessToken, err := s.processorJWT.GenerateAccessToken(user.ID, user.Email, user.Roles)
	if err != nil {
		return "", "", errors.New("Не получилось получить доступ")
	}

	refreshToken, err := s.processorJWT.GenerateRefreshToken(user.ID)
	if err != nil {
		return "", "", errors.New("Не получилось получить доступ")
	}
	refreshTokenClaims, err := s.processorJWT.GetClaims(refreshToken)
	if err != nil {
		return "", "", errors.New("Не получилось получить доступ")
	}

	if err := s.refreshTokensRepo.AddRefreshToken(ctx, user.ID, refreshTokenClaims.ExpiresAt.Time, refreshTokenClaims.ID); err != nil {
		return "", "", err
	}

	return accessToken, refreshToken, nil
}

func (s *UsersService) GetUsers(
	ctx context.Context,
) ([]*repos.User, error) {
	users, err := s.usersRepo.GetUsers(ctx)
	if err != nil {
		return nil, err
	}
	return users, nil
}

func (s *UsersService) GetUserByID(
	ctx context.Context,
	userId int,
) (*repos.User, error) {
	user, err := s.usersRepo.GetUserByID(ctx, userId)
	if err != nil {
		return nil, err
	}
	return user, nil
}

func (s *UsersService) AddUserRole(
	ctx context.Context,
	userId int,
	role string,
) error {
	if !slices.Contains(roles.AllRoles, role) {
		return fmt.Errorf("role %s not support", role)
	}
	return s.usersRepo.AddUserRole(ctx, userId, role)
}

func (s *UsersService) DeleteUserRole(
	ctx context.Context,
	userId int,
	role string,
) error {
	if !slices.Contains(roles.AllRoles, role) {
		return fmt.Errorf("role %s not support", role)
	}
	return s.usersRepo.DeleteUserRole(ctx, userId, role)
}
