package main

import (
	"crypto/rand"
	"crypto/sha256"
	"database/sql"
	"encoding/hex"
	"errors"
	"flag"
	"fmt"
)

// MCPToken is a per-user access token used by remote Claude clients to talk to
// the kanban MCP HTTP endpoint. The plaintext value is shown ONCE at creation
// time; we only persist the SHA-256 hash.
type MCPToken struct {
	ID         string  `json:"id"`
	Name       string  `json:"name"`
	CreatedAt  string  `json:"created_at"`
	LastUsedAt *string `json:"last_used_at,omitempty"`
}

const mcpTokenPrefix = "kmcp_"

var errMCPTokenNotFound = errors.New("mcp token not found")

// MintMCPToken creates a new active token for userID and returns the plaintext
// value (caller must surface it to the user immediately; it cannot be
// recovered later) along with the row metadata.
func (db *DB) MintMCPToken(userID, name string) (string, *MCPToken, error) {
	if userID == "" {
		return "", nil, fmt.Errorf("user_id required")
	}
	plaintext, err := generateMCPTokenPlaintext()
	if err != nil {
		return "", nil, fmt.Errorf("generate token: %w", err)
	}
	tok := &MCPToken{
		ID:        newID(),
		Name:      name,
		CreatedAt: nowRFC3339(),
	}
	_, err = db.conn.Exec(
		`INSERT INTO mcp_tokens (id, user_id, token_hash, name, created_at) VALUES (?, ?, ?, ?, ?)`,
		tok.ID, userID, hashMCPToken(plaintext), tok.Name, tok.CreatedAt,
	)
	if err != nil {
		return "", nil, err
	}
	return plaintext, tok, nil
}

func (db *DB) ListMCPTokens(userID string) ([]MCPToken, error) {
	rows, err := db.conn.Query(
		`SELECT id, name, created_at, last_used_at FROM mcp_tokens
		 WHERE user_id=? AND revoked_at IS NULL
		 ORDER BY created_at DESC`, userID,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	out := []MCPToken{}
	for rows.Next() {
		var t MCPToken
		var lastUsed sql.NullString
		if err := rows.Scan(&t.ID, &t.Name, &t.CreatedAt, &lastUsed); err != nil {
			return nil, err
		}
		if lastUsed.Valid {
			t.LastUsedAt = &lastUsed.String
		}
		out = append(out, t)
	}
	return out, rows.Err()
}

// RevokeMCPToken sets revoked_at on the token belonging to userID. Returns
// errMCPTokenNotFound if no active row matches.
func (db *DB) RevokeMCPToken(userID, tokenID string) error {
	res, err := db.conn.Exec(
		`UPDATE mcp_tokens SET revoked_at=? WHERE id=? AND user_id=? AND revoked_at IS NULL`,
		nowRFC3339(), tokenID, userID,
	)
	if err != nil {
		return err
	}
	n, err := res.RowsAffected()
	if err != nil {
		return err
	}
	if n == 0 {
		return errMCPTokenNotFound
	}
	return nil
}

// LookupMCPToken hashes plaintext and returns the owning user_id if the token
// is active. Updates last_used_at as a side effect. Returns "" + nil when the
// token does not match an active row.
func (db *DB) LookupMCPToken(plaintext string) (string, error) {
	if plaintext == "" {
		return "", nil
	}
	hash := hashMCPToken(plaintext)
	var userID, id string
	err := db.conn.QueryRow(
		`SELECT id, user_id FROM mcp_tokens WHERE token_hash=? AND revoked_at IS NULL`, hash,
	).Scan(&id, &userID)
	if errors.Is(err, sql.ErrNoRows) {
		return "", nil
	}
	if err != nil {
		return "", err
	}
	if _, err := db.conn.Exec(`UPDATE mcp_tokens SET last_used_at=? WHERE id=?`, nowRFC3339(), id); err != nil {
		return userID, fmt.Errorf("touch last_used_at: %w", err)
	}
	return userID, nil
}

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

func generateMCPTokenPlaintext() (string, error) {
	b := make([]byte, 32)
	if _, err := rand.Read(b); err != nil {
		return "", err
	}
	return mcpTokenPrefix + hex.EncodeToString(b), nil
}

// runMintToken implements `kanban mint-token --user <id> --name <pc>`.
// Generates a fresh token, persists its sha256 in mcp_tokens, and prints the
// plaintext ONCE to stdout. The caller must save it — the server keeps only
// the hash.
func runMintToken(args []string) error {
	fs := flag.NewFlagSet("kanban mint-token", flag.ContinueOnError)
	dbPath := fs.String("db", "operations.db", "SQLite database path")
	userID := fs.String("user", "", "owner user_id (must exist in users table)")
	name := fs.String("name", "", "label for this token (e.g. PC name)")
	if err := fs.Parse(args); err != nil {
		return err
	}
	if *userID == "" || *name == "" {
		return fmt.Errorf("--user and --name required")
	}

	db, err := openDB(*dbPath)
	if err != nil {
		return fmt.Errorf("open db: %w", err)
	}
	defer db.Close()

	var exists int
	if err := db.conn.QueryRow(`SELECT COUNT(*) FROM users WHERE id=?`, *userID).Scan(&exists); err != nil {
		return fmt.Errorf("user lookup: %w", err)
	}
	if exists == 0 {
		return fmt.Errorf("user %q not found", *userID)
	}

	plaintext, tok, err := db.MintMCPToken(*userID, *name)
	if err != nil {
		return fmt.Errorf("mint: %w", err)
	}
	fmt.Printf("token id:   %s\n", tok.ID)
	fmt.Printf("name:       %s\n", tok.Name)
	fmt.Printf("created_at: %s\n", tok.CreatedAt)
	fmt.Printf("\ntoken (save now, will not be shown again):\n%s\n", plaintext)
	return nil
}
