package credentialstore

import (
	"context"
	"crypto/rand"
	"crypto/sha256"
	"database/sql"
	"encoding/hex"
	"errors"
	"fmt"
	"strings"
	"time"

	"github.com/google/uuid"
)

// Status values for principal tokens. These are the only values accepted by
// the CHECK constraint on tokens.status.
const (
	StatusActive   = "active"
	StatusDisabled = "disabled"
	StatusRevoked  = "revoked"
)

// CreatePrincipalInput is the caller-provided payload for CreatePrincipalWithToken.
type CreatePrincipalInput struct {
	// Alias is a human-readable name for the principal. It must be unique
	// across all principals and is used for display and management.
	Alias string
}

// IssuedPrincipal is the one-time response from CreatePrincipalWithToken.
// RawToken is returned exactly once to the issuer and must be delivered to
// the corresponding Edge configuration before the reference is lost. The
// TokenRecord embedded in the response contains only the digest; the raw
// value is never persisted.
type IssuedPrincipal struct {
	Principal PrincipalRecord
	Token     TokenRecord
	RawToken  string
}

// IssuedToken is the one-time response when generating an additional token for an existing principal.
type IssuedToken struct {
	Token    TokenRecord
	RawToken string
}

// PrincipalRecord is the persisted principal row returned by queries.
type PrincipalRecord struct {
	ID        string
	Alias     string
	CreatedAt time.Time
	UpdatedAt time.Time
}

// TokenRecord is the persisted token row returned by queries. It never
// contains the raw token value; only the SHA-256 digest hex string.
type TokenRecord struct {
	ID          string
	PrincipalID string
	TokenRef    string
	Digest      string
	Status      string
	Revision    int64
	CreatedAt   time.Time
	UpdatedAt   time.Time
	RevokedAt   *time.Time
}

// ErrPrincipalAlreadyExists is returned when CreatePrincipalWithToken is
// called with an alias that is already in use.
var ErrPrincipalAlreadyExists = errors.New("credentialstore: principal alias already exists")

// ErrPrincipalNotFound is returned when a principal lookup finds no matching row.
var ErrPrincipalNotFound = errors.New("credentialstore: principal not found")

// ErrTokenNotFound is returned when a token lookup finds no matching row.
var ErrTokenNotFound = errors.New("credentialstore: token not found")

// ErrTokenNotActive is returned when an operation requires an active token.
var ErrTokenNotActive = errors.New("credentialstore: token is not active")

// ErrRevisionMismatch is returned by CAS operations when the provided
// revision does not match the current row revision.
var ErrRevisionMismatch = errors.New("credentialstore: revision mismatch")

// ErrTokenRevoked is returned by re-enable attempts on a revoked token.
var ErrTokenRevoked = errors.New("credentialstore: revoked token cannot be re-enabled")

// generateRawToken produces 32 random bytes encoded as a hex string.
// This is the only place the plaintext token exists; it is returned to the
// caller and never persisted.
func generateRawToken() (string, error) {
	b := make([]byte, 32)
	if _, err := rand.Read(b); err != nil {
		return "", fmt.Errorf("credentialstore: generate random: %w", err)
	}
	return hex.EncodeToString(b), nil
}

// digestOf returns the SHA-256 hex digest of raw.
func digestOf(raw string) string {
	sum := sha256.Sum256([]byte(raw))
	return hex.EncodeToString(sum[:])
}

