package db

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"maps"
	"slices"

	"github.com/owncord/server/db/dbgen"
)

// roleFromGen maps the sqlc-generated Role row to the domain Role model,
// narrowing the int64 position and converting the int64 is_default flag to a
// bool. Shared by all role reads that delegate to dbgen.
func roleFromGen(r dbgen.Role) *Role {
	return &Role{
		ID:          r.ID,
		Name:        r.Name,
		Color:       r.Color,
		Permissions: r.Permissions,
		Position:    int(r.Position),
		IsDefault:   r.IsDefault != 0,
	}
}

// GetRoleByID returns the role with the given ID, or nil if not found.
func (d *DB) GetRoleByID(ctx context.Context, id int64) (*Role, error) {
	r, err := d.q.GetRoleByID(ctx, id)
	if errors.Is(err, sql.ErrNoRows) {
		return nil, nil
	}
	if err != nil {
		return nil, fmt.Errorf("GetRoleByID: %w", err)
	}
	return roleFromGen(r), nil
}

// ListRoles returns all roles ordered by position descending.
func (d *DB) ListRoles(ctx context.Context) ([]*Role, error) {
	rows, err := d.q.ListRoles(ctx)
	if err != nil {
		return nil, fmt.Errorf("ListRoles: %w", err)
	}
	roles := make([]*Role, 0, len(rows))
	for _, r := range rows {
		roles = append(roles, roleFromGen(r))
	}
	return roles, nil
}

// GetRoleForUser returns only the role for a given user via a single JOIN.
// Unlike GetUserWithRole, this does not fetch sensitive user columns (password,
// TOTP secret). Use this on hot paths like permission checks.
// Returns (nil, nil) when the user is not found.
func (d *DB) GetRoleForUser(ctx context.Context, userID int64) (*Role, error) {
	r, err := d.q.GetRoleForUser(ctx, userID)
	if errors.Is(err, sql.ErrNoRows) {
		return nil, nil
	}
	if err != nil {
		return nil, fmt.Errorf("GetRoleForUser: %w", err)
	}
	return roleFromGen(r), nil
}

// GetRoleByName returns the role whose name matches name case-insensitively,
// or nil if there is none. Case-insensitive because migration 023 enforces
// uniqueness under the same collation — the lookup and the constraint must
// agree, or "Moderator" and "moderator" become two roles the client (which
// matches names case-insensitively) cannot tell apart.
func (d *DB) GetRoleByName(ctx context.Context, name string) (*Role, error) {
	r, err := d.q.GetRoleByName(ctx, name)
	if errors.Is(err, sql.ErrNoRows) {
		return nil, nil
	}
	if err != nil {
		return nil, fmt.Errorf("GetRoleByName: %w", err)
	}
	return roleFromGen(r), nil
}

// GetDefaultRole returns the role new members are created with and deleted
// roles' members fall back to. Returns (nil, nil) when no role is flagged
// default — callers must treat that as a configuration error, not a licence to
// leave members pointing at a deleted role.
func (d *DB) GetDefaultRole(ctx context.Context) (*Role, error) {
	r, err := d.q.GetDefaultRole(ctx)
	if errors.Is(err, sql.ErrNoRows) {
		return nil, nil
	}
	if err != nil {
		return nil, fmt.Errorf("GetDefaultRole: %w", err)
	}
	return roleFromGen(r), nil
}

// CreateRole inserts a role and returns it. is_default is always 0: exactly one
// role is the default and it is seeded, never created through the API.
func (d *DB) CreateRole(ctx context.Context, name string, color *string, perms int64, position int) (*Role, error) {
	r, err := d.q.CreateRole(ctx, dbgen.CreateRoleParams{
		Name:        name,
		Color:       color,
		Permissions: perms,
		Position:    int64(position),
	})
	if err != nil {
		return nil, fmt.Errorf("CreateRole: %w", err)
	}
	return roleFromGen(r), nil
}

// UpdateRole overwrites a role's mutable columns. is_default is deliberately
// not writable: which role is the fallback is a schema decision.
func (d *DB) UpdateRole(ctx context.Context, id int64, name string, color *string, perms int64, position int) error {
	if err := d.q.UpdateRole(ctx, dbgen.UpdateRoleParams{
		Name:        name,
		Color:       color,
		Permissions: perms,
		Position:    int64(position),
		ID:          id,
	}); err != nil {
		return fmt.Errorf("UpdateRole: %w", err)
	}
	return nil
}

// ListUserIDsByRole returns the ids of every user currently holding roleID.
// Used to invalidate exactly the permission-cache entries a role change
// affects instead of dropping the whole cache.
func (d *DB) ListUserIDsByRole(ctx context.Context, roleID int64) ([]int64, error) {
	ids, err := d.q.ListUserIDsByRole(ctx, roleID)
	if err != nil {
		return nil, fmt.Errorf("ListUserIDsByRole: %w", err)
	}
	return ids, nil
}

