package auth

import (
	"context"
	"database/sql"
	"fmt"
	"net/http"
	"net/url"
	"strings"
	"time"

	"github.com/go-webauthn/webauthn/protocol"
	"github.com/go-webauthn/webauthn/webauthn"
)

const (
	SessionCookieName = "cairnquire_session"
	sessionTTL        = 24 * time.Hour
	challengeTTL      = 5 * time.Minute
	deviceCodeTTL     = 15 * time.Minute
	passwordResetTTL  = time.Hour
	devicePollSeconds = 5
)

type EmailSender interface {
	Send(ctx context.Context, to []string, subject, body string) error
}

type Service struct {
	repo         *Repository
	webauthn     *webauthn.WebAuthn
	publicOrigin string
	email        EmailSender
}

func NewService(repo *Repository, publicOrigin string) (*Service, error) {
	if publicOrigin == "" {
		publicOrigin = "http://localhost:8080"
	}
	parsed, err := url.Parse(publicOrigin)
	if err != nil {
		return nil, fmt.Errorf("parse public origin: %w", err)
	}
	rpID := parsed.Hostname()
	if rpID == "" {
		return nil, fmt.Errorf("public origin must include host")
	}
	web, err := webauthn.New(&webauthn.Config{
		RPID:          rpID,
		RPDisplayName: "Cairnquire",
		RPOrigins:     []string{publicOrigin},
		AuthenticatorSelection: protocol.AuthenticatorSelection{
			ResidentKey:      protocol.ResidentKeyRequirementPreferred,
			UserVerification: protocol.VerificationPreferred,
		},
		AttestationPreference: protocol.PreferNoAttestation,
	})
	if err != nil {
		return nil, err
	}
	return &Service{repo: repo, webauthn: web, publicOrigin: publicOrigin}, nil
}

func (s *Service) SetEmailSender(sender EmailSender) {
	s.email = sender
}

func (s *Service) GetInstanceSettings(ctx context.Context) (InstanceSettings, error) {
	return s.repo.GetInstanceSettings(ctx)
}

func (s *Service) CompleteInitialSetup(ctx context.Context, email, displayName, password string, signupsEnabled bool) (User, error) {
	email = normalizeEmail(email)
	if email == "" {
		return User{}, fmt.Errorf("email is required")
	}
	if len(password) < 12 {
		return User{}, fmt.Errorf("password must be at least 12 characters")
	}
	passwordHash, err := hashPassword(password)
	if err != nil {
		return User{}, err
	}
	return s.repo.CompleteInitialSetup(ctx, User{
		Email:        email,
		DisplayName:  strings.TrimSpace(displayName),
		PasswordHash: passwordHash,
		Role:         RoleAdmin,
	}, signupsEnabled)
}

func (s *Service) RegisterPasswordUser(ctx context.Context, email, displayName, password, role string) (User, error) {
	if err := s.requirePublicSignups(ctx); err != nil {
		return User{}, err
	}
	email = normalizeEmail(email)
	if email == "" {
		return User{}, fmt.Errorf("email is required")
	}
	if len(password) < 12 {
		return User{}, fmt.Errorf("password must be at least 12 characters")
	}
	if _, err := s.repo.GetUserByEmail(ctx, email); err == nil {
		return User{}, fmt.Errorf("user already exists")
	} else if err != nil && !isNoRows(err) {
		return User{}, err
	}
	passwordHash, err := hashPassword(password)
	if err != nil {
		return User{}, err
	}
	return s.repo.UpsertUser(ctx, User{
		Email:        email,
		DisplayName:  strings.TrimSpace(displayName),
		PasswordHash: passwordHash,
		Role:         RoleViewer,
	})
}