// CreatePrincipalWithToken atomically creates a principal and an active
// token. The raw token is returned exactly once; the persisted TokenRecord
// contains only the SHA-256 digest.
//
// If the alias already exists, ErrPrincipalAlreadyExists is returned and no
// row is modified.
func (s *Store) CreatePrincipalWithToken(ctx context.Context, in CreatePrincipalInput) (*IssuedPrincipal, error) {
	if in.Alias == "" {
		return nil, fmt.Errorf("credentialstore: CreatePrincipalWithToken: alias is required")
	}
	now := time.Now().UTC()
	raw, err := generateRawToken()
	if err != nil {
		return nil, err
	}
	dig := digestOf(raw)
	principalID := uuid.New().String()
	tokenID := uuid.New().String()

	var issued IssuedPrincipal
	err = s.RunInTransaction(ctx, func(ctx context.Context, tx *sql.Tx) error {
		// Check for duplicate alias inside the transaction.
		var count int
		if err := tx.QueryRowContext(ctx, s.bind(`SELECT COUNT(*) FROM principals WHERE alias=?`), in.Alias).Scan(&count); err != nil {
			return fmt.Errorf("credentialstore: check alias: %w", err)
		}
		if count > 0 {
			return ErrPrincipalAlreadyExists
		}

		// Insert principal.
		if _, err := tx.ExecContext(ctx, s.bind(
			`INSERT INTO principals (id, alias, created_at, updated_at) VALUES (?,?,?,?)`),
			principalID, in.Alias, now, now,
		); err != nil {
			return fmt.Errorf("credentialstore: insert principal: %w", err)
		}

		// Insert token with digest only.
		if _, err := tx.ExecContext(ctx, s.bind(
			`INSERT INTO tokens (id, principal_id, token_ref, digest, status, revision, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)`),
			tokenID, principalID, tokenRefFor(dig), dig, StatusActive, 0, now, now,
		); err != nil {
			return fmt.Errorf("credentialstore: insert token: %w", err)
		}
		if err := s.bumpProjectionGenerationTx(ctx, tx); err != nil {
			return err
		}

		issued = IssuedPrincipal{
			Principal: PrincipalRecord{ID: principalID, Alias: in.Alias, CreatedAt: now, UpdatedAt: now},
			Token: TokenRecord{
				ID: tokenID, PrincipalID: principalID, TokenRef: tokenRefFor(dig),
				Digest: dig, Status: StatusActive, Revision: 0, CreatedAt: now, UpdatedAt: now,
			},
			RawToken: raw,
		}
		return nil
	})
	if err != nil {
		return nil, err
	}
	return &issued, nil
}

// CreateFirstPrincipalWithToken atomically creates the first principal and its
// active token. It locks the projection state singleton row to serialize
// concurrent callers, verifies the principal store is currently empty, and
// rejects any non-empty store with ErrPrincipalAlreadyExists.
func (s *Store) CreateFirstPrincipalWithToken(ctx context.Context, in CreatePrincipalInput) (*IssuedPrincipal, error) {
	if in.Alias == "" {
		return nil, fmt.Errorf("credentialstore: CreateFirstPrincipalWithToken: alias is required")
	}
	now := time.Now().UTC()
	raw, err := generateRawToken()
	if err != nil {
		return nil, err
	}
	dig := digestOf(raw)
	principalID := uuid.New().String()
	tokenID := uuid.New().String()

	var issued IssuedPrincipal
	err = s.RunInTransaction(ctx, func(ctx context.Context, tx *sql.Tx) error {
		res, err := tx.ExecContext(ctx, s.bind(`UPDATE principal_projection_state SET generation=generation WHERE singleton=1`))
		if err != nil {
			return fmt.Errorf("credentialstore: acquire first principal lock: %w", err)
		}
		affected, err := res.RowsAffected()
		if err != nil {
			return fmt.Errorf("credentialstore: inspect first principal lock: %w", err)
		}
		if affected != 1 {
			return ErrProjectionGenerationMissing
		}

		var count int
		if err := tx.QueryRowContext(ctx, s.bind(`SELECT COUNT(*) FROM principals`)).Scan(&count); err != nil {
			return fmt.Errorf("credentialstore: count principals: %w", err)
		}
		if count > 0 {
			return ErrPrincipalAlreadyExists
		}

		if _, err := tx.ExecContext(ctx, s.bind(
			`INSERT INTO principals (id, alias, created_at, updated_at) VALUES (?,?,?,?)`),
			principalID, in.Alias, now, now,
		); err != nil {
			return fmt.Errorf("credentialstore: insert principal: %w", err)
		}

		if _, err := tx.ExecContext(ctx, s.bind(
			`INSERT INTO tokens (id, principal_id, token_ref, digest, status, revision, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)`),
			tokenID, principalID, tokenRefFor(dig), dig, StatusActive, 0, now, now,
		); err != nil {
			return fmt.Errorf("credentialstore: insert token: %w", err)
		}
		if err := s.bumpProjectionGenerationTx(ctx, tx); err != nil {
			return err
		}

		issued = IssuedPrincipal{
			Principal: PrincipalRecord{ID: principalID, Alias: in.Alias, CreatedAt: now, UpdatedAt: now},
			Token: TokenRecord{
				ID: tokenID, PrincipalID: principalID, TokenRef: tokenRefFor(dig),
				Digest: dig, Status: StatusActive, Revision: 0, CreatedAt: now, UpdatedAt: now,
			},
			RawToken: raw,
		}
		return nil
	})
	if err != nil {
		return nil, err
	}
	return &issued, nil
}

