package group

import (
	"context"

	"github.com/google/uuid"
	"github.com/jackc/pgx/v5/pgxpool"
)

type Repository interface {
	List(ctx context.Context) ([]Group, error)
	Create(ctx context.Context, input CreateRequest) (Group, error)
	Update(ctx context.Context, groupID uuid.UUID, input UpdateRequest) (Group, error)
	Delete(ctx context.Context, groupID uuid.UUID) error
}

type PGRepository struct {
	db *pgxpool.Pool
}

func NewPGRepository(db *pgxpool.Pool) *PGRepository {
	return &PGRepository{db: db}
}

func (r *PGRepository) List(ctx context.Context) ([]Group, error) {
	rows, err := r.db.Query(ctx, `
		select id, name, description
		from groups
		where deleted_at is null
		order by created_at desc
	`)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var items []Group
	for rows.Next() {
		var item Group
		if err := rows.Scan(&item.ID, &item.Name, &item.Description); err != nil {
			return nil, err
		}

		members, err := r.listMembers(ctx, item.ID)
		if err != nil {
			return nil, err
		}
		item.Members = members
		for _, member := range members {
			item.UserIDs = append(item.UserIDs, member.ID)
		}
		items = append(items, item)
	}

	return items, rows.Err()
}

func (r *PGRepository) Create(ctx context.Context, input CreateRequest) (Group, error) {
	tx, err := r.db.Begin(ctx)
	if err != nil {
		return Group{}, err
	}
	defer tx.Rollback(ctx)

	groupID := uuid.New()
	if _, err := tx.Exec(ctx, `
		insert into groups (id, name, description)
		values ($1, $2, $3)
	`, groupID, input.Name, input.Description); err != nil {
		return Group{}, err
	}

	for _, userID := range input.UserIDs {
		if _, err := tx.Exec(ctx, `
			insert into group_memberships (id, group_id, user_id)
			values ($1, $2, $3)
		`, uuid.New(), groupID, userID); err != nil {
			return Group{}, err
		}
	}

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

	return r.getByID(ctx, groupID)
}

func (r *PGRepository) Update(ctx context.Context, groupID uuid.UUID, input UpdateRequest) (Group, error) {
	tx, err := r.db.Begin(ctx)
	if err != nil {
		return Group{}, err
	}
	defer tx.Rollback(ctx)

	if _, err := tx.Exec(ctx, `
		update groups
		set name = coalesce($2, name),
		    description = coalesce($3, description),
		    updated_at = now()
		where id = $1 and deleted_at is null
	`, groupID, input.Name, input.Description); err != nil {
		return Group{}, err
	}

	if input.UserIDs != nil {
		if _, err := tx.Exec(ctx, `delete from group_memberships where group_id = $1`, groupID); err != nil {
			return Group{}, err
		}
		for _, userID := range input.UserIDs {
			if _, err := tx.Exec(ctx, `
				insert into group_memberships (id, group_id, user_id)
				values ($1, $2, $3)
			`, uuid.New(), groupID, userID); err != nil {
				return Group{}, err
			}
		}
	}

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

	return r.getByID(ctx, groupID)
}

func (r *PGRepository) Delete(ctx context.Context, groupID uuid.UUID) error {
	_, err := r.db.Exec(ctx, `
		update groups
		set deleted_at = now(), updated_at = now()
		where id = $1 and deleted_at is null
	`, groupID)
	return err
}

func (r *PGRepository) getByID(ctx context.Context, groupID uuid.UUID) (Group, error) {
	row := r.db.QueryRow(ctx, `
		select id, name, description
		from groups
		where id = $1 and deleted_at is null
	`, groupID)

	var item Group
	if err := row.Scan(&item.ID, &item.Name, &item.Description); err != nil {
		return Group{}, err
	}

	members, err := r.listMembers(ctx, item.ID)
	if err != nil {
		return Group{}, err
	}
	item.Members = members
	for _, member := range members {
		item.UserIDs = append(item.UserIDs, member.ID)
	}

	return item, nil
}

func (r *PGRepository) listMembers(ctx context.Context, groupID uuid.UUID) ([]Member, error) {
	rows, err := r.db.Query(ctx, `
		select u.id, u.username, u.display_name
		from group_memberships gm
		join users u on u.id = gm.user_id
		where gm.group_id = $1 and u.deleted_at is null
		order by u.username asc
	`, groupID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var items []Member
	for rows.Next() {
		var item Member
		if err := rows.Scan(&item.ID, &item.Username, &item.DisplayName); err != nil {
			return nil, err
		}
		items = append(items, item)
	}

	return items, rows.Err()
}
