package db

import (
	"database/sql"
	"time"
)

// ExecutorToken is a registered mill executor identity. The raw token is never
// stored; token_hash is a hash of it (see mill.HashToken). ExpiresAt is nil for
// a token that never expires.
type ExecutorToken struct {
	Name      string
	CreatedAt string
	ExpiresAt *time.Time
}

// AddExecutorToken registers a new executor identity. expiresAt is optional (nil
// means the token never expires). It fails if the name is already taken.
func (d *DB) AddExecutorToken(name, tokenHash string, expiresAt *time.Time) error {
	_, err := d.Exec(
		`insert into mill_executors (name, token_hash, expires_at) values (?, ?, ?)`,
		name, tokenHash, expiryArg(expiresAt),
	)
	return err
}

// UpsertExecutorToken registers or replaces an executor identity's token with no
// expiry. Used for the dev bootstrap seed; production registers per-executor
// tokens via AddExecutorToken.
func (d *DB) UpsertExecutorToken(name, tokenHash string) error {
	_, err := d.Exec(
		`insert into mill_executors (name, token_hash, expires_at) values (?, ?, null)
		 on conflict(name) do update set token_hash = excluded.token_hash, expires_at = null`,
		name, tokenHash,
	)
	return err
}

// ResolveExecutorToken returns the executor name a token hash is registered
// under. ok is false when no identity matches or the token has expired.
func (d *DB) ResolveExecutorToken(tokenHash string) (string, bool, error) {
	var name string
	var expires sql.NullString
	err := d.QueryRow(
		`select name, expires_at from mill_executors where token_hash = ?`, tokenHash,
	).Scan(&name, &expires)
	if err == sql.ErrNoRows {
		return "", false, nil
	}
	if err != nil {
		return "", false, err
	}
	if exp, ok := parseExpiry(expires); ok && time.Now().After(exp) {
		return "", false, nil
	}
	return name, true, nil
}

// RevokeExecutorToken removes an executor identity, returning whether a row was
// deleted.
func (d *DB) RevokeExecutorToken(name string) (bool, error) {
	res, err := d.Exec(`delete from mill_executors where name = ?`, name)
	if err != nil {
		return false, err
	}
	n, err := res.RowsAffected()
	return n > 0, err
}

// ListExecutorTokens returns the registered executor identities ordered by name.
func (d *DB) ListExecutorTokens() ([]ExecutorToken, error) {
	rows, err := d.Query(`select name, created_at, expires_at from mill_executors order by name`)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var out []ExecutorToken
	for rows.Next() {
		var t ExecutorToken
		var expires sql.NullString
		if err := rows.Scan(&t.Name, &t.CreatedAt, &expires); err != nil {
			return nil, err
		}
		if exp, ok := parseExpiry(expires); ok {
			t.ExpiresAt = &exp
		}
		out = append(out, t)
	}
	return out, rows.Err()
}

// expiryArg renders an optional expiry as a sql argument (nil -> NULL).
func expiryArg(t *time.Time) any {
	if t == nil {
		return nil
	}
	return t.UTC().Format(time.RFC3339)
}

// parseExpiry decodes a stored expiry column; ok is false for NULL or unparseable.
func parseExpiry(s sql.NullString) (time.Time, bool) {
	if !s.Valid || s.String == "" {
		return time.Time{}, false
	}
	t, err := time.Parse(time.RFC3339, s.String)
	if err != nil {
		return time.Time{}, false
	}
	return t, true
}