// CreateToken atomically generates and inserts an active token for an existing principal.
// The raw token is returned exactly once in IssuedToken; the persisted TokenRecord contains only the SHA-256 digest.
func (s *Store) CreateToken(ctx context.Context, principalID string) (*IssuedToken, error) {
	if principalID == "" {
		return nil, fmt.Errorf("credentialstore: CreateToken: principalID is required")
	}
	now := time.Now().UTC()
	raw, err := generateRawToken()
	if err != nil {
		return nil, err
	}
	dig := digestOf(raw)
	tokenID := uuid.New().String()

	var issued IssuedToken
	err = s.RunInTransaction(ctx, func(ctx context.Context, tx *sql.Tx) error {
		var count int
		if err := tx.QueryRowContext(ctx, s.bind(`SELECT COUNT(*) FROM principals WHERE id=?`), principalID).Scan(&count); err != nil {
			return fmt.Errorf("credentialstore: check principal: %w", err)
		}
		if count == 0 {
			return ErrPrincipalNotFound
		}

		if _, err := tx.ExecContext(ctx, s.bind(
			`INSERT INTO tokens (id, principal_id, token_ref, digest, status, revision, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)`),
			tokenID, principalID, tokenRefFor(dig), dig, StatusActive, 0, now, now,
		); err != nil {
			return fmt.Errorf("credentialstore: insert token: %w", err)
		}
		if err := s.bumpProjectionGenerationTx(ctx, tx); err != nil {
			return err
		}

		issued = IssuedToken{
			Token: TokenRecord{
				ID:          tokenID,
				PrincipalID: principalID,
				TokenRef:    tokenRefFor(dig),
				Digest:      dig,
				Status:      StatusActive,
				Revision:    0,
				CreatedAt:   now,
				UpdatedAt:   now,
			},
			RawToken: raw,
		}
		return nil
	})
	if err != nil {
		return nil, err
	}
	return &issued, nil
}