func (s *Service) LoginPassword(ctx context.Context, email, password, ip, userAgent string) (Principal, string, error) {
	user, err := s.repo.GetUserByEmail(ctx, normalizeEmail(email))
	if err != nil {
		if isNoRows(err) {
			_, _ = verifyPassword(password, mustDummyPasswordHash())
		}
		return Principal{}, "", fmt.Errorf("invalid credentials")
	}
	if user.PasswordHash == "" {
		return Principal{}, "", fmt.Errorf("invalid credentials")
	}
	ok, err := verifyPassword(password, user.PasswordHash)
	if err != nil || !ok || user.Disabled {
		return Principal{}, "", fmt.Errorf("invalid credentials")
	}
	principal, token, err := s.createSession(ctx, user, ip, userAgent)
	if err == nil {
		s.repo.Audit(ctx, user.ID, "auth.password.login", "user", user.ID, ip, userAgent, nil)
	}
	return principal, token, err
}

func (s *Service) BeginPasskeyRegistration(ctx context.Context, email, displayName, role string) (any, string, error) {
	if err := s.requirePublicSignups(ctx); err != nil {
		return nil, "", err
	}
	user, err := s.repo.GetUserByEmail(ctx, normalizeEmail(email))
	if err != nil {
		if !isNoRows(err) {
			return nil, "", err
		}
		user, err = s.repo.UpsertUser(ctx, User{
			Email:       normalizeEmail(email),
			DisplayName: strings.TrimSpace(displayName),
			Role:        RoleViewer,
		})
		if err != nil {
			return nil, "", err
		}
	} else {
		return nil, "", fmt.Errorf("user already exists")
	}
	creation, session, err := s.webauthn.BeginRegistration(user)
	if err != nil {
		return nil, "", err
	}
	challengeID, err := s.repo.SaveChallenge(ctx, user.ID, "webauthn_registration", *session, challengeTTL)
	if err != nil {
		return nil, "", err
	}
	return creation, challengeID, nil
}

func (s *Service) FinishPasskeyRegistration(ctx context.Context, challengeID string, r *http.Request) (User, error) {
	session, userID, err := s.repo.ConsumeChallenge(ctx, challengeID, "webauthn_registration")
	if err != nil {
		return User{}, err
	}
	user, err := s.repo.GetUserByID(ctx, userID)
	if err != nil {
		return User{}, err
	}
	credential, err := s.webauthn.FinishRegistration(user, session, r)
	if err != nil {
		return User{}, err
	}
	if err := s.repo.SaveCredential(ctx, user.ID, *credential); err != nil {
		return User{}, err
	}
	s.repo.Audit(ctx, user.ID, "auth.passkey.register", "user", user.ID, clientIP(r), r.UserAgent(), nil)
	return s.repo.GetUserByID(ctx, user.ID)
}

func (s *Service) BeginCurrentUserPasskeyRegistration(ctx context.Context, principal Principal) (any, string, error) {
	if principal.UserID == "" {
		return nil, "", fmt.Errorf("authentication required")
	}
	user, err := s.repo.GetUserByID(ctx, principal.UserID)
	if err != nil {
		return nil, "", err
	}
	if user.Disabled {
		return nil, "", fmt.Errorf("account disabled")
	}
	creation, session, err := s.webauthn.BeginRegistration(user)
	if err != nil {
		return nil, "", err
	}
	challengeID, err := s.repo.SaveChallenge(ctx, user.ID, "webauthn_registration", *session, challengeTTL)
	if err != nil {
		return nil, "", err
	}
	return creation, challengeID, nil
}

func (s *Service) FinishCurrentUserPasskeyRegistration(ctx context.Context, principal Principal, challengeID string, r *http.Request) (User, error) {
	if principal.UserID == "" {
		return User{}, fmt.Errorf("authentication required")
	}
	session, userID, err := s.repo.ConsumeChallenge(ctx, challengeID, "webauthn_registration")
	if err != nil {
		return User{}, err
	}
	if userID != principal.UserID {
		return User{}, fmt.Errorf("challenge does not belong to the current user")
	}
	user, err := s.repo.GetUserByID(ctx, userID)
	if err != nil {
		return User{}, err
	}
	if user.Disabled {
		return User{}, fmt.Errorf("account disabled")
	}
	credential, err := s.webauthn.FinishRegistration(user, session, r)
	if err != nil {
		return User{}, err
	}
	if err := s.repo.SaveCredential(ctx, user.ID, *credential); err != nil {
		return User{}, err
	}
	s.repo.Audit(ctx, user.ID, "auth.passkey.add", "user", user.ID, clientIP(r), r.UserAgent(), nil)
	return s.repo.GetUserByID(ctx, user.ID)
}

