package auth

import (
	"context"
	"database/sql"
	"encoding/base64"
	"encoding/json"
	"errors"
	"time"

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

// ErrCredentialNotFound signals that no credential with the given id is
// stored. Callers map this to a 401.
var ErrCredentialNotFound = errors.New("auth: credential not found")

// CredentialStore wraps the webauthn_credentials table.
type CredentialStore struct {
	db *sql.DB
}

// NewCredentialStore constructs the store.
func NewCredentialStore(db *sql.DB) *CredentialStore {
	return &CredentialStore{db: db}
}

// Count returns the number of stored credentials. Used by the bootstrap
// check (first registration is unauthenticated; subsequent registrations
// require an active session).
func (s *CredentialStore) Count(ctx context.Context) (int, error) {
	var n int
	err := s.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM webauthn_credentials`).Scan(&n)
	return n, err
}

// All returns every stored credential as a webauthn.Credential. The library
// uses these to populate the "allowed credentials" list during login and to
// check the signature counter against replay.
//
// BE/BS flags are persisted (migration 013) so go-webauthn's assertion-time
// flag-consistency check passes for credentials that move between devices
// via a password manager. Without persistence the flags loaded zero and
// every cross-device login from a synced credential failed with
// "Backup Eligible flag inconsistency". See DECISIONS.md D50.
func (s *CredentialStore) All(ctx context.Context) ([]webauthn.Credential, error) {
	rows, err := s.db.QueryContext(ctx, `
		SELECT id, public_key, counter, transports, backup_eligible, backup_state, attachment
		FROM webauthn_credentials
	`)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var out []webauthn.Credential
	for rows.Next() {
		var (
			idB64           string
			publicKey       []byte
			counter         int64
			transports      sql.NullString
			backupEligible  int
			backupState     int
			attachment      sql.NullString
		)
		if err := rows.Scan(&idB64, &publicKey, &counter, &transports, &backupEligible, &backupState, &attachment); err != nil {
			return nil, err
		}
		idBytes, err := base64.RawURLEncoding.DecodeString(idB64)
		if err != nil {
			return nil, err
		}
		cred := webauthn.Credential{
			ID:        idBytes,
			PublicKey: publicKey,
		}
		// AuthenticatorData carries the counter; we set it on the inner
		// Authenticator field for the library's counter check.
		cred.Authenticator.SignCount = uint32(counter)
		cred.Flags.BackupEligible = backupEligible == 1
		cred.Flags.BackupState = backupState == 1
		if attachment.Valid {
			cred.Authenticator.Attachment = protocolAuthenticatorAttachment(attachment.String)
		}
		if transports.Valid {
			var ts []string
			if err := json.Unmarshal([]byte(transports.String), &ts); err == nil {
				cred.Transport = parseTransports(ts)
			}
		}
		out = append(out, cred)
	}
	return out, rows.Err()
}

// Insert persists a freshly created credential, including BE/BS flags and
// authenticator attachment so multi-device assertion validation works (D50).
func (s *CredentialStore) Insert(ctx context.Context, cred *webauthn.Credential, name string) error {
	transports, _ := json.Marshal(transportsToStrings(cred.Transport))
	backupEligible := 0
	if cred.Flags.BackupEligible {
		backupEligible = 1
	}
	backupState := 0
	if cred.Flags.BackupState {
		backupState = 1
	}
	_, err := s.db.ExecContext(ctx, `
		INSERT INTO webauthn_credentials(id, public_key, counter, transports, name, created_at, backup_eligible, backup_state, attachment)
		VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
	`,
		base64.RawURLEncoding.EncodeToString(cred.ID),
		cred.PublicKey,
		int64(cred.Authenticator.SignCount),
		string(transports),
		nullableText(name),
		time.Now().Unix(),
		backupEligible,
		backupState,
		nullableText(string(cred.Authenticator.Attachment)),
	)
	return err
}

// UpdateCounter records a new signature counter after successful login.
// Protects against authenticator cloning replays per WebAuthn spec.
func (s *CredentialStore) UpdateCounter(ctx context.Context, credID []byte, newCounter uint32) error {
	idB64 := base64.RawURLEncoding.EncodeToString(credID)
	_, err := s.db.ExecContext(ctx,
		`UPDATE webauthn_credentials SET counter = ?, last_used_at = ? WHERE id = ?`,
		int64(newCounter), time.Now().Unix(), idB64)
	return err
}

// Delete removes a credential by its base64url id. Returns ErrCredentialNotFound
// if no row was deleted.
func (s *CredentialStore) Delete(ctx context.Context, idB64 string) error {
	res, err := s.db.ExecContext(ctx, `DELETE FROM webauthn_credentials WHERE id = ?`, idB64)
	if err != nil {
		return err
	}
	n, _ := res.RowsAffected()
	if n == 0 {
		return ErrCredentialNotFound
	}
	return nil
}

// CredentialSummary is the JSON shape returned by GET /api/auth/devices.
type CredentialSummary struct {
	ID         string `json:"id"`
	Name       string `json:"name,omitempty"`
	CreatedAt  int64  `json:"created_at"`
	LastUsedAt *int64 `json:"last_used_at,omitempty"`
}

// List returns operator-facing summaries (no public key bytes).
func (s *CredentialStore) List(ctx context.Context) ([]CredentialSummary, error) {
	rows, err := s.db.QueryContext(ctx, `
		SELECT id, name, created_at, last_used_at FROM webauthn_credentials
		ORDER BY created_at
	`)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	out := []CredentialSummary{}
	for rows.Next() {
		var (
			c    CredentialSummary
			name sql.NullString
			last sql.NullInt64
		)
		if err := rows.Scan(&c.ID, &name, &c.CreatedAt, &last); err != nil {
			return nil, err
		}
		if name.Valid {
			c.Name = name.String
		}
		if last.Valid {
			v := last.Int64
			c.LastUsedAt = &v
		}
		out = append(out, c)
	}
	return out, rows.Err()
}
