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 ProjectRepository interface {
	GetByID(ctx context.Context, id pgtype.UUID) (*models.ProjectEntity, error)
	GetByIDs(ctx context.Context, ids []string) ([]*models.ProjectEntity, error)
	GetByUserID(ctx context.Context, params sqlc.GetProjectsByUserIdParams) ([]*models.ProjectEntity, error)
	Search(ctx context.Context, params sqlc.SearchProjectsParams) ([]*models.ProjectEntity, error)
	Count(ctx context.Context, params sqlc.CountProjectsParams) (int64, error)
	Create(ctx context.Context, params sqlc.CreateProjectParams) (*models.ProjectEntity, error)
	Update(ctx context.Context, params sqlc.UpdateProjectParams) (*models.ProjectEntity, error)
	Delete(ctx context.Context, id pgtype.UUID) error
	AddMember(ctx context.Context, params sqlc.AddProjectMemberParams) error
	UpdateMemberRole(ctx context.Context, params sqlc.UpdateProjectMemberRoleParams) error
	RemoveMember(ctx context.Context, params sqlc.RemoveProjectMemberParams) error
	CheckPermission(ctx context.Context, params sqlc.CheckProjectPermissionParams) (int16, error)
	ChangeOwner(ctx context.Context, params sqlc.ChangeProjectOwnerParams) error
	UpdateLatestCommit(ctx context.Context, params sqlc.UpdateLatestCommitParams) error
	WithTx(tx pgx.Tx) ProjectRepository
}

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

func NewProjectRepository(db sqlc.DBTX, c cache.Cache) ProjectRepository {
	return &projectRepository{
		q: sqlc.New(db),
		c: c,
	}
}

func (r *projectRepository) WithTx(tx pgx.Tx) ProjectRepository {
	return &projectRepository{
		q: r.q.WithTx(tx),
		c: r.c,
	}
}
func (r *projectRepository) 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 *projectRepository) getByIDsWithFallback(ctx context.Context, ids []string) ([]*models.ProjectEntity, error) {
	if len(ids) == 0 {
		return []*models.ProjectEntity{}, nil
	}
	keys := make([]string, len(ids))
	for i, id := range ids {
		keys[i] = fmt.Sprintf("project:id:%s", id)
	}
	raws := r.c.MGet(ctx, keys...)

	var projects []*models.ProjectEntity
	missingToCache := 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.ProjectEntity)
	if len(missingPgIds) > 0 {
		dbRows, err := r.q.GetProjectsByIDs(ctx, missingPgIds)
		if err == nil {
			for _, row := range dbRows {
				item := models.ProjectEntity{
					ID:             convert.UUIDToString(row.ID),
					Title:          row.Title,
					Description:    convert.TextToString(row.Description),
					LatestCommitID: convert.UUIDToStringPtr(row.LatestCommitID),
					ProjectStatus:  constants.ParseProjectStatusType(row.ProjectStatus),
					LockedBy:       convert.UUIDToStringPtr(row.LockedBy),
					IsDeleted:      row.IsDeleted,
					UserID:         convert.UUIDToString(row.UserID),
					CreatedAt:      convert.TimeToPtr(row.CreatedAt),
					UpdatedAt:      convert.TimeToPtr(row.UpdatedAt),
				}
				_ = item.ParseUser(row.User)
				_ = item.ParseCommits(row.Commits)
				_ = item.ParseSubmissions(row.Submissions)
				_ = item.ParseMembers(row.Members)
				dbMap[item.ID] = &item
			}
		}
	}

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

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

	return projects, nil
}

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

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

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

	project = models.ProjectEntity{
		ID:             convert.UUIDToString(row.ID),
		Title:          row.Title,
		Description:    convert.TextToString(row.Description),
		LatestCommitID: convert.UUIDToStringPtr(row.LatestCommitID),
		ProjectStatus:  constants.ParseProjectStatusType(row.ProjectStatus),
		LockedBy:       convert.UUIDToStringPtr(row.LockedBy),
		IsDeleted:      row.IsDeleted,
		UserID:         convert.UUIDToString(row.UserID),
		CreatedAt:      convert.TimeToPtr(row.CreatedAt),
		UpdatedAt:      convert.TimeToPtr(row.UpdatedAt),
	}
	_ = project.ParseUser(row.User)
	_ = project.ParseCommits(row.Commits)
	_ = project.ParseSubmissions(row.Submissions)
	_ = project.ParseMembers(row.Members)

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

	return &project, nil
}