func (s *Service) BeginPasskeyLogin(ctx context.Context, email string) (any, string, error) {
	user, err := s.repo.GetUserByEmail(ctx, normalizeEmail(email))
	if err != nil {
		return nil, "", err
	}
	if user.Disabled {
		return nil, "", fmt.Errorf("invalid credentials")
	}
	assertion, session, err := s.webauthn.BeginLogin(user)
	if err != nil {
		return nil, "", err
	}
	challengeID, err := s.repo.SaveChallenge(ctx, user.ID, "webauthn_login", *session, challengeTTL)
	if err != nil {
		return nil, "", err
	}
	return assertion, challengeID, nil
}

func (s *Service) FinishPasskeyLogin(ctx context.Context, challengeID string, r *http.Request) (Principal, string, error) {
	session, userID, err := s.repo.ConsumeChallenge(ctx, challengeID, "webauthn_login")
	if err != nil {
		return Principal{}, "", err
	}
	user, err := s.repo.GetUserByID(ctx, userID)
	if err != nil {
		return Principal{}, "", err
	}
	if user.Disabled {
		return Principal{}, "", fmt.Errorf("invalid credentials")
	}
	credential, err := s.webauthn.FinishLogin(user, session, r)
	if err != nil {
		return Principal{}, "", err
	}
	if err := s.repo.SaveCredential(ctx, user.ID, *credential); err != nil {
		return Principal{}, "", err
	}
	principal, token, err := s.createSession(ctx, user, clientIP(r), r.UserAgent())
	if err == nil {
		s.repo.Audit(ctx, user.ID, "auth.passkey.login", "user", user.ID, clientIP(r), r.UserAgent(), nil)
	}
	return principal, token, err
}

func (s *Service) ValidateSessionToken(ctx context.Context, token string) (Principal, error) {
	session, user, err := s.repo.ValidateSession(ctx, hashSecret(token))
	if err != nil {
		return Principal{}, err
	}
	if user.Disabled {
		return Principal{}, fmt.Errorf("account disabled")
	}
	return principalFromUser(user, "session", session.ID, "", nil, session.ExpiresAt), nil
}

func (s *Service) RevokeSession(ctx context.Context, sessionID string) error {
	return s.repo.RevokeSession(ctx, sessionID)
}

func (s *Service) ChangePassword(ctx context.Context, principal Principal, currentPassword, newPassword string) error {
	if principal.UserID == "" {
		return fmt.Errorf("authentication required")
	}
	if len(newPassword) < 12 {
		return fmt.Errorf("password must be at least 12 characters")
	}
	user, err := s.repo.GetUserByID(ctx, principal.UserID)
	if err != nil {
		return err
	}
	if user.PasswordHash != "" {
		ok, err := verifyPassword(currentPassword, user.PasswordHash)
		if err != nil || !ok {
			return fmt.Errorf("invalid credentials")
		}
	}
	passwordHash, err := hashPassword(newPassword)
	if err != nil {
		return err
	}
	if err := s.repo.UpdatePasswordHash(ctx, user.ID, passwordHash); err != nil {
		return err
	}
	s.repo.Audit(ctx, user.ID, "auth.password.change", "user", user.ID, "", "", nil)
	return nil
}

