// Service admin provides administrative endpoints for managing application data.
package admin

import (
	"context"
	"errors"
	"time"

	authsvc "encore.app/auth"
	profilessvc "encore.app/profiles"
	"encore.dev/beta/errs"
	"encore.dev/storage/sqldb"
	"encore.dev/types/uuid"
	"github.com/jackc/pgx/v5/pgconn"
)

var profilesDB = sqldb.Named("profiles")

type Profile struct {
	UserID      uuid.UUID          `json:"user_id"`
	Email       string             `json:"email"`
	DisplayName string             `json:"display_name"`
	AvatarURL   string             `json:"avatar_url"`
	IsArtist    bool               `json:"is_artist"`
	Role        string             `json:"role"`
	Status      profilessvc.Status `json:"status"`
	CreatedAt   time.Time          `json:"created_at"`
	UpdatedAt   time.Time          `json:"updated_at"`
}

type ProfileParams struct {
	DisplayName string `json:"display_name"`
	AvatarURL   string `json:"avatar_url"`
}

type RegisterParams struct {
	Email       string `json:"email"`
	DisplayName string `json:"display_name"`
	AvatarURL   string `json:"avatar_url"`
	Password    string `json:"password" encore:"sensitive"`
}

type ListProfilesResponse struct {
	Profiles []*Profile `json:"profiles"`
}

// ListProfiles returns all user profiles, ordered by user ID.
//
//encore:api auth method=GET path=/admin/profiles
func ListProfiles(ctx context.Context) (*ListProfilesResponse, error) {
	rows, err := profilesDB.Query(ctx, `
		SELECT user_id, email, display_name, avatar_url, role, status, is_artist, created_at, updated_at
		FROM user_profiles
		ORDER BY user_id
	`)
	if err != nil {
		return nil, errs.WrapCode(err, errs.Internal, "failed to list profiles")
	}
	defer rows.Close()

	profiles := []*Profile{}
	for rows.Next() {
		var p Profile
		if err := rows.Scan(&p.UserID, &p.Email, &p.DisplayName, &p.AvatarURL, &p.Role, &p.Status, &p.IsArtist, &p.CreatedAt, &p.UpdatedAt); err != nil {
			return nil, errs.WrapCode(err, errs.Internal, "failed to scan profile")
		}
		profiles = append(profiles, &p)
	}
	if err := rows.Err(); err != nil {
		return nil, errs.WrapCode(err, errs.Internal, "failed to list profiles")
	}
	return &ListProfilesResponse{Profiles: profiles}, nil
}

// GetProfile returns the profile for the given user.
//
//encore:api auth method=GET path=/admin/profiles/:userID
func GetProfile(ctx context.Context, userID uuid.UUID) (*Profile, error) {
	p := Profile{UserID: userID}
	err := profilesDB.QueryRow(ctx, `
		SELECT email, display_name, avatar_url, role, status, is_artist, created_at, updated_at FROM user_profiles WHERE user_id = $1
	`, userID).Scan(&p.Email, &p.DisplayName, &p.AvatarURL, &p.Role, &p.Status, &p.IsArtist, &p.CreatedAt, &p.UpdatedAt)
	if errors.Is(err, sqldb.ErrNoRows) {
		return nil, &errs.Error{Code: errs.NotFound, Message: "profile not found"}
	}
	if err != nil {
		return nil, errs.WrapCode(err, errs.Internal, "failed to fetch profile")
	}
	return &p, nil
}

// InsertProfile creates a new user profile with a server-generated UUID v4,
// delegating credential setup to the auth service.
//
//encore:api auth method=POST path=/admin/profiles
func InsertProfile(ctx context.Context, p *RegisterParams) (*Profile, error) {
	userID, err := uuid.NewV4()
	if err != nil {
		return nil, errs.WrapCode(err, errs.Internal, "failed to generate profile id")
	}
	profile := Profile{UserID: userID, Email: p.Email, DisplayName: p.DisplayName, AvatarURL: p.AvatarURL}
	err = profilesDB.QueryRow(ctx, `
		INSERT INTO user_profiles (user_id, email, display_name, avatar_url)
		VALUES ($1, $2, $3, $4)
		RETURNING role, status, is_artist, created_at, updated_at
	`, userID, p.Email, p.DisplayName, p.AvatarURL).Scan(&profile.Role, &profile.Status, &profile.IsArtist, &profile.CreatedAt, &profile.UpdatedAt)
	if isUniqueViolation(err) {
		return nil, &errs.Error{Code: errs.AlreadyExists, Message: "email already registered"}
	}
	if err != nil {
		return nil, errs.WrapCode(err, errs.Internal, "failed to create profile")
	}
	if err := authsvc.Register(ctx, &authsvc.RegisterParams{UserID: userID, Password: p.Password}); err != nil {
		return nil, errs.WrapCode(err, errs.Internal, "failed to register credentials")
	}
	return &profile, nil
}

