package repositories

import (
	"context"
	"crypto/md5"
	"encoding/json"
	"fmt"

	"github.com/jackc/pgx/v5"
	"github.com/jackc/pgx/v5/pgtype"

	"history-api/internal/gen/sqlc"
	"history-api/internal/models"
	"history-api/pkg/cache"
	"history-api/pkg/constants"
	"history-api/pkg/convert"
)

type RoleRepository interface {
	GetByID(ctx context.Context, id pgtype.UUID) (*models.RoleEntity, error)
	GetByIDs(ctx context.Context, ids []string) ([]*models.RoleEntity, error)
	GetByName(ctx context.Context, name string) (*models.RoleEntity, error)
	All(ctx context.Context) ([]*models.RoleEntity, error)
	Create(ctx context.Context, name string) (*models.RoleEntity, error)
	Update(ctx context.Context, params sqlc.UpdateRoleParams) (*models.RoleEntity, error)
	Delete(ctx context.Context, id pgtype.UUID) error
	Restore(ctx context.Context, id pgtype.UUID) error
	CreateUserRole(ctx context.Context, params sqlc.CreateUserRoleParams) error
	DeleteUserRole(ctx context.Context, params sqlc.DeleteUserRoleParams) error
	BulkDeleteRolesFromUser(ctx context.Context, userId pgtype.UUID) error
	BulkDeleteUsersFromRole(ctx context.Context, roleId pgtype.UUID) error
	WithTx(tx pgx.Tx) RoleRepository
}

type roleRepository struct {
	q *sqlc.Queries
	c cache.Cache
}

func NewRoleRepository(db sqlc.DBTX, c cache.Cache) RoleRepository {
	return &roleRepository{
		q: sqlc.New(db),
		c: c,
	}
}

func (r *roleRepository) WithTx(tx pgx.Tx) RoleRepository {
	return &roleRepository{
		q: r.q.WithTx(tx),
		c: r.c,
	}
}

func (r *roleRepository) generateQueryKey(prefix string, params any) string {
	b, _ := json.Marshal(params)
	hash := fmt.Sprintf("%x", md5.Sum(b))
	return fmt.Sprintf("%s:%s", prefix, hash)
}

func (r *roleRepository) getByIDsWithFallback(ctx context.Context, ids []string) ([]*models.RoleEntity, error) {
	if len(ids) == 0 {
		return []*models.RoleEntity{}, nil
	}
	keys := make([]string, len(ids))
	for i, id := range ids {
		keys[i] = fmt.Sprintf("role:id:%s", id)
	}
	raws := r.c.MGet(ctx, keys...)

	var roles []*models.RoleEntity
	missingRolesToCache := make(map[string]any)

	var missingPgIds []pgtype.UUID
	for i, b := range raws {
		if len(b) == 0 {
			pgId := pgtype.UUID{}
			err := pgId.Scan(ids[i])
			if err == nil {
				missingPgIds = append(missingPgIds, pgId)
			}
		}
	}

	dbMap := make(map[string]*models.RoleEntity)
	if len(missingPgIds) > 0 {
		dbRows, err := r.q.GetRolesByIDs(ctx, missingPgIds)
		if err == nil {
			for _, row := range dbRows {
				item := models.RoleEntity{
					ID:        convert.UUIDToString(row.ID),
					Name:      row.Name,
					IsDeleted: row.IsDeleted,
					CreatedAt: convert.TimeToPtr(row.CreatedAt),
					UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
				}
				dbMap[item.ID] = &item
			}
		}
	}

	for i, b := range raws {
		if len(b) > 0 {
			var u models.RoleEntity
			if err := json.Unmarshal(b, &u); err == nil {
				roles = append(roles, &u)
			}
		} else {
			if item, ok := dbMap[ids[i]]; ok {
				roles = append(roles, item)
				missingRolesToCache[keys[i]] = item
			}
		}
	}

	if len(missingRolesToCache) > 0 {
		_ = r.c.MSet(ctx, missingRolesToCache, constants.NormalCacheDuration)
	}

	return roles, nil
}

func (r *roleRepository) GetByIDs(ctx context.Context, ids []string) ([]*models.RoleEntity, error) {
	return r.getByIDsWithFallback(ctx, ids)
}

func (r *roleRepository) GetByID(ctx context.Context, id pgtype.UUID) (*models.RoleEntity, error) {
	cacheId := fmt.Sprintf("role:id:%s", convert.UUIDToString(id))
	var role models.RoleEntity
	err := r.c.Get(ctx, cacheId, &role)
	if err == nil {
		_ = r.c.Set(ctx, cacheId, role, constants.NormalCacheDuration)
		return &role, nil
	}

	row, err := r.q.GetRoleByID(ctx, id)
	if err != nil {
		return nil, err
	}

	role = models.RoleEntity{
		ID:        convert.UUIDToString(row.ID),
		Name:      row.Name,
		IsDeleted: row.IsDeleted,
		CreatedAt: convert.TimeToPtr(row.CreatedAt),
		UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
	}
	_ = r.c.Set(ctx, cacheId, role, constants.NormalCacheDuration)

	return &role, nil
}