func (s *Service) DeleteAccount(ctx context.Context, principal Principal, currentPassword string) error {
	if principal.UserID == "" {
		return fmt.Errorf("authentication required")
	}
	user, err := s.repo.GetUserByID(ctx, principal.UserID)
	if err != nil {
		return err
	}
	if user.Role == RoleAdmin {
		admins, err := s.repo.CountAdmins(ctx)
		if err != nil {
			return err
		}
		if admins <= 1 {
			return fmt.Errorf("cannot delete the last admin account")
		}
	}
	if user.PasswordHash != "" {
		ok, err := verifyPassword(currentPassword, user.PasswordHash)
		if err != nil || !ok {
			return fmt.Errorf("invalid credentials")
		}
	}
	s.repo.Audit(ctx, user.ID, "auth.account.delete", "user", user.ID, "", "", nil)
	return s.repo.DeleteUser(ctx, user.ID)
}

func (s *Service) ListUsers(ctx context.Context) ([]User, error) {
	return s.repo.ListUsers(ctx)
}

func (s *Service) UpdateSignupsEnabled(ctx context.Context, actor Principal, enabled bool) (InstanceSettings, error) {
	if !Allows(actor, ScopeAdmin) {
		return InstanceSettings{}, fmt.Errorf("admin role required")
	}
	if err := s.repo.UpdateSignupsEnabled(ctx, enabled); err != nil {
		return InstanceSettings{}, err
	}
	return s.repo.GetInstanceSettings(ctx)
}

func (s *Service) EnsureDevUser(ctx context.Context) (User, error) {
	const email = "dev@localhost"
	settings, err := s.repo.GetInstanceSettings(ctx)
	if err != nil {
		return User{}, err
	}
	if !settings.SetupComplete {
		return s.CompleteInitialSetup(ctx, email, "Development Admin", "development-only-password", true)
	}
	user, err := s.repo.GetUserByEmail(ctx, email)
	if err == nil {
		return user, nil
	}
	if !isNoRows(err) {
		return User{}, err
	}
	return s.repo.UpsertUser(ctx, User{Email: email, DisplayName: "Development Admin", Role: RoleAdmin})
}

func (s *Service) PrincipalForUser(ctx context.Context, userID string) (Principal, error) {
	user, err := s.repo.GetUserByID(ctx, userID)
	if err != nil {
		return Principal{}, err
	}
	if user.Disabled {
		return Principal{}, fmt.Errorf("account disabled")
	}
	return principalFromUser(user, "dev", "", "", nil, time.Time{}), nil
}

func (s *Service) UpdateUserRole(ctx context.Context, actor Principal, userID, role string) (User, error) {
	user, err := s.repo.GetUserByID(ctx, userID)
	if err != nil {
		return User{}, err
	}
	users, err := s.UpdateUserAccess(ctx, actor, []UserAccessUpdate{{
		ID:       userID,
		Role:     Role(role),
		Disabled: user.Disabled,
	}})
	if err != nil {
		return User{}, err
	}
	return users[0], nil
}

func (s *Service) UpdateUserAccess(ctx context.Context, actor Principal, updates []UserAccessUpdate) ([]User, error) {
	if !Allows(actor, ScopeAdmin) {
		return nil, fmt.Errorf("admin role required")
	}
	if len(updates) == 0 {
		return nil, fmt.Errorf("at least one user update is required")
	}
	normalized := make([]UserAccessUpdate, 0, len(updates))
	seen := make(map[string]struct{}, len(updates))
	for _, update := range updates {
		update.ID = strings.TrimSpace(update.ID)
		if update.ID == "" {
			return nil, fmt.Errorf("user id is required")
		}
		if _, ok := seen[update.ID]; ok {
			return nil, fmt.Errorf("duplicate user update")
		}
		seen[update.ID] = struct{}{}
		update.Role = Role(strings.ToLower(strings.TrimSpace(string(update.Role))))
		switch update.Role {
		case RoleViewer, RoleEditor, RoleAdmin:
		default:
			return nil, fmt.Errorf("invalid role")
		}
		normalized = append(normalized, update)
	}
	if err := s.repo.UpdateUserAccess(ctx, normalized); err != nil {
		return nil, err
	}
	users := make([]User, 0, len(normalized))
	for _, update := range normalized {
		user, err := s.repo.GetUserByID(ctx, update.ID)
		if err != nil {
			return nil, err
		}
		s.repo.Audit(ctx, actor.UserID, "auth.user.access.update", "user", user.ID, "", "", map[string]any{
			"role":     user.Role,
			"disabled": user.Disabled,
		})
		users = append(users, user)
	}
	return users, nil
}