// UpdateProfile replaces the data of any user's profile.
//
//encore:api auth method=PUT path=/admin/profiles/:userID
func UpdateProfile(ctx context.Context, userID uuid.UUID, p *ProfileParams) (*Profile, error) {
	profile := Profile{UserID: userID, DisplayName: p.DisplayName, AvatarURL: p.AvatarURL}
	err := profilesDB.QueryRow(ctx, `
		UPDATE user_profiles
		SET display_name = $2, avatar_url = $3, updated_at = NOW()
		WHERE user_id = $1
		RETURNING email, role, status, is_artist, created_at, updated_at
	`, userID, p.DisplayName, p.AvatarURL).Scan(&profile.Email, &profile.Role, &profile.Status, &profile.IsArtist, &profile.CreatedAt, &profile.UpdatedAt)
	if errors.Is(err, sqldb.ErrNoRows) {
		return nil, &errs.Error{Code: errs.NotFound, Message: "profile not found"}
	}
	if err != nil {
		return nil, errs.WrapCode(err, errs.Internal, "failed to update profile")
	}
	return &profile, nil
}

type UpdateProfileStatusParams struct {
	Status profilessvc.Status `json:"status"`
}

func (p *UpdateProfileStatusParams) Validate() error {
	if !p.Status.IsValid() {
		return &errs.Error{Code: errs.InvalidArgument, Message: "invalid status"}
	}
	return nil
}

// UpdateProfileStatus sets the status of any user's profile.
//
//encore:api auth method=PUT path=/admin/profiles/:userID/status
func UpdateProfileStatus(ctx context.Context, userID uuid.UUID, p *UpdateProfileStatusParams) (*Profile, error) {
	profile := Profile{UserID: userID, Status: p.Status}
	err := profilesDB.QueryRow(ctx, `
		UPDATE user_profiles
		SET status = $2, updated_at = NOW()
		WHERE user_id = $1
		RETURNING email, display_name, avatar_url, role, is_artist, created_at, updated_at
	`, userID, p.Status).Scan(&profile.Email, &profile.DisplayName, &profile.AvatarURL, &profile.Role, &profile.IsArtist, &profile.CreatedAt, &profile.UpdatedAt)
	if errors.Is(err, sqldb.ErrNoRows) {
		return nil, &errs.Error{Code: errs.NotFound, Message: "profile not found"}
	}
	if err != nil {
		return nil, errs.WrapCode(err, errs.Internal, "failed to update profile status")
	}
	return &profile, nil
}

type UpdateProfileRoleParams struct {
	Role string `json:"role"`
}

func (p *UpdateProfileRoleParams) Validate() error {
	if !authsvc.IsValidRole(p.Role) {
		return &errs.Error{Code: errs.InvalidArgument, Message: "invalid role"}
	}
	return nil
}

// UpdateProfileRole sets the role of any user's profile.
//
//encore:api auth method=PUT path=/admin/profiles/:userID/role
func UpdateProfileRole(ctx context.Context, userID uuid.UUID, p *UpdateProfileRoleParams) (*Profile, error) {
	profile := Profile{UserID: userID, Role: p.Role}
	err := profilesDB.QueryRow(ctx, `
		UPDATE user_profiles
		SET role = $2, updated_at = NOW()
		WHERE user_id = $1
		RETURNING email, display_name, avatar_url, status, is_artist, created_at, updated_at
	`, userID, p.Role).Scan(&profile.Email, &profile.DisplayName, &profile.AvatarURL, &profile.Status, &profile.IsArtist, &profile.CreatedAt, &profile.UpdatedAt)
	if errors.Is(err, sqldb.ErrNoRows) {
		return nil, &errs.Error{Code: errs.NotFound, Message: "profile not found"}
	}
	if err != nil {
		return nil, errs.WrapCode(err, errs.Internal, "failed to update profile role")
	}
	return &profile, nil
}

type UpdateProfilePasswordParams struct {
	Password string `json:"password" encore:"sensitive"`
}

// UpdateProfilePassword sets the password of any user's profile.
//
//encore:api auth method=PUT path=/admin/profiles/:userID/password
func UpdateProfilePassword(ctx context.Context, userID uuid.UUID, p *UpdateProfilePasswordParams) error {
	if err := authsvc.SetUserPassword(ctx, userID, &authsvc.SetUserPasswordParams{UserID: userID, Password: p.Password}); err != nil {
		return err
	}
	return nil
}

type UpdateProfileArtistParams struct {
	IsArtist bool `json:"is_artist"`
}