// ListTokens returns all tokens for a given principal.
func (s *Store) ListTokens(ctx context.Context, principalID string) ([]TokenRecord, error) {
	rows, err := s.db.QueryContext(ctx, s.bind(`
		SELECT id, principal_id, token_ref, digest, status, revision, created_at, updated_at, revoked_at
		FROM tokens
		WHERE principal_id=?
		ORDER BY created_at ASC, id ASC
	`), principalID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var out []TokenRecord
	for rows.Next() {
		var t TokenRecord
		var createdAt, updatedAt string
		var revokedAt sql.NullString
		if err := rows.Scan(&t.ID, &t.PrincipalID, &t.TokenRef, &t.Digest, &t.Status, &t.Revision, &createdAt, &updatedAt, &revokedAt); err != nil {
			return nil, err
		}
		t.CreatedAt, _ = parseTime(createdAt)
		t.UpdatedAt, _ = parseTime(updatedAt)
		if revokedAt.Valid {
			parsed, _ := parseTime(revokedAt.String)
			t.RevokedAt = &parsed
		}
		out = append(out, t)
	}
	return out, rows.Err()
}

// tokenRefFor derives a stable, opaque token reference from a digest. It is a
// safe identifier for metadata and projections; callers authenticate with the
// one-time raw token, never with this reference or the digest itself.
func tokenRefFor(digest string) string {
	return "tok_" + digest[:16]
}

// GetPrincipal returns the principal with the given alias, or (nil, nil) if
// no such principal exists.
func (s *Store) GetPrincipal(ctx context.Context, alias string) (*PrincipalRecord, error) {
	var r PrincipalRecord
	var createdAt, updatedAt string
	err := s.db.QueryRowContext(ctx, s.bind(
		`SELECT id, alias, created_at, updated_at FROM principals WHERE alias=?`), alias,
	).Scan(&r.ID, &r.Alias, &createdAt, &updatedAt)
	if err != nil {
		if err == sql.ErrNoRows {
			return nil, nil
		}
		return nil, err
	}
	r.CreatedAt, _ = time.Parse(time.RFC3339, createdAt)
	r.UpdatedAt, _ = time.Parse(time.RFC3339, updatedAt)
	return &r, nil
}

// ListPrincipals returns all principals with their active token metadata.
func (s *Store) ListPrincipals(ctx context.Context) ([]PrincipalWithToken, error) {
	rows, err := s.db.QueryContext(ctx, `
		SELECT p.id, p.alias, p.created_at, p.updated_at,
		       t.id AS token_id, t.token_ref, t.digest, t.status, t.revision, t.created_at AS token_created_at, t.updated_at AS token_updated_at, t.revoked_at
		FROM principals p
		LEFT JOIN tokens t ON t.principal_id = p.id AND t.status = 'active'
		ORDER BY p.alias
	`)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var out []PrincipalWithToken
	for rows.Next() {
		var p PrincipalRecord
		var t TokenRecord
		var principalCreatedAt, principalUpdatedAt string
		var tokenID, tokenRef, tokenDigest, tokenStatus sql.NullString
		var tokenRevision sql.NullInt64
		var tokenCreatedAt, tokenUpdatedAt, tokenRevokedAt sql.NullString
		if err := rows.Scan(&p.ID, &p.Alias, &principalCreatedAt, &principalUpdatedAt,
			&tokenID, &tokenRef, &tokenDigest, &tokenStatus, &tokenRevision,
			&tokenCreatedAt, &tokenUpdatedAt, &tokenRevokedAt); err != nil {
			return nil, err
		}
		p.CreatedAt, _ = parseTime(principalCreatedAt)
		p.UpdatedAt, _ = parseTime(principalUpdatedAt)
		if tokenID.Valid {
			t.ID = tokenID.String
			t.PrincipalID = p.ID
			t.TokenRef = tokenRef.String
			t.Digest = tokenDigest.String
			t.Status = tokenStatus.String
			t.Revision = tokenRevision.Int64
			t.CreatedAt, _ = time.Parse(time.RFC3339, tokenCreatedAt.String)
			t.UpdatedAt, _ = time.Parse(time.RFC3339, tokenUpdatedAt.String)
			if tokenRevokedAt.Valid {
				timestamp := tokenRevokedAt.String
				parsed, err := time.Parse(time.RFC3339, timestamp)
				if err != nil {
					parsed, _ = time.Parse("2006-01-02T15:04:05.999999999Z", timestamp)
				}
				t.RevokedAt = &parsed
			}
		}
		out = append(out, PrincipalWithToken{Principal: p, Token: t})
	}
	return out, rows.Err()
}

// PrincipalWithToken pairs a principal record with its active token metadata.
type PrincipalWithToken struct {
	Principal PrincipalRecord
	Token     TokenRecord
}

// LookupTokenByDigest verifies a token digest against the store and returns
// the associated principal and token metadata. It is the authentication
// lookup path used by Edge projection validators.
//
// Only active tokens are returned; disabled and revoked tokens are hidden and
// yield ErrTokenNotFound just like an unknown digest.
func (s *Store) LookupTokenByDigest(ctx context.Context, digest string) (*PrincipalRecord, *TokenRecord, error) {
	var p PrincipalRecord
	var t TokenRecord
	var principalCreatedAt, principalUpdatedAt, tokenCreatedAt, tokenUpdatedAt string
	var revokedAt sql.NullString
	err := s.db.QueryRowContext(ctx, s.bind(`
		SELECT p.id, p.alias, p.created_at, p.updated_at,
		       t.id, t.token_ref, t.digest, t.status, t.revision, t.created_at, t.updated_at, t.revoked_at
		FROM principals p
		JOIN tokens t ON t.principal_id = p.id
		WHERE t.digest = ? AND t.status = ?
	`), digest, StatusActive).Scan(
		&p.ID, &p.Alias, &principalCreatedAt, &principalUpdatedAt,
		&t.ID, &t.TokenRef, &t.Digest, &t.Status, &t.Revision, &tokenCreatedAt, &tokenUpdatedAt, &revokedAt,
	)
	if err != nil {
		if err == sql.ErrNoRows {
			return nil, nil, ErrTokenNotFound
		}
		return nil, nil, err
	}
	p.CreatedAt, _ = parseTime(principalCreatedAt)
	p.UpdatedAt, _ = parseTime(principalUpdatedAt)
	t.CreatedAt, _ = parseTime(tokenCreatedAt)
	t.UpdatedAt, _ = parseTime(tokenUpdatedAt)
	if revokedAt.Valid {
		parsed, _ := parseTime(revokedAt.String)
		t.RevokedAt = &parsed
	}
	return &p, &t, nil
}

// DisableToken transitions an active token to disabled using CAS on revision.
// Disabled tokens cannot authenticate. Revoked tokens are terminal.
func (s *Store) DisableToken(ctx context.Context, principalID, tokenRef string, currentRevision int64) (*TokenRecord, error) {
	return s.casStatus(ctx, principalID, tokenRef, currentRevision, []string{StatusActive}, StatusDisabled)
}

// RevokeToken transitions an active or disabled token to revoked. Revocation
// is irreversible: no method can return a revoked token to any other state.
func (s *Store) RevokeToken(ctx context.Context, principalID, tokenRef string, currentRevision int64) (*TokenRecord, error) {
	return s.casStatus(ctx, principalID, tokenRef, currentRevision, []string{StatusActive, StatusDisabled}, StatusRevoked)
}

// casStatus performs a compare-and-swap on the token status. It requires
// currentRevision to match the row's current revision and transitions from
// fromStatus to toStatus. The revision is incremented atomically.
func (s *Store) casStatus(ctx context.Context, principalID, tokenRef string, currentRevision int64, fromStatuses []string, toStatus string) (*TokenRecord, error) {
	var updated TokenRecord
	err := s.RunInTransaction(ctx, func(ctx context.Context, tx *sql.Tx) error {
		now := time.Now().UTC()
		placeholders := strings.TrimRight(strings.Repeat("?,", len(fromStatuses)), ",")
		query := `UPDATE tokens SET status=?, revision=revision+1, updated_at=?, revoked_at=?
			WHERE principal_id=? AND token_ref=? AND revision=? AND status IN (` + placeholders + `)`
		args := []any{toStatus, now, nil, principalID, tokenRef, currentRevision}
		if toStatus == StatusRevoked {
			args[2] = now
		}
		for _, status := range fromStatuses {
			args = append(args, status)
		}
		result, err := tx.ExecContext(ctx, s.bind(query), args...)
		if err != nil {
			return err
		}
		affected, err := result.RowsAffected()
		if err != nil {
			return err
		}
		if affected != 1 {
			return s.classifyTransitionMiss(ctx, tx, principalID, tokenRef, currentRevision, fromStatuses)
		}
		if err := s.loadToken(ctx, tx, principalID, tokenRef, &updated); err != nil {
			return err
		}
		return s.bumpProjectionGenerationTx(ctx, tx)
	})
	if err != nil {
		return nil, err
	}
	return &updated, nil
}

func (s *Store) classifyTransitionMiss(ctx context.Context, tx *sql.Tx, principalID, tokenRef string, currentRevision int64, fromStatuses []string) error {
	var status string
	var revision int64
	err := tx.QueryRowContext(ctx, s.bind(
		`SELECT status, revision FROM tokens WHERE principal_id=? AND token_ref=?`), principalID, tokenRef,
	).Scan(&status, &revision)
	if err == sql.ErrNoRows {
		return ErrTokenNotFound
	}
	if err != nil {
		return err
	}
	if revision != currentRevision {
		return fmt.Errorf("%w: have %d want %d", ErrRevisionMismatch, revision, currentRevision)
	}
	if status == StatusRevoked {
		return ErrTokenRevoked
	}
	return fmt.Errorf("%w: have %s want one of %s", ErrTokenNotActive, status, strings.Join(fromStatuses, ","))
}

func (s *Store) loadToken(ctx context.Context, tx *sql.Tx, principalID, tokenRef string, target *TokenRecord) error {
	var createdAt, updatedAt string
	var revokedAt sql.NullString
	err := tx.QueryRowContext(ctx, s.bind(
		`SELECT id, principal_id, token_ref, digest, status, revision, created_at, updated_at, revoked_at FROM tokens WHERE principal_id=? AND token_ref=?`),
		principalID, tokenRef,
	).Scan(&target.ID, &target.PrincipalID, &target.TokenRef, &target.Digest, &target.Status, &target.Revision, &createdAt, &updatedAt, &revokedAt)
	if err != nil {
		return err
	}
	target.CreatedAt, _ = parseTime(createdAt)
	target.UpdatedAt, _ = parseTime(updatedAt)
	if revokedAt.Valid {
		parsed, _ := parseTime(revokedAt.String)
		target.RevokedAt = &parsed
	}
	return nil
}

// parseTime attempts RFC3339 first, then falls back to microsecond-precision
// SQLite datetime format.
func parseTime(s string) (time.Time, error) {
	t, err := time.Parse(time.RFC3339, s)
	if err != nil {
		t, err = time.Parse("2006-01-02T15:04:05.999999999Z", s)
	}
	return t, err
}