// CountRoleMembers returns member counts keyed by role id. Roles with no
// members are absent from the map rather than present with a zero.
func (d *DB) CountRoleMembers(ctx context.Context) (map[int64]int, error) {
	rows, err := d.q.CountRoleMembers(ctx)
	if err != nil {
		return nil, fmt.Errorf("CountRoleMembers: %w", err)
	}
	counts := make(map[int64]int, len(rows))
	for _, row := range rows {
		counts[row.RoleID] = int(row.MemberCount)
	}
	return counts, nil
}

// SetRolePositions writes new positions for several roles in one writer
// transaction, so a reader can never observe a half-applied reorder (which
// would briefly duplicate or invert the hierarchy the permission checks read).
func (d *DB) SetRolePositions(ctx context.Context, positions map[int64]int) error {
	if len(positions) == 0 {
		return nil
	}
	tx, err := d.writer.BeginTx(ctx, nil)
	if err != nil {
		return fmt.Errorf("SetRolePositions begin tx: %w", err)
	}
	defer tx.Rollback() //nolint:errcheck

	q := d.q.WithTx(tx)
	// Sorted so concurrent reorders always take the rows in the same order.
	for _, id := range slices.Sorted(maps.Keys(positions)) {
		if err := q.SetRolePosition(ctx, dbgen.SetRolePositionParams{
			Position: int64(positions[id]),
			ID:       id,
		}); err != nil {
			return fmt.Errorf("SetRolePositions update %d: %w", id, err)
		}
	}
	if err := tx.Commit(); err != nil {
		return fmt.Errorf("SetRolePositions commit: %w", err)
	}
	return nil
}

// DeleteRoleReassigning deletes roleID after moving every member onto
// fallbackRoleID and dropping the role's channel_overrides rows, all in one
// writer transaction: a member must never be observable pointing at a role row
// that no longer exists, and a stale override would silently apply to whichever
// role reused the id.
//
// Returns the ids of the reassigned members so the caller can invalidate their
// cached permissions and re-sync their clients.
func (d *DB) DeleteRoleReassigning(ctx context.Context, roleID, fallbackRoleID int64) ([]int64, error) {
	tx, err := d.writer.BeginTx(ctx, nil)
	if err != nil {
		return nil, fmt.Errorf("DeleteRoleReassigning begin tx: %w", err)
	}
	defer tx.Rollback() //nolint:errcheck

	q := d.q.WithTx(tx)
	moved, err := q.ListUserIDsByRole(ctx, roleID)
	if err != nil {
		return nil, fmt.Errorf("DeleteRoleReassigning list members: %w", err)
	}
	// One UPDATE for every member, not one per member.
	if _, err := tx.ExecContext(ctx,
		`UPDATE users SET role_id = ? WHERE role_id = ?`, fallbackRoleID, roleID,
	); err != nil {
		return nil, fmt.Errorf("DeleteRoleReassigning reassign: %w", err)
	}
	// channel_overrides cascades on delete in the production schema, but the
	// delete is explicit so the behaviour does not depend on the foreign-key
	// pragma being on for this connection.
	if _, err := tx.ExecContext(ctx,
		`DELETE FROM channel_overrides WHERE role_id = ?`, roleID,
	); err != nil {
		return nil, fmt.Errorf("DeleteRoleReassigning drop overrides: %w", err)
	}
	if err := q.DeleteRole(ctx, roleID); err != nil {
		return nil, fmt.Errorf("DeleteRoleReassigning delete: %w", err)
	}
	if err := tx.Commit(); err != nil {
		return nil, fmt.Errorf("DeleteRoleReassigning commit: %w", err)
	}
	return moved, nil
}

// GetUserWithRole returns the user and their role in a single query.
// Returns (nil, nil, nil) when the user is not found.
func (d *DB) GetUserWithRole(ctx context.Context, userID int64) (*User, *Role, error) {
	row := d.reader.QueryRowContext(ctx,
		`SELECT u.id, u.username, u.password, u.avatar, u.role_id,
		        u.totp_secret, u.status, u.created_at, u.last_seen,
		        u.banned, u.ban_reason, u.ban_expires,
		        r.id, r.name, r.color, r.permissions, r.position, r.is_default
		 FROM users u
		 JOIN roles r ON u.role_id = r.id
		 WHERE u.id = ?`,
		userID,
	)

	u := &User{}
	r := &Role{}
	var banned, isDefault int
	err := row.Scan(
		&u.ID, &u.Username, &u.PasswordHash, &u.Avatar, &u.RoleID,
		&u.TOTPSecret, &u.Status, &u.CreatedAt, &u.LastSeen,
		&banned, &u.BanReason, &u.BanExpires,
		&r.ID, &r.Name, &r.Color, &r.Permissions, &r.Position, &isDefault,
	)
	if errors.Is(err, sql.ErrNoRows) {
		return nil, nil, nil
	}
	if err != nil {
		return nil, nil, fmt.Errorf("GetUserWithRole: %w", err)
	}
	u.Banned = banned != 0
	r.IsDefault = isDefault != 0
	return u, r, nil
}
