package auth

import (
	"crypto/sha256"
	"database/sql"
	"encoding/hex"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"time"

	_ "modernc.org/sqlite"
)

// tokenPrefix marks a Nadir machine credential, like GitHub's "ghp_". It's
// cosmetic (helps secret scanners and humans recognize a leaked token), not a
// security boundary - the prefix is hashed and stored along with the rest.
const tokenPrefix = "nad_"

// TokenStore persists machine credentials for non-interactive callers (a
// central dashboard managing N nodes), so they authenticate with a static
// Bearer token instead of a per-host PAM session. Only the SHA-256 of each
// token is stored - a leaked DB or backup can't hand out live credentials.
//
// It lives in its own SQLite file because both the server (read, on every
// Bearer request) and the `nadir token` CLI (write, when minting/revoking)
// touch it. WAL + a busy timeout let those two processes share the file without
// "database is locked".
type TokenStore struct {
	db *sql.DB
}

// TokenInfo is one stored credential's public metadata (never the secret).
type TokenInfo struct {
	Name    string
	Created time.Time
}

// NewTokenStore opens (creating if needed) the token database at path.
func NewTokenStore(path string) (*TokenStore, error) {
	if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
		return nil, fmt.Errorf("token db dir: %w", err)
	}
	// WAL + busy_timeout: the server process and the CLI process both open this
	// file, so a plain rollback journal would surface transient lock errors.
	dsn := path + "?_pragma=busy_timeout(5000)&_pragma=journal_mode(WAL)"
	db, err := sql.Open("sqlite", dsn)
	if err != nil {
		return nil, fmt.Errorf("open token db: %w", err)
	}
	if _, err := db.Exec(`CREATE TABLE IF NOT EXISTS tokens (
		name       TEXT PRIMARY KEY,
		token_hash TEXT NOT NULL UNIQUE,
		created_at INTEGER NOT NULL
	)`); err != nil {
		return nil, fmt.Errorf("create tokens table: %w", err)
	}
	if err := os.Chmod(path, 0600); err != nil {
		return nil, fmt.Errorf("chmod token db: %w", err)
	}
	return &TokenStore{db: db}, nil
}

func (s *TokenStore) Close() error { return s.db.Close() }

// Create mints a new token named name and returns the raw secret. The secret is
// shown only here, at generation time - only its hash is stored. A duplicate
// name is rejected (the caller should revoke and re-mint to rotate).
func (s *TokenStore) Create(name string) (string, error) {
	if name == "" {
		return "", fmt.Errorf("token name required")
	}
	raw := tokenPrefix + randomToken()
	if _, err := s.db.Exec(
		`INSERT INTO tokens (name, token_hash, created_at) VALUES (?, ?, ?)`,
		name, hashToken(raw), time.Now().Unix(),
	); err != nil {
		return "", fmt.Errorf("store token %q (already exists?): %w", name, err)
	}
	return raw, nil
}

// Lookup returns the token name for a presented raw secret.
//
// No constant-time compare is needed: we look the secret up by its SHA-256, so
// the value compared in the index is already a hash of attacker-controlled
// input. A timing side-channel could at most leak a stored hash, which is
// useless without a SHA-256 preimage (the actual token).
func (s *TokenStore) Lookup(raw string) (string, bool) {
	if !strings.HasPrefix(raw, tokenPrefix) {
		return "", false
	}
	var name string
	err := s.db.QueryRow(
		`SELECT name FROM tokens WHERE token_hash = ?`, hashToken(raw),
	).Scan(&name)
	if err != nil {
		return "", false
	}
	return name, true
}

// Delete revokes a token by name. Revoking an unknown name is a no-op. The
// change is effective immediately - the server reads this DB live, no restart.
func (s *TokenStore) Delete(name string) error {
	_, err := s.db.Exec(`DELETE FROM tokens WHERE name = ?`, name)
	return err
}

// List returns all token names with their creation time, newest first.
func (s *TokenStore) List() ([]TokenInfo, error) {
	rows, err := s.db.Query(`SELECT name, created_at FROM tokens ORDER BY created_at DESC`)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	infos := []TokenInfo{}
	for rows.Next() {
		var t TokenInfo
		var created int64
		if err := rows.Scan(&t.Name, &created); err != nil {
			return nil, err
		}
		t.Created = time.Unix(created, 0).UTC()
		infos = append(infos, t)
	}
	return infos, rows.Err()
}

func hashToken(raw string) string {
	sum := sha256.Sum256([]byte(raw))
	return hex.EncodeToString(sum[:])
}