func (s *Service) ListResourcePermissions(ctx context.Context, actor Principal, resourceType ResourceType, resourceID string) ([]ResourcePermission, error) {
	if actor.UserID == "" {
		return nil, fmt.Errorf("authentication required")
	}
	if actor.Role != RoleAdmin {
		permission, err := s.repo.EffectiveResourcePermission(ctx, actor, resourceType, resourceID, nil)
		if err != nil {
			return nil, err
		}
		if !PermissionAllows(permission, PermissionAdmin) {
			return nil, fmt.Errorf("admin permission required")
		}
	}
	return s.repo.ListResourcePermissions(ctx, resourceType, resourceID)
}

func (s *Service) UpdateResourcePermissions(ctx context.Context, actor Principal, resourceType ResourceType, resourceID string, updates []ResourcePermissionUpdate) ([]ResourcePermission, error) {
	if actor.UserID == "" {
		return nil, fmt.Errorf("authentication required")
	}
	if actor.Role != RoleAdmin {
		permission, err := s.repo.EffectiveResourcePermission(ctx, actor, resourceType, resourceID, nil)
		if err != nil {
			return nil, err
		}
		if !PermissionAllows(permission, PermissionAdmin) {
			return nil, fmt.Errorf("admin permission required")
		}
	}
	normalized := make([]ResourcePermissionUpdate, 0, len(updates))
	seen := make(map[string]struct{}, len(updates))
	for _, update := range updates {
		update.UserID = strings.TrimSpace(update.UserID)
		if update.UserID == "" {
			continue
		}
		if _, ok := seen[update.UserID]; ok {
			return nil, fmt.Errorf("duplicate user permission")
		}
		seen[update.UserID] = struct{}{}
		update.Permission = normalizePermission(update.Permission)
		if !validPermission(update.Permission) {
			return nil, fmt.Errorf("invalid permission")
		}
		normalized = append(normalized, update)
	}
	if err := s.repo.ReplaceResourcePermissions(ctx, resourceType, resourceID, actor.UserID, normalized); err != nil {
		return nil, err
	}
	s.repo.Audit(ctx, actor.UserID, "auth.resource.permissions.update", string(resourceType), resourceID, "", "", map[string]any{
		"updates": len(normalized),
	})
	return s.repo.ListResourcePermissions(ctx, resourceType, resourceID)
}

func (s *Service) EnsureResourcePermission(ctx context.Context, actor Principal, resourceType ResourceType, resourceID string, update ResourcePermissionUpdate) error {
	if actor.UserID == "" {
		return fmt.Errorf("authentication required")
	}
	update.UserID = strings.TrimSpace(update.UserID)
	if update.UserID == "" {
		return fmt.Errorf("user id is required")
	}
	update.Permission = normalizePermission(update.Permission)
	if !validPermission(update.Permission) || update.Permission == PermissionNone {
		return fmt.Errorf("invalid permission")
	}
	if update.UserID != actor.UserID && actor.Role != RoleAdmin {
		permission, err := s.repo.EffectiveResourcePermission(ctx, actor, resourceType, resourceID, nil)
		if err != nil {
			return err
		}
		if !PermissionAllows(permission, PermissionAdmin) {
			return fmt.Errorf("admin permission required")
		}
	}
	if err := s.repo.EnsureResourcePermission(ctx, resourceType, resourceID, actor.UserID, update); err != nil {
		return err
	}
	s.repo.Audit(ctx, actor.UserID, "auth.resource.permissions.ensure", string(resourceType), resourceID, "", "", map[string]any{
		"userID":     update.UserID,
		"permission": update.Permission,
	})
	return nil
}

