package users_repo

import (
	"context"
	"errors"
	"evening_detective_server/internal/repos"
	"evening_detective_server/internal/repos/user_agreements_repo"
	"fmt"

	"github.com/jackc/pgx/v5"
	"github.com/jackc/pgx/v5/pgconn"
	"github.com/jackc/pgx/v5/pgxpool"
)

var (
	ErrUserNotFound = errors.New("Пользователь не найден")
)

type UsersRepo struct {
	pool *pgxpool.Pool
}

func NewUserRepo(pool *pgxpool.Pool) *UsersRepo {
	return &UsersRepo{
		pool: pool,
	}
}

// AddUserWithAgreements создаёт пользователя и фиксирует акцепты документов
// (user_agreements) в одной транзакции: пользователь либо существует вместе с
// записями о согласиях, либо не существует вовсе (доказуемое согласие,
// ст. 9 152-ФЗ). Возвращает id созданного пользователя.
func (s *UsersRepo) AddUserWithAgreements(
	ctx context.Context,
	username string,
	email string,
	passwordHash string,
	roles []string,
	agreements []user_agreements_repo.Agreement,
) (int, error) {
	// Инвариант «пользователь не существует без записей о согласиях»
	// держится на том, что акцепты передаются всегда.
	if len(agreements) == 0 {
		return 0, errors.New("Не переданы записи об акцепте соглашений")
	}

	tx, err := s.pool.Begin(ctx)
	if err != nil {
		return 0, err
	}
	defer tx.Rollback(ctx) //nolint:errcheck

	var id int
	err = tx.QueryRow(
		ctx,
		`INSERT INTO users (username, email, password_hash, roles)
		SELECT $1, $2, $3, $4
		WHERE NOT EXISTS (
			SELECT 1 FROM users 
			WHERE username = $5 OR email = $6
		)
		RETURNING id`,
		username,
		email,
		passwordHash,
		roles,

		username,
		email,
	).Scan(&id)
	if err != nil {
		// Ничего не вставлено — занят username или email.
		if errors.Is(err, pgx.ErrNoRows) {
			return 0, errors.New("Username или email уже используется")
		}
		// Гонка: между проверкой NOT EXISTS и INSERT другой запрос успел
		// создать пользователя с тем же username/email — нарушение
		// уникальности. Возвращаем тот же понятный клиенту ответ.
		var pgErr *pgconn.PgError
		if errors.As(err, &pgErr) && pgErr.Code == "23505" { // unique_violation
			return 0, errors.New("Username или email уже используется")
		}
		return 0, err
	}

	for _, agreement := range agreements {
		if _, err = tx.Exec(
			ctx,
			`INSERT INTO user_agreements (user_id, agreement_type, version, ip, user_agent)
			VALUES ($1, $2, $3, $4, $5)`,
			id,
			agreement.AgreementType,
			agreement.Version,
			agreement.IP,
			agreement.UserAgent,
		); err != nil {
			return 0, err
		}
	}

	if err = tx.Commit(ctx); err != nil {
		return 0, err
	}
	return id, nil
}

func (s *UsersRepo) UpdateUserPassword(
	ctx context.Context,
	email string,
	passwordHash string,
) error {
	tag, err := s.pool.Exec(
		ctx,
		`UPDATE users
		SET password_hash = $1
		WHERE email = $2`,
		passwordHash,
		email,
	)
	if err != nil {
		return err
	}
	if tag.RowsAffected() == 0 {
		return ErrUserNotFound
	}

	return nil
}

func (s *UsersRepo) GetUserByEmail(
	ctx context.Context,
	email string,
) (*repos.User, error) {
	user := &repos.User{}
	row := s.pool.QueryRow(
		ctx,
		`SELECT id, username, email, password_hash, roles, is_active
		FROM users
		WHERE email = $1`,
		email,
	)
	err := row.Scan(
		&user.ID,
		&user.Username,
		&user.Email,
		&user.PasswordHash,
		&user.Roles,
		&user.IsActive,
	)
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return nil, ErrUserNotFound
		}
		return nil, err
	}

	return user, nil
}