func (r *projectRepository) GetByUserID(ctx context.Context, params sqlc.GetProjectsByUserIdParams) ([]*models.ProjectEntity, error) {
	queryKey := r.generateQueryKey("project:user", params)
	var cachedIDs []string
	if err := r.c.Get(ctx, queryKey, &cachedIDs); err == nil && len(cachedIDs) > 0 {
		return r.getByIDsWithFallback(ctx, cachedIDs)
	}

	rows, err := r.q.GetProjectsByUserId(ctx, params)
	if err != nil {
		return nil, err
	}

	var projects []*models.ProjectEntity
	var ids []string
	projectToCache := make(map[string]any)

	for _, row := range rows {
		project := &models.ProjectEntity{
			ID:             convert.UUIDToString(row.ID),
			Title:          row.Title,
			Description:    convert.TextToString(row.Description),
			LatestCommitID: convert.UUIDToStringPtr(row.LatestCommitID),
			ProjectStatus:  constants.ParseProjectStatusType(row.ProjectStatus),
			LockedBy:       convert.UUIDToStringPtr(row.LockedBy),
			IsDeleted:      row.IsDeleted,
			UserID:         convert.UUIDToString(row.UserID),
			CreatedAt:      convert.TimeToPtr(row.CreatedAt),
			UpdatedAt:      convert.TimeToPtr(row.UpdatedAt),
		}
		_ = project.ParseUser(row.User)
		_ = project.ParseCommits(row.Commits)
		_ = project.ParseSubmissions(row.Submissions)
		_ = project.ParseMembers(row.Members)

		ids = append(ids, project.ID)
		projects = append(projects, project)
		projectToCache[fmt.Sprintf("project:id:%s", project.ID)] = project
	}

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

	return projects, nil
}

func (r *projectRepository) Search(ctx context.Context, params sqlc.SearchProjectsParams) ([]*models.ProjectEntity, error) {
	queryKey := r.generateQueryKey("project:search", params)
	var cachedIDs []string
	if err := r.c.Get(ctx, queryKey, &cachedIDs); err == nil && len(cachedIDs) > 0 {
		return r.getByIDsWithFallback(ctx, cachedIDs)
	}

	rows, err := r.q.SearchProjects(ctx, params)
	if err != nil {
		return nil, err
	}
	var projects []*models.ProjectEntity
	var ids []string
	projectToCache := make(map[string]any)

	for _, row := range rows {
		project := &models.ProjectEntity{
			ID:             convert.UUIDToString(row.ID),
			Title:          row.Title,
			Description:    convert.TextToString(row.Description),
			LatestCommitID: convert.UUIDToStringPtr(row.LatestCommitID),
			ProjectStatus:  constants.ParseProjectStatusType(row.ProjectStatus),
			LockedBy:       convert.UUIDToStringPtr(row.LockedBy),
			IsDeleted:      row.IsDeleted,
			UserID:         convert.UUIDToString(row.UserID),
			CreatedAt:      convert.TimeToPtr(row.CreatedAt),
			UpdatedAt:      convert.TimeToPtr(row.UpdatedAt),
		}
		_ = project.ParseUser(row.User)
		_ = project.ParseCommits(row.Commits)
		_ = project.ParseSubmissions(row.Submissions)
		_ = project.ParseMembers(row.Members)

		ids = append(ids, project.ID)
		projects = append(projects, project)
		projectToCache[fmt.Sprintf("project:id:%s", project.ID)] = project
	}

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

	return projects, nil
}

func (r *projectRepository) Count(ctx context.Context, params sqlc.CountProjectsParams) (int64, error) {
	queryKey := r.generateQueryKey("project:count", params)
	var count int64
	if err := r.c.Get(ctx, queryKey, &count); err == nil {
		return count, nil
	}

	count, err := r.q.CountProjects(ctx, params)
	if err != nil {
		return 0, err
	}

	_ = r.c.Set(ctx, queryKey, count, constants.NormalCacheDuration)
	return count, nil
}

func (r *projectRepository) Create(ctx context.Context, params sqlc.CreateProjectParams) (*models.ProjectEntity, error) {
	row, err := r.q.CreateProject(ctx, params)
	if err != nil {
		return nil, err
	}

	project := models.ProjectEntity{
		ID:             convert.UUIDToString(row.ID),
		Title:          row.Title,
		Description:    convert.TextToString(row.Description),
		LatestCommitID: convert.UUIDToStringPtr(row.LatestCommitID),
		ProjectStatus:  constants.ParseProjectStatusType(row.ProjectStatus),
		LockedBy:       convert.UUIDToStringPtr(row.LockedBy),
		IsDeleted:      row.IsDeleted,
		UserID:         convert.UUIDToString(row.UserID),
		CreatedAt:      convert.TimeToPtr(row.CreatedAt),
		UpdatedAt:      convert.TimeToPtr(row.UpdatedAt),
	}
	_ = project.ParseUser(row.User)
	_ = project.ParseCommits(row.Commits)
	_ = project.ParseSubmissions(row.Submissions)
	_ = project.ParseMembers(row.Members)

	go func() {
		bgCtx := context.Background()
		_ = r.c.DelByPattern(bgCtx, "project:search*")
		_ = r.c.DelByPattern(bgCtx, "project:user*")
		_ = r.c.DelByPattern(bgCtx, "project:count*")
	}()
	return &project, nil
}