func (s *Service) EffectiveResourcePermission(ctx context.Context, principal Principal, resourceType ResourceType, resourceID string, collectionIDs []string) (Permission, error) {
	return s.repo.EffectiveResourcePermission(ctx, principal, resourceType, resourceID, collectionIDs)
}

func (s *Service) SendPasswordReset(ctx context.Context, actor Principal, userID string) error {
	if !Allows(actor, ScopeAdmin) {
		return fmt.Errorf("admin role required")
	}
	if s.email == nil {
		return fmt.Errorf("email sender is not configured")
	}
	user, err := s.repo.GetUserByID(ctx, userID)
	if err != nil {
		return err
	}
	secret, err := randomToken(32)
	if err != nil {
		return err
	}
	id, err := randomToken(18)
	if err != nil {
		return err
	}
	now := time.Now().UTC()
	if err := s.repo.CreatePasswordResetToken(ctx, PasswordResetToken{
		ID:          "reset:" + id,
		UserID:      user.ID,
		TokenHash:   hashSecret(secret),
		CreatedAt:   now,
		ExpiresAt:   now.Add(passwordResetTTL),
		InitiatedBy: actor.UserID,
	}); err != nil {
		return err
	}
	link := s.publicOrigin + "/password-reset?token=" + url.QueryEscape(secret)
	body := fmt.Sprintf("A Cairnquire administrator requested a password reset for your account.\n\nSet a new password within one hour:\n%s\n\nIf you did not expect this message, contact your administrator.", link)
	if err := s.email.Send(ctx, []string{user.Email}, "Reset your Cairnquire password", body); err != nil {
		s.repo.ExpirePasswordResetToken(ctx, hashSecret(secret))
		return err
	}
	s.repo.Audit(ctx, actor.UserID, "auth.password.reset.request", "user", user.ID, "", "", nil)
	return nil
}

func (s *Service) ResetPassword(ctx context.Context, token, newPassword string) error {
	if len(newPassword) < 12 {
		return fmt.Errorf("password must be at least 12 characters")
	}
	if strings.TrimSpace(token) == "" {
		return fmt.Errorf("password reset token is required")
	}
	passwordHash, err := hashPassword(newPassword)
	if err != nil {
		return err
	}
	userID, err := s.repo.CompletePasswordReset(ctx, hashSecret(token), passwordHash)
	if err != nil {
		return err
	}
	s.repo.Audit(ctx, userID, "auth.password.reset.complete", "user", userID, "", "", nil)
	return nil
}

func (s *Service) CreateAPIKey(ctx context.Context, userID, name string, scopes []Scope, expiresAt *time.Time) (CreatedAPIKey, error) {
	user, err := s.repo.GetUserByID(ctx, userID)
	if err != nil {
		return CreatedAPIKey{}, err
	}
	if strings.TrimSpace(name) == "" {
		name = "API token"
	}
	tokenID, err := randomHex(12)
	if err != nil {
		return CreatedAPIKey{}, err
	}
	secret, err := randomHex(32)
	if err != nil {
		return CreatedAPIKey{}, err
	}
	now := time.Now().UTC()
	keyID := "api:" + tokenID
	token := "cq_pat_" + tokenID + "_" + secret
	record := APIKey{
		ID:        keyID,
		UserID:    user.ID,
		Name:      strings.TrimSpace(name),
		KeyHash:   hashSecret(secret),
		Scopes:    normalizeScopes(scopes),
		CreatedAt: now,
		ExpiresAt: expiresAt,
	}
	if err := s.repo.CreateAPIKey(ctx, record); err != nil {
		return CreatedAPIKey{}, err
	}
	return CreatedAPIKey{Record: record, Token: token}, nil
}

func (s *Service) ValidateBearerToken(ctx context.Context, token string) (Principal, error) {
	keyID, secret, err := parseAPIToken(token)
	if err != nil {
		return Principal{}, err
	}
	key, user, err := s.repo.ValidateAPIKey(ctx, keyID, hashSecret(secret))
	if err != nil {
		return Principal{}, err
	}
	if user.Disabled {
		return Principal{}, fmt.Errorf("account disabled")
	}
	expiresAt := time.Time{}
	if key.ExpiresAt != nil {
		expiresAt = *key.ExpiresAt
	}
	return principalFromUser(user, "api_token", "", key.ID, key.Scopes, expiresAt), nil
}