func (r *roleRepository) GetByName(ctx context.Context, name string) (*models.RoleEntity, error) {
	cacheId := fmt.Sprintf("role:name:%s", name)
	var role models.RoleEntity
	err := r.c.Get(ctx, cacheId, &role)
	if err == nil {
		_ = r.c.Set(ctx, cacheId, role, constants.NormalCacheDuration)
		return &role, nil
	}
	row, err := r.q.GetRoleByName(ctx, name)
	if err != nil {
		return nil, err
	}
	role = models.RoleEntity{
		ID:        convert.UUIDToString(row.ID),
		Name:      row.Name,
		IsDeleted: row.IsDeleted,
		CreatedAt: convert.TimeToPtr(row.CreatedAt),
		UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
	}

	_ = r.c.Set(ctx, cacheId, role, constants.NormalCacheDuration)

	return &role, nil
}

func (r *roleRepository) Create(ctx context.Context, name string) (*models.RoleEntity, error) {
	row, err := r.q.CreateRole(ctx, name)
	if err != nil {
		return nil, err
	}

	go func() {
		bgCtx := context.Background()
		_ = r.c.DelByPattern(bgCtx, "role:all*")
	}()

	role := models.RoleEntity{
		ID:        convert.UUIDToString(row.ID),
		Name:      row.Name,
		IsDeleted: row.IsDeleted,
		CreatedAt: convert.TimeToPtr(row.CreatedAt),
		UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
	}
	return &role, nil
}

func (r *roleRepository) Update(ctx context.Context, params sqlc.UpdateRoleParams) (*models.RoleEntity, error) {
	row, err := r.q.UpdateRole(ctx, params)
	if err != nil {
		return nil, err
	}
	role := models.RoleEntity{
		ID:        convert.UUIDToString(row.ID),
		Name:      row.Name,
		IsDeleted: row.IsDeleted,
		CreatedAt: convert.TimeToPtr(row.CreatedAt),
		UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
	}

	_ = r.c.Del(ctx, fmt.Sprintf("role:id:%s", convert.UUIDToString(row.ID)), fmt.Sprintf("role:name:%s", row.Name))
	return &role, nil
}

func (r *roleRepository) All(ctx context.Context) ([]*models.RoleEntity, error) {
	queryKey := "role:all"
	var cachedIDs []string
	if err := r.c.Get(ctx, queryKey, &cachedIDs); err == nil && len(cachedIDs) > 0 {
		listItem, err := r.getByIDsWithFallback(ctx, cachedIDs)
		if err != nil {
			return nil, err
		}
		newCachedIDs := make([]string, len(listItem))
		for i, media := range listItem {
			newCachedIDs[i] = media.ID
		}
		_ = r.c.Set(ctx, queryKey, newCachedIDs, constants.ListCacheDuration)
		return listItem, err
	}

	rows, err := r.q.GetRoles(ctx)
	if err != nil {
		return nil, err
	}
	var roles []*models.RoleEntity
	var ids []string
	roleToCache := make(map[string]any)

	for _, row := range rows {
		role := &models.RoleEntity{
			ID:        convert.UUIDToString(row.ID),
			Name:      row.Name,
			IsDeleted: row.IsDeleted,
			CreatedAt: convert.TimeToPtr(row.CreatedAt),
			UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
		}
		ids = append(ids, role.ID)
		roles = append(roles, role)

		roleToCache[fmt.Sprintf("role:id:%s", role.ID)] = role
	}

	if len(roleToCache) > 0 {
		_ = r.c.MSet(ctx, roleToCache, constants.NormalCacheDuration)
	}

	if len(ids) > 0 {
		_ = r.c.Set(ctx, queryKey, ids, constants.ListCacheDuration)
	}

	return roles, nil
}

func (r *roleRepository) Delete(ctx context.Context, id pgtype.UUID) error {
	role, err := r.GetByID(ctx, id)
	if err != nil {
		return err
	}

	err = r.q.DeleteRole(ctx, id)
	if err != nil {
		return err
	}
	_ = r.c.Del(ctx, fmt.Sprintf("role:id:%s", role.ID), fmt.Sprintf("role:name:%s", role.Name))
	return nil
}

func (r *roleRepository) Restore(ctx context.Context, id pgtype.UUID) error {
	err := r.q.RestoreRole(ctx, id)
	if err != nil {
		return err
	}
	_ = r.c.Del(ctx, fmt.Sprintf("role:id:%s", convert.UUIDToString(id)))
	return nil
}

func (r *roleRepository) CreateUserRole(ctx context.Context, params sqlc.CreateUserRoleParams) error {
	err := r.q.CreateUserRole(ctx, params)
	return err
}

func (r *roleRepository) DeleteUserRole(ctx context.Context, params sqlc.DeleteUserRoleParams) error {
	err := r.q.DeleteUserRole(ctx, params)
	return err
}

func (r *roleRepository) BulkDeleteUsersFromRole(ctx context.Context, roleId pgtype.UUID) error {
	err := r.q.BulkDeleteUsersFromRole(ctx, roleId)
	return err
}

func (r *roleRepository) BulkDeleteRolesFromUser(ctx context.Context, roleId pgtype.UUID) error {
	err := r.q.BulkDeleteRolesFromUser(ctx, roleId)
	return err
}