func (r *projectRepository) Update(ctx context.Context, params sqlc.UpdateProjectParams) (*models.ProjectEntity, error) {
	row, err := r.q.UpdateProject(ctx, params)
	if err != nil {
		return nil, err
	}
	project := models.ProjectEntity{
		ID:             convert.UUIDToString(row.ID),
		Title:          row.Title,
		Description:    convert.TextToString(row.Description),
		LatestCommitID: convert.UUIDToStringPtr(row.LatestCommitID),
		ProjectStatus:  constants.ParseProjectStatusType(row.ProjectStatus),
		LockedBy:       convert.UUIDToStringPtr(row.LockedBy),
		IsDeleted:      row.IsDeleted,
		UserID:         convert.UUIDToString(row.UserID),
		CreatedAt:      convert.TimeToPtr(row.CreatedAt),
		UpdatedAt:      convert.TimeToPtr(row.UpdatedAt),
	}
	_ = project.ParseUser(row.User)
	_ = project.ParseCommits(row.Commits)
	_ = project.ParseSubmissions(row.Submissions)
	_ = project.ParseMembers(row.Members)

	_ = r.c.Del(ctx, fmt.Sprintf("project:id:%s", project.ID))
	return &project, nil
}

func (r *projectRepository) Delete(ctx context.Context, id pgtype.UUID) error {
	err := r.q.DeleteProject(ctx, id)
	if err != nil {
		return err
	}
	_ = r.c.Del(ctx, fmt.Sprintf("project:id:%s", convert.UUIDToString(id)))
	go func() {
		bgCtx := context.Background()
		_ = r.c.DelByPattern(bgCtx, "project:search*")
		_ = r.c.DelByPattern(bgCtx, "project:user*")
		_ = r.c.DelByPattern(bgCtx, "project:count*")
	}()
	return nil
}

func (r *projectRepository) AddMember(ctx context.Context, params sqlc.AddProjectMemberParams) error {
	_, err := r.q.AddProjectMember(ctx, params)
	if err != nil {
		return err
	}
	go func() {
		_ = r.c.DelByPattern(context.Background(), "project:user*")
	}()
	_ = r.c.Del(ctx, fmt.Sprintf("project:id:%s", convert.UUIDToString(params.ProjectID)))
	_ = r.c.Del(ctx, fmt.Sprintf("project:perm:%s:%s", convert.UUIDToString(params.ProjectID), convert.UUIDToString(params.UserID)))
	return nil
}

func (r *projectRepository) UpdateMemberRole(ctx context.Context, params sqlc.UpdateProjectMemberRoleParams) error {
	_, err := r.q.UpdateProjectMemberRole(ctx, params)
	if err != nil {
		return err
	}
	go func() {
		_ = r.c.DelByPattern(context.Background(), "project:user*")
	}()
	_ = r.c.Del(ctx, fmt.Sprintf("project:id:%s", convert.UUIDToString(params.ProjectID)))
	_ = r.c.Del(ctx, fmt.Sprintf("project:perm:%s:%s", convert.UUIDToString(params.ProjectID), convert.UUIDToString(params.UserID)))
	return nil
}

func (r *projectRepository) RemoveMember(ctx context.Context, params sqlc.RemoveProjectMemberParams) error {
	err := r.q.RemoveProjectMember(ctx, params)
	if err != nil {
		return err
	}
	go func() {
		_ = r.c.DelByPattern(context.Background(), "project:user*")
	}()
	_ = r.c.Del(ctx, fmt.Sprintf("project:id:%s", convert.UUIDToString(params.ProjectID)))
	_ = r.c.Del(ctx, fmt.Sprintf("project:perm:%s:%s", convert.UUIDToString(params.ProjectID), convert.UUIDToString(params.UserID)))
	return nil
}

func (r *projectRepository) CheckPermission(ctx context.Context, params sqlc.CheckProjectPermissionParams) (int16, error) {
	cacheKey := fmt.Sprintf("project:perm:%s:%s", convert.UUIDToString(params.ProjectID), convert.UUIDToString(params.UserID))
	var role int16
	if err := r.c.Get(ctx, cacheKey, &role); err == nil {
		return role, nil
	}

	role, err := r.q.CheckProjectPermission(ctx, params)
	if err != nil {
		return 0, err
	}

	_ = r.c.Set(ctx, cacheKey, role, constants.NormalCacheDuration)
	return role, nil
}

func (r *projectRepository) ChangeOwner(ctx context.Context, params sqlc.ChangeProjectOwnerParams) error {
	err := r.q.ChangeProjectOwner(ctx, params)
	if err != nil {
		return err
	}
	projectID := convert.UUIDToString(params.ID)
	_ = r.c.Del(ctx, fmt.Sprintf("project:id:%s", projectID))
	go func() {
		_ = r.c.DelByPattern(context.Background(), fmt.Sprintf("project:perm:%s:*", projectID))
	}()
	return nil
}

func (r *projectRepository) UpdateLatestCommit(ctx context.Context, params sqlc.UpdateLatestCommitParams) error {
	err := r.q.UpdateLatestCommit(ctx, params)
	if err != nil {
		return err
	}
	_ = r.c.Del(ctx, fmt.Sprintf("project:id:%s", convert.UUIDToString(params.ID)))
	return nil
}