// UpdateProfileArtist sets the artist flag of any user's profile.
//
//encore:api auth method=PUT path=/admin/profiles/:userID/artist
func UpdateProfileArtist(ctx context.Context, userID uuid.UUID, p *UpdateProfileArtistParams) (*Profile, error) {
	profile := Profile{UserID: userID, IsArtist: p.IsArtist}
	err := profilesDB.QueryRow(ctx, `
		UPDATE user_profiles
		SET is_artist = $2, updated_at = NOW()
		WHERE user_id = $1
		RETURNING email, display_name, avatar_url, role, status, created_at, updated_at
	`, userID, p.IsArtist).Scan(&profile.Email, &profile.DisplayName, &profile.AvatarURL, &profile.Role, &profile.Status, &profile.CreatedAt, &profile.UpdatedAt)
	if errors.Is(err, sqldb.ErrNoRows) {
		return nil, &errs.Error{Code: errs.NotFound, Message: "profile not found"}
	}
	if err != nil {
		return nil, errs.WrapCode(err, errs.Internal, "failed to update profile artist flag")
	}
	return &profile, nil
}

// DeleteProfile removes any user's profile.
//
//encore:api auth method=DELETE path=/admin/profiles/:userID
func DeleteProfile(ctx context.Context, userID uuid.UUID) error {
	res, err := profilesDB.Exec(ctx, `
		DELETE FROM user_profiles WHERE user_id = $1
	`, userID)
	if err != nil {
		return errs.WrapCode(err, errs.Internal, "failed to delete profile")
	}
	if res.RowsAffected() == 0 {
		return &errs.Error{Code: errs.NotFound, Message: "profile not found"}
	}
	return nil
}

type PersonalData struct {
	UserID    uuid.UUID `json:"user_id"`
	FirstName string    `json:"first_name"`
	LastName  string    `json:"last_name"`
	Address   string    `json:"address"`
	Cap       string    `json:"cap"`
	City      string    `json:"city"`
	Country   string    `json:"country"`
	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

type PersonalDataParams struct {
	FirstName string `json:"first_name"`
	LastName  string `json:"last_name"`
	Address   string `json:"address"`
	Cap       string `json:"cap"`
	City      string `json:"city"`
	Country   string `json:"country"`
}

// GetPersonalData returns the personal data for the given user.
//
//encore:api auth method=GET path=/admin/profiles/:userID/personal-data
func GetPersonalData(ctx context.Context, userID uuid.UUID) (*PersonalData, error) {
	pd := PersonalData{UserID: userID}
	err := profilesDB.QueryRow(ctx, `
		SELECT first_name, last_name, address, cap, city, country, created_at, updated_at
		FROM personal_data WHERE user_id = $1
	`, userID).Scan(&pd.FirstName, &pd.LastName, &pd.Address, &pd.Cap, &pd.City, &pd.Country, &pd.CreatedAt, &pd.UpdatedAt)
	if errors.Is(err, sqldb.ErrNoRows) {
		return nil, &errs.Error{Code: errs.NotFound, Message: "personal data not found"}
	}
	if err != nil {
		return nil, errs.WrapCode(err, errs.Internal, "failed to fetch personal data")
	}
	return &pd, nil
}

// UpsertPersonalData creates or replaces the personal data for the given user.
//
//encore:api auth method=PUT path=/admin/profiles/:userID/personal-data
func UpsertPersonalData(ctx context.Context, userID uuid.UUID, p *PersonalDataParams) (*PersonalData, error) {
	pd := PersonalData{
		UserID:    userID,
		FirstName: p.FirstName,
		LastName:  p.LastName,
		Address:   p.Address,
		Cap:       p.Cap,
		City:      p.City,
		Country:   p.Country,
	}
	err := profilesDB.QueryRow(ctx, `
		INSERT INTO personal_data (user_id, first_name, last_name, address, cap, city, country)
		VALUES ($1, $2, $3, $4, $5, $6, $7)
		ON CONFLICT (user_id) DO UPDATE
		SET first_name = EXCLUDED.first_name,
		    last_name = EXCLUDED.last_name,
		    address = EXCLUDED.address,
		    cap = EXCLUDED.cap,
		    city = EXCLUDED.city,
		    country = EXCLUDED.country,
		    updated_at = NOW()
		RETURNING created_at, updated_at
	`, userID, p.FirstName, p.LastName, p.Address, p.Cap, p.City, p.Country).Scan(&pd.CreatedAt, &pd.UpdatedAt)
	if isForeignKeyViolation(err) {
		return nil, &errs.Error{Code: errs.NotFound, Message: "profile not found"}
	}
	if err != nil {
		return nil, errs.WrapCode(err, errs.Internal, "failed to save personal data")
	}
	return &pd, nil
}

// isUniqueViolation reports whether err is a Postgres unique constraint violation.
func isUniqueViolation(err error) bool {
	var pgErr *pgconn.PgError
	return errors.As(err, &pgErr) && pgErr.Code == "23505"
}

// isForeignKeyViolation reports whether err is a Postgres foreign key constraint violation.
func isForeignKeyViolation(err error) bool {
	var pgErr *pgconn.PgError
	return errors.As(err, &pgErr) && pgErr.Code == "23503"
}