func (s *Service) ListAPIKeys(ctx context.Context, userID string) ([]APIKey, error) {
	return s.repo.ListAPIKeys(ctx, userID)
}

func (s *Service) RevokeAPIKey(ctx context.Context, userID, keyID string) error {
	return s.repo.RevokeAPIKey(ctx, userID, keyID)
}

func (s *Service) StartDeviceFlow(ctx context.Context, name string, scopes []Scope) (DeviceStart, error) {
	deviceSecret, err := randomToken(32)
	if err != nil {
		return DeviceStart{}, err
	}
	rawUserCode, err := randomToken(5)
	if err != nil {
		return DeviceStart{}, err
	}
	userCode := formatUserCode(rawUserCode)
	idPart, err := randomToken(12)
	if err != nil {
		return DeviceStart{}, err
	}
	now := time.Now().UTC()
	device := DeviceCode{
		ID:              "dev:" + idPart,
		DeviceCodeHash:  hashSecret(deviceSecret),
		UserCodeHash:    hashSecret(normalizeUserCode(userCode)),
		UserCodeDisplay: userCode,
		Name:            strings.TrimSpace(name),
		Scopes:          normalizeScopes(scopes),
		Status:          "pending",
		CreatedAt:       now,
		ExpiresAt:       now.Add(deviceCodeTTL),
	}
	if device.Name == "" {
		device.Name = "Device"
	}
	if err := s.repo.CreateDeviceCode(ctx, device); err != nil {
		return DeviceStart{}, err
	}
	verifyURI := strings.TrimRight(s.publicOrigin, "/") + "/device/verify"
	return DeviceStart{
		DeviceCode:              deviceSecret,
		UserCode:                userCode,
		VerificationURI:         verifyURI,
		VerificationURIComplete: verifyURI + "?user_code=" + url.QueryEscape(userCode),
		ExpiresIn:               int(deviceCodeTTL.Seconds()),
		Interval:                devicePollSeconds,
	}, nil
}

func (s *Service) ApproveDeviceFlow(ctx context.Context, principal Principal, userCode string) (DeviceCode, error) {
	if principal.UserID == "" {
		return DeviceCode{}, fmt.Errorf("authentication required")
	}
	return s.repo.ApproveDeviceCode(ctx, hashSecret(normalizeUserCode(userCode)), principal.UserID)
}

func (s *Service) PollDeviceFlow(ctx context.Context, deviceCode string) (CreatedAPIKey, string, error) {
	device, err := s.repo.GetDeviceCodeByDeviceCode(ctx, hashSecret(deviceCode))
	if err != nil {
		if errorsIsNoRows(err) {
			return CreatedAPIKey{}, "invalid_device_code", err
		}
		return CreatedAPIKey{}, "", err
	}
	s.repo.TouchDevicePoll(ctx, device.ID)
	switch device.Status {
	case "pending":
		return CreatedAPIKey{}, "authorization_pending", sql.ErrNoRows
	case "denied", "expired":
		return CreatedAPIKey{}, device.Status, fmt.Errorf("device flow %s", device.Status)
	case "approved":
		created, err := s.CreateAPIKey(ctx, device.UserID, device.Name, device.Scopes, nil)
		if err == nil {
			s.repo.FinishDeviceCode(ctx, device.ID)
		}
		return created, "", err
	default:
		return CreatedAPIKey{}, "invalid_state", fmt.Errorf("invalid device status")
	}
}