func (s *UsersRepo) GetUserByID(
	ctx context.Context,
	id int,
) (*repos.User, error) {
	user := &repos.User{}
	row := s.pool.QueryRow(
		ctx,
		`SELECT id, username, email, password_hash, roles, is_active
		FROM users
		WHERE id = $1`,
		id,
	)
	err := row.Scan(
		&user.ID,
		&user.Username,
		&user.Email,
		&user.PasswordHash,
		&user.Roles,
		&user.IsActive,
	)
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return nil, ErrUserNotFound
		}
		return nil, err
	}

	return user, nil
}

func (s *UsersRepo) GetUsers(
	ctx context.Context,
) ([]*repos.User, error) {
	rows, err := s.pool.Query(
		ctx,
		`SELECT id, username, email, roles, is_active, created_at
		FROM users
		ORDER BY created_at DESC`,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var users []*repos.User
	for rows.Next() {
		user := &repos.User{}
		err := rows.Scan(
			&user.ID,
			&user.Username,
			&user.Email,
			&user.Roles,
			&user.IsActive,
			&user.CreatedAt,
		)
		if err != nil {
			return nil, err
		}
		users = append(users, user)
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}

	return users, nil
}

func (s *UsersRepo) AddUserRole(
	ctx context.Context,
	userId int,
	role string,
) error {
	_, err := s.pool.Exec(
		ctx,
		`UPDATE users 
		SET roles = roles || $1::jsonb
		WHERE id = $2 AND NOT roles @> $1::jsonb`,
		fmt.Sprintf(`["%s"]`, role),
		userId,
	)
	return err
}

func (s *UsersRepo) DeleteUserRole(
	ctx context.Context,
	userId int,
	role string,
) error {
	_, err := s.pool.Exec(
		ctx,
		`UPDATE users 
		SET roles = roles - $1::text
		WHERE id = $2`,
		role,
		userId,
	)
	return err
}

// DeleteUser удаляет учётную запись и персональные данные пользователя
// (ст. 21 152-ФЗ — уничтожение ПДн при удалении аккаунта). Выполняется в одной
// транзакции. Публичный контент пользователя (сценарии, команды) сохраняется:
// FK-ограничения с ON DELETE SET NULL (миграция
// 20260818232725_keep_content_on_user_delete.sql) обнуляют привязку к автору —
// контент остаётся в обезличенном виде.
func (s *UsersRepo) DeleteUser(
	ctx context.Context,
	userId int,
) error {
	tx, err := s.pool.Begin(ctx)
	if err != nil {
		return err
	}
	defer tx.Rollback(ctx) //nolint:errcheck

	// Refresh-токены (FK CASCADE, удаляем явно для предсказуемости).
	if _, err = tx.Exec(
		ctx,
		`DELETE FROM refresh_tokens WHERE user_id = $1`,
		userId,
	); err != nil {
		return err
	}

	// Зафиксированные акцепты соглашений НЕ удаляются: ст. 9 ч. 4 152-ФЗ
	// возлагает на оператора бремя доказывания получения согласия, поэтому
	// факт акцепта (документ, версия, время) сохраняется в обезличенном виде:
	// ссылка на пользователя и персональные данные внутри записи (ip,
	// user_agent) обнуляются. FK ON DELETE SET NULL (миграция
	// 20260818231943_create_user_agreements_table.sql) гарантирует, что при
	// удалении users строки акцептов остаются с user_id = NULL.
	if _, err = tx.Exec(
		ctx,
		`UPDATE user_agreements
		SET user_id = NULL, ip = NULL, user_agent = NULL
		WHERE user_id = $1`,
		userId,
	); err != nil {
		return err
	}

	// Пользователь. Связанные сценарии (author_id) и команды (creator_id)
	// обнуляются автоматически через ON DELETE SET NULL.
	tag, err := tx.Exec(
		ctx,
		`DELETE FROM users WHERE id = $1`,
		userId,
	)
	if err != nil {
		return err
	}
	if tag.RowsAffected() == 0 {
		return ErrUserNotFound
	}

	return tx.Commit(ctx)
}
