package membership

import (
	"database/sql"
	"encoding/hex"
	"errors"
	"fmt"
	"strings"
)

// User roles and statuses. They are stored as free text in the users table so
// new values can be introduced without a schema change; these constants name
// the ones the code reasons about today.
const (
	RoleAdmin     = "admin"
	RoleMember    = "member"
	StatusActive  = "active"
	StatusRevoked = "revoked"
)

// ErrUserExists is returned by AddUser when a user with the same sign_pub is
// already registered. Callers that want upsert semantics should branch on it.
var ErrUserExists = errors.New("membership: user already exists")

// User is a bus-level identity in the allowlist: the Ed25519 signing public key
// that authenticates a peer on both the control plane (request signatures) and
// the data plane (NATS nkey), plus its role and revocation status. SignPub is
// the lowercase hex of the 32-byte Ed25519 public key — the same key that
// derives the endpoint id via frame.EndpointID.
type User struct {
	SignPub   string // Ed25519 public key, lowercase hex
	Handle    string
	Role      string // RoleAdmin | RoleMember
	Status    string // StatusActive | StatusRevoked
	CreatedAt string
	RevokedAt string // empty unless revoked
}

// ValidateSignPubHex ensures signPub is exactly a 32-byte Ed25519 public key in
// hex (64 hex chars). It is the single source of truth for that check, shared by
// the local admin CLI (which validates before seeding the first admin) and the
// HTTP user-management handlers (which validate an admin-supplied key before it
// reaches the store). Catching a malformed key here turns a silent "authorized
// nobody" into an explicit error at the boundary.
func ValidateSignPubHex(signPub string) error {
	b, err := hex.DecodeString(signPub)
	if err != nil {
		return fmt.Errorf("sign-pub is not valid hex: %w", err)
	}
	if len(b) != 32 {
		return fmt.Errorf("sign-pub must be a 32-byte Ed25519 public key (64 hex chars), got %d bytes", len(b))
	}
	return nil
}

// normalizeSignPub lowercases the hex key so lookups are case-insensitive: the
// primary key is stored lowercase and every query normalizes its input the same
// way, so a caller passing uppercase hex still matches.
func normalizeSignPub(signPub string) string {
	return strings.ToLower(strings.TrimSpace(signPub))
}

// AddUser inserts a new bus user. role defaults to RoleMember when empty. It
// returns ErrUserExists if the sign_pub is already registered (the caller may
// choose to revoke+re-add or ignore). handle and signPub must be non-empty.
func (s *sqliteStore) AddUser(signPub, handle, role string) error {
	signPub = normalizeSignPub(signPub)
	if signPub == "" || handle == "" {
		return fmt.Errorf("membership: AddUser: sign_pub and handle required")
	}
	if role == "" {
		role = RoleMember
	}
	if role != RoleAdmin && role != RoleMember {
		return fmt.Errorf("membership: AddUser: invalid role %q (want %q or %q)", role, RoleAdmin, RoleMember)
	}
	_, err := s.db.Exec(
		`INSERT INTO users (sign_pub, handle, role, status, created_at) VALUES (?, ?, ?, ?, ?)`,
		signPub, handle, role, StatusActive, nowRFC3339(),
	)
	if err != nil {
		// modernc.org/sqlite surfaces a UNIQUE/PRIMARY KEY violation as a message
		// containing "UNIQUE constraint failed"; translate it into a typed error so
		// callers do not have to string-match.
		if strings.Contains(err.Error(), "UNIQUE constraint") || strings.Contains(err.Error(), "PRIMARY KEY") {
			return ErrUserExists
		}
		return fmt.Errorf("membership: insert user: %w", err)
	}
	return nil
}

// GetUser returns the user with the given signing public key. It returns
// sql.ErrNoRows (wrapped) when there is no such user.
func (s *sqliteStore) GetUser(signPub string) (User, error) {
	signPub = normalizeSignPub(signPub)
	var u User
	var revoked sql.NullString
	err := s.db.QueryRow(
		`SELECT sign_pub, handle, role, status, created_at, revoked_at FROM users WHERE sign_pub = ?`,
		signPub,
	).Scan(&u.SignPub, &u.Handle, &u.Role, &u.Status, &u.CreatedAt, &revoked)
	if err != nil {
		return User{}, fmt.Errorf("membership: get user %q: %w", signPub, err)
	}
	u.RevokedAt = revoked.String
	return u, nil
}

// ListUsers returns every user ordered by handle then sign_pub (stable output).
func (s *sqliteStore) ListUsers() ([]User, error) {
	rows, err := s.db.Query(
		`SELECT sign_pub, handle, role, status, created_at, revoked_at FROM users ORDER BY handle, sign_pub`,
	)
	if err != nil {
		return nil, fmt.Errorf("membership: list users: %w", err)
	}
	defer rows.Close()

	var out []User
	for rows.Next() {
		var u User
		var revoked sql.NullString
		if err := rows.Scan(&u.SignPub, &u.Handle, &u.Role, &u.Status, &u.CreatedAt, &revoked); err != nil {
			return nil, fmt.Errorf("membership: scan user: %w", err)
		}
		u.RevokedAt = revoked.String
		out = append(out, u)
	}
	return out, rows.Err()
}

// RevokeUser marks a user as revoked and stamps revoked_at. Revocation is a
// status flip (not a delete) so the identity stays auditable and IsAuthorized
// immediately denies it on both planes. Revoking an unknown or already-revoked
// user returns an error / is a no-op respectively.
func (s *sqliteStore) RevokeUser(signPub string) error {
	signPub = normalizeSignPub(signPub)
	res, err := s.db.Exec(
		`UPDATE users SET status = ?, revoked_at = ? WHERE sign_pub = ? AND status = ?`,
		StatusRevoked, nowRFC3339(), signPub, StatusActive,
	)
	if err != nil {
		return fmt.Errorf("membership: revoke user %q: %w", signPub, err)
	}
	n, err := res.RowsAffected()
	if err != nil {
		return fmt.Errorf("membership: revoke user %q: rows affected: %w", signPub, err)
	}
	if n == 0 {
		return fmt.Errorf("membership: revoke user %q: no active user with that key", signPub)
	}
	return nil
}

// IsAuthorized reports whether signPub belongs to an active (non-revoked) bus
// user. It is the single authorization predicate consulted by both the control
// plane (HTTP request middleware) and the data plane (NATS nkey authenticator),
// so revoking a user denies access on both without restarting anything. An
// unknown key, a revoked key, or any query error all yield false (fail closed).
func (s *sqliteStore) IsAuthorized(signPub string) bool {
	signPub = normalizeSignPub(signPub)
	if signPub == "" {
		return false
	}
	var one int
	err := s.db.QueryRow(
		`SELECT 1 FROM users WHERE sign_pub = ? AND status = ?`, signPub, StatusActive,
	).Scan(&one)
	return err == nil && one == 1
}

// HasAdmin reports whether at least one active admin exists. The control plane
// uses it to gate user-management endpoints: until the host operator seeds the
// first admin via the local CLI, those endpoints stay closed (chicken-egg).
func (s *sqliteStore) HasAdmin() bool {
	var one int
	err := s.db.QueryRow(
		`SELECT 1 FROM users WHERE role = ? AND status = ? LIMIT 1`, RoleAdmin, StatusActive,
	).Scan(&one)
	return err == nil && one == 1
}