func (s *Service) createSession(ctx context.Context, user User, ip, userAgent string) (Principal, string, error) {
	if user.Disabled {
		return Principal{}, "", fmt.Errorf("account disabled")
	}
	token, err := randomToken(32)
	if err != nil {
		return Principal{}, "", err
	}
	session, err := s.repo.CreateSession(ctx, user.ID, hashSecret(token), ip, userAgent, sessionTTL)
	if err != nil {
		return Principal{}, "", err
	}
	return principalFromUser(user, "session", session.ID, "", nil, session.ExpiresAt), token, nil
}

func (s *Service) requirePublicSignups(ctx context.Context) error {
	settings, err := s.repo.GetInstanceSettings(ctx)
	if err != nil {
		return err
	}
	if !settings.SetupComplete {
		return fmt.Errorf("initial setup is required")
	}
	if !settings.SignupsEnabled {
		return fmt.Errorf("signups are disabled")
	}
	return nil
}

func principalFromUser(user User, method, sessionID, apiKeyID string, scopes []Scope, expiresAt time.Time) Principal {
	return Principal{
		UserID:      user.ID,
		Email:       user.Email,
		DisplayName: user.DisplayName,
		Role:        user.Role,
		Scopes:      scopes,
		Method:      method,
		SessionID:   sessionID,
		APIKeyID:    apiKeyID,
		ExpiresAt:   expiresAt,
	}
}

func parseAPIToken(token string) (string, string, error) {
	parts := strings.SplitN(token, "_", 4)
	if len(parts) != 4 || parts[0] != "cq" || parts[1] != "pat" {
		return "", "", fmt.Errorf("invalid api token")
	}
	return "api:" + parts[2], parts[3], nil
}

func normalizeEmail(email string) string {
	return strings.TrimSpace(strings.ToLower(email))
}

func formatUserCode(raw string) string {
	clean := strings.ToUpper(strings.NewReplacer("-", "", "_", "").Replace(raw))
	if len(clean) > 8 {
		clean = clean[:8]
	}
	if len(clean) > 4 {
		return clean[:4] + "-" + clean[4:]
	}
	return clean
}

func normalizeUserCode(code string) string {
	return strings.ToUpper(strings.NewReplacer("-", "", " ", "", "_", "").Replace(strings.TrimSpace(code)))
}

func errorsIsNoRows(err error) bool {
	return err == sql.ErrNoRows
}

var dummyPasswordHash string

func mustDummyPasswordHash() string {
	if dummyPasswordHash == "" {
		hash, err := hashPassword("not-the-password")
		if err != nil {
			panic(err)
		}
		dummyPasswordHash = hash
	}
	return dummyPasswordHash
}

func clientIP(r *http.Request) string {
	if r == nil {
		return ""
	}
	if forwarded := r.Header.Get("X-Forwarded-For"); forwarded != "" {
		parts := strings.Split(forwarded, ",")
		return strings.TrimSpace(parts[0])
	}
	return r.RemoteAddr
}

func RoleAllows(role Role, required Scope) bool {
	switch required {
	case ScopeDocsRead, ScopeSyncRead:
		return role == RoleViewer || role == RoleEditor || role == RoleAdmin
	case ScopeDocsWrite, ScopeSyncWrite:
		return role == RoleEditor || role == RoleAdmin
	case ScopeAdmin:
		return role == RoleAdmin
	default:
		return false
	}
}

func PermissionAllows(actual Permission, required Permission) bool {
	return permissionRank(actual) >= permissionRank(required)
}

func normalizePermission(permission Permission) Permission {
	switch Permission(strings.ToLower(strings.TrimSpace(string(permission)))) {
	case "view":
		return PermissionRead
	case "edit":
		return PermissionWrite
	case PermissionRead, PermissionWrite, PermissionAdmin, PermissionNone:
		return Permission(strings.ToLower(strings.TrimSpace(string(permission))))
	default:
		return Permission(strings.ToLower(strings.TrimSpace(string(permission))))
	}
}

func Allows(principal Principal, required Scope) bool {
	if principal.UserID == "" {
		return false
	}
	if len(principal.Scopes) > 0 && !hasScope(principal.Scopes, required) {
		return false
	}
	return RoleAllows(principal.Role, required)
}
