// Package webauthn implements the WebAuthn (passkeys) connector for
// the bundled Dex (REQ-148, D-240, D-243, D-244). Passkeys are
// public-key credentials — the private key never leaves the
// authenticator — directly satisfying R-021 (no passwords, no shared
// secrets). The connector serves registration + login ceremonies
// behind Traefik at /orca/webauthn/{register,login}.
//
// Credential storage: SQLite at ClusterDir()/webauthn-credentials.db
// (0600). Stores public keys + credential IDs + sign counts only.
// No private keys, no secrets.
package webauthn

import (
	"database/sql"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"sync"
	"time"

	_ "modernc.org/sqlite"
)

// Credential is a stored WebAuthn public-key credential.
type Credential struct {
	UserID       string    `json:"user_id"`
	CredentialID []byte    `json:"credential_id"`
	PublicKey    []byte    `json:"public_key"`
	SignCount    uint32    `json:"sign_count"`
	AAGUID       string    `json:"aaguid"`
	CreatedAt    time.Time `json:"created_at"`
}

// Store is the SQLite-backed credential store.
type Store struct {
	db   *sql.DB
	path string
	mu   sync.Mutex
}

// NewStore opens (or creates) the WebAuthn credential DB at the given
// path. The DB file mode is enforced at 0600.
func NewStore(dbPath string) (*Store, error) {
	if err := os.MkdirAll(filepath.Dir(dbPath), 0o700); err != nil {
		return nil, fmt.Errorf("webauthn: mkdir: %w", err)
	}
	dsn := fmt.Sprintf("file:%s?_pragma=journal_mode(WAL)", dbPath)
	db, err := sql.Open("sqlite", dsn)
	if err != nil {
		return nil, fmt.Errorf("webauthn: open db: %w", err)
	}
	if err := db.Ping(); err != nil {
		db.Close()
		return nil, fmt.Errorf("webauthn: ping: %w", err)
	}
	schema := `
CREATE TABLE IF NOT EXISTS credentials (
    user_id       TEXT PRIMARY KEY,
    credential_id BLOB NOT NULL,
    public_key    BLOB NOT NULL,
    sign_count    INTEGER NOT NULL DEFAULT 0,
    aaguid        TEXT NOT NULL DEFAULT '',
    created_at    TEXT NOT NULL
);`
	if _, err := db.Exec(schema); err != nil {
		db.Close()
		return nil, fmt.Errorf("webauthn: schema: %w", err)
	}
	// Enforce 0600 on the DB file.
	if err := os.Chmod(dbPath, 0o600); err != nil {
		// Non-fatal: the file may not exist yet (WAL mode creates on first write).
		_ = err
	}
	return &Store{db: db, path: dbPath}, nil
}

// PutCredential stores a credential (insert or replace by user_id).
func (s *Store) PutCredential(c *Credential) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	_, err := s.db.Exec(
		`INSERT INTO credentials (user_id, credential_id, public_key, sign_count, aaguid, created_at)
		 VALUES (?, ?, ?, ?, ?, ?)
		 ON CONFLICT(user_id) DO UPDATE SET
		   credential_id = excluded.credential_id,
		   public_key = excluded.public_key,
		   sign_count = excluded.sign_count`,
		c.UserID, c.CredentialID, c.PublicKey, c.SignCount, c.AAGUID, c.CreatedAt.Format(time.RFC3339),
	)
	if err != nil {
		return fmt.Errorf("webauthn: put: %w", err)
	}
	return nil
}

// GetCredential retrieves a credential by user_id.
func (s *Store) GetCredential(userID string) (*Credential, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	var c Credential
	var createdStr string
	err := s.db.QueryRow(
		`SELECT user_id, credential_id, public_key, sign_count, aaguid, created_at
		 FROM credentials WHERE user_id = ?`, userID,
	).Scan(&c.UserID, &c.CredentialID, &c.PublicKey, &c.SignCount, &c.AAGUID, &createdStr)
	if err == sql.ErrNoRows {
		return nil, nil
	}
	if err != nil {
		return nil, fmt.Errorf("webauthn: get: %w", err)
	}
	c.CreatedAt, _ = time.Parse(time.RFC3339, createdStr)
	return &c, nil
}

// ListCredentials returns all stored credentials (for admin/debug).
func (s *Store) ListCredentials() ([]*Credential, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	rows, err := s.db.Query(
		`SELECT user_id, credential_id, public_key, sign_count, aaguid, created_at
		 FROM credentials ORDER BY created_at`)
	if err != nil {
		return nil, fmt.Errorf("webauthn: list: %w", err)
	}
	defer rows.Close()
	var out []*Credential
	for rows.Next() {
		var c Credential
		var createdStr string
		if err := rows.Scan(&c.UserID, &c.CredentialID, &c.PublicKey, &c.SignCount, &c.AAGUID, &createdStr); err != nil {
			return nil, err
		}
		c.CreatedAt, _ = time.Parse(time.RFC3339, createdStr)
		out = append(out, &c)
	}
	return out, nil
}

// DeleteCredential removes a credential (revoke a passkey).
func (s *Store) DeleteCredential(userID string) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	_, err := s.db.Exec(`DELETE FROM credentials WHERE user_id = ?`, userID)
	if err != nil {
		return fmt.Errorf("webauthn: delete: %w", err)
	}
	return nil
}

// UpdateSignCount updates the sign count after a successful login.
func (s *Store) UpdateSignCount(userID string, count uint32) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	_, err := s.db.Exec(`UPDATE credentials SET sign_count = ? WHERE user_id = ?`, count, userID)
	if err != nil {
		return fmt.Errorf("webauthn: update count: %w", err)
	}
	return nil
}

// Close closes the DB.
func (s *Store) Close() error { return s.db.Close() }

// EncodeID base64-encodes a credential ID for transport.
func EncodeID(id []byte) string {
	return base64.RawURLEncoding.EncodeToString(id)
}

// DecodeID base64-decodes a credential ID.
func DecodeID(s string) ([]byte, error) {
	return base64.RawURLEncoding.DecodeString(s)
}

// User represents a WebAuthn user (for the ceremony).
type User struct {
	ID          []byte
	Name         string
	DisplayName  string
	Credentials  []*Credential
}

// ToJSON marshals a value for the connector response.
func ToJSON(v any) ([]byte, error) {
	return json.Marshal(v)
}
