package meta

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"sync"
	"time"
)

const apiKeyIssuanceStripeCount = 64

var apiKeyIssuanceStripes [apiKeyIssuanceStripeCount]sync.Mutex

func apiKeyIssuanceMutex(tenantID string) *sync.Mutex {
	var h uint32
	for i := 0; i < len(tenantID); i++ {
		h = h*31 + uint32(tenantID[i])
	}
	return &apiKeyIssuanceStripes[h%apiKeyIssuanceStripeCount]
}

// GetAPIKey returns a single API key row by tenant and id (uncached direct read).
func (s *Store) GetAPIKey(ctx context.Context, tenantID, apiKeyID string) (out *APIKey, err error) {
	start := time.Now()
	defer observeMeta(ctx, "get_api_key", start, &err)
	row := s.db.QueryRowContext(ctx, `SELECT
			id, tenant_id, key_name, jwt_ciphertext, jwt_hash, token_version, status, scope_kind,
			issued_by_provider, issued_by_subject_key, issued_by_metadata_json, issued_at,
			revoked_at, expires_at, created_at, updated_at
		FROM tenant_api_keys
		WHERE tenant_id = ? AND id = ?`, tenantID, apiKeyID)
	rec, err := scanAPIKeyRow(row)
	if err != nil {
		return nil, err
	}
	return rec, nil
}

// ListAPIKeys lists active/disabled keys for a tenant. By default only non-expired
// rows are returned; set includeExpired to also include expired ones. Revoked rows
// are never listed. Order is fixed at created_at, id ascending.
func (s *Store) ListAPIKeys(ctx context.Context, tenantID string, includeExpired bool, offset, limit int) (out []APIKey, err error) {
	start := time.Now()
	defer observeMeta(ctx, "list_api_keys", start, &err)
	if offset < 0 {
		offset = 0
	}
	if limit <= 0 {
		limit = 50
	}
	// HTTP single-tenant list clamps to 200; org-wide enumeration uses
	// maxOrgListKeysPerTenant+1 (501) to detect the design's 500-row truncation.
	if limit > 501 {
		limit = 501
	}
	// List deliberately omits jwt_ciphertext / jwt_hash so secret material never
	// leaves the store on the enumeration path (defense in depth vs §4.4).
	query := `SELECT
			id, tenant_id, key_name, token_version, status, scope_kind,
			issued_by_provider, issued_by_subject_key, issued_by_metadata_json, issued_at,
			revoked_at, expires_at, created_at, updated_at
		FROM tenant_api_keys
		WHERE tenant_id = ? AND status IN (?, ?)`
	args := []any{tenantID, APIKeyActive, APIKeyDisabled}
	if !includeExpired {
		// UTC_TIMESTAMP matches write-side UTC wall clock; NOW(3) is session-local.
		query += ` AND (expires_at IS NULL OR expires_at > UTC_TIMESTAMP(3))`
	}
	query += ` ORDER BY created_at, id LIMIT ? OFFSET ?`
	args = append(args, limit, offset)

	rows, err := s.db.QueryContext(ctx, query, args...)
	if err != nil {
		return nil, err
	}
	defer func() { _ = rows.Close() }()

	out = make([]APIKey, 0)
	for rows.Next() {
		rec, scanErr := scanAPIKeyListRow(rows)
		if scanErr != nil {
			return nil, scanErr
		}
		out = append(out, *rec)
	}
	if err = rows.Err(); err != nil {
		return nil, err
	}
	return out, nil
}

// CountActiveAPIKeys counts keys that are active or disabled and not past
// expires_at. Owner rows with expires_at IS NULL count as non-expired (no
// expiry). Legacy fs_scoped rows with expires_at IS NULL are excluded: they
// cannot be distinguished as still-valid vs JWT-expired in SQL, and new
// fs_scoped writes are fail-closed by the storage guard.
func (s *Store) CountActiveAPIKeys(ctx context.Context, tenantID string) (n int, err error) {
	start := time.Now()
	defer observeMeta(ctx, "count_active_api_keys", start, &err)
	err = s.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM tenant_api_keys
		WHERE tenant_id = ? AND status IN (?, ?)
			AND ((expires_at IS NULL AND scope_kind = ?) OR expires_at > UTC_TIMESTAMP(3))`,
		tenantID, APIKeyActive, APIKeyDisabled, APIKeyScopeKindOwner).Scan(&n)
	return n, err
}

func countActiveAPIKeysTx(ctx context.Context, tx *sql.Tx, tenantID string) (int, error) {
	var n int
	err := tx.QueryRowContext(ctx, `SELECT COUNT(*) FROM tenant_api_keys
		WHERE tenant_id = ? AND status IN (?, ?)
			AND ((expires_at IS NULL AND scope_kind = ?) OR expires_at > UTC_TIMESTAMP(3))`,
		tenantID, APIKeyActive, APIKeyDisabled, APIKeyScopeKindOwner).Scan(&n)
	return n, err
}

// SetAPIKeyStatus transitions a key between active and disabled only.
// from and to must be active/disabled and from != to. Returns the current
// status and whether a transition occurred.
//
// When to == active (activation), the UPDATE also requires the key to be
// non-expired so an expires_at boundary cannot race between a pre-read and the
// status write (§4.5).
func (s *Store) SetAPIKeyStatus(ctx context.Context, tenantID, apiKeyID string, from, to APIKeyStatus) (current APIKeyStatus, changed bool, err error) {
	start := time.Now()
	defer observeMeta(ctx, "set_api_key_status", start, &err)

	if !isTransitionableAPIKeyStatus(from) || !isTransitionableAPIKeyStatus(to) {
		return "", false, fmt.Errorf("api key status transition only allows active/disabled (from=%q to=%q)", from, to)
	}
	if from == to {
		return "", false, fmt.Errorf("api key status from and to must differ")
	}

	now := time.Now().UTC()
	err = s.InTx(ctx, func(tx *sql.Tx) error {
		var res sql.Result
		var execErr error
		if to == APIKeyActive {
			// Activation is fail-closed on expiry: expires_at IS NULL (no expiry)
			// or expires_at still in the future, evaluated in the same statement.
			res, execErr = tx.ExecContext(ctx, `UPDATE tenant_api_keys
				SET status = ?, updated_at = ?
				WHERE tenant_id = ? AND id = ? AND status = ?
					AND (expires_at IS NULL OR expires_at > ?)`,
				to, now, tenantID, apiKeyID, from, now)
		} else {
			res, execErr = tx.ExecContext(ctx, `UPDATE tenant_api_keys
				SET status = ?, updated_at = ?
				WHERE tenant_id = ? AND id = ? AND status = ?`,
				to, now, tenantID, apiKeyID, from)
		}
		if execErr != nil {
			return execErr
		}
		n, rowsErr := res.RowsAffected()
		if rowsErr != nil {
			return rowsErr
		}
		if n == 1 {
			current = to
			changed = true
			return insertTenantNotifyTx(ctx, tx, tenantID, TenantNotifyWorkAPIKeyCacheCleanup)
		}

		// Read-back for race / idempotent / expired-activation semantics.
		var status string
		var expiresAt sql.NullTime
		scanErr := tx.QueryRowContext(ctx, `SELECT status, expires_at FROM tenant_api_keys
			WHERE tenant_id = ? AND id = ?`, tenantID, apiKeyID).Scan(&status, &expiresAt)
		if scanErr != nil {
			if errors.Is(scanErr, sql.ErrNoRows) {
				return ErrNotFound
			}
			return scanErr
		}
		current = APIKeyStatus(status)
		if current == APIKeyRevoked {
			return ErrAPIKeyTerminal
		}
		if to == APIKeyActive && current == from &&
			expiresAt.Valid && !expiresAt.Time.After(now) {
			// Still disabled (or from-status) but past expires_at — activation blocked.
			return ErrAPIKeyExpired
		}
		if current == to {
			changed = false
			return nil
		}
		return fmt.Errorf("unexpected api key status %q after transition from %q to %q", current, from, to)
	})
	if err != nil {
		return "", false, err
	}
	if changed {
		s.apiKeys.evictTenant(tenantID)
	}
	return current, changed, nil
}

// RotateAPIKey replaces secret material for an active key under optimistic locking
// on token_version. next must carry the new hash/ciphertext/version/issued_at/expires_at.
// Zero rows matched (version conflict / non-active) returns ErrNotFound; callers
// that map concurrent refresh to HTTP 409 should treat that as conflict.
func (s *Store) RotateAPIKey(ctx context.Context, tenantID, apiKeyID string, oldVersion int, next *APIKey) (err error) {
	start := time.Now()
	defer observeMeta(ctx, "rotate_api_key", start, &err)
	if next == nil {
		return fmt.Errorf("next api key is required")
	}
	// Fail-closed: rotation must strictly advance token_version. A same-version
	// write would leave a residual-window stale Bearer able to re-pass the
	// caller-version check; a regressed version would resurrect historical
	// Bearers. Reject before any write.
	if next.TokenVersion <= oldVersion {
		return ErrAPIKeyVersionUnchanged
	}
	// Fast-path when the caller populated ScopeKind. Empty ScopeKind is still
	// checked against the row inside the transaction (fail-closed for fs_scoped).
	if next.ScopeKind == APIKeyScopeKindFS && next.ExpiresAt == nil {
		return fmt.Errorf("fs_scoped api key requires expires_at")
	}

	now := time.Now().UTC()
	issuedAt := next.IssuedAt
	if issuedAt.IsZero() {
		issuedAt = now
	}
	updated := false
	err = s.InTx(ctx, func(tx *sql.Tx) error {
		var scopeKind string
		if scanErr := tx.QueryRowContext(ctx, `SELECT scope_kind FROM tenant_api_keys
			WHERE tenant_id = ? AND id = ? AND token_version = ? AND status = ?`,
			tenantID, apiKeyID, oldVersion, APIKeyActive).Scan(&scopeKind); scanErr != nil {
			if errors.Is(scanErr, sql.ErrNoRows) {
				return nil
			}
			return scanErr
		}
		if APIKeyScopeKind(scopeKind) == APIKeyScopeKindFS && next.ExpiresAt == nil {
			return fmt.Errorf("fs_scoped api key requires expires_at")
		}

		res, execErr := tx.ExecContext(ctx, `UPDATE tenant_api_keys
			SET jwt_hash = ?, jwt_ciphertext = ?, token_version = ?, issued_at = ?, expires_at = ?, updated_at = ?
			WHERE tenant_id = ? AND id = ? AND token_version = ? AND status = ?`,
			next.JWTHash, next.JWTCiphertext, next.TokenVersion, issuedAt.UTC(), nullTimeFromPtr(next.ExpiresAt), now,
			tenantID, apiKeyID, oldVersion, APIKeyActive)
		if execErr != nil {
			if isDuplicateEntry(execErr) {
				return ErrDuplicate
			}
			return execErr
		}
		n, rowsErr := res.RowsAffected()
		if rowsErr != nil {
			return rowsErr
		}
		if n == 0 {
			return nil
		}
		updated = true
		return insertTenantNotifyTx(ctx, tx, tenantID, TenantNotifyWorkAPIKeyCacheCleanup)
	})
	if err != nil {
		return err
	}
	s.apiKeys.evictTenant(tenantID)
	if !updated {
		return ErrNotFound
	}
	return nil
}

// InsertAPIKeyWithLimit inserts a key after locking the tenant row and verifying
// the non-expired active/disabled count is below maxKeys.
func (s *Store) InsertAPIKeyWithLimit(ctx context.Context, key *APIKey, maxKeys int) (err error) {
	start := time.Now()
	defer observeMeta(ctx, "insert_api_key_with_limit", start, &err)
	if key == nil {
		return fmt.Errorf("api key is required")
	}
	if maxKeys <= 0 {
		maxKeys = DefaultMaxActiveAPIKeys
	}
	scopeKind, err := apiKeyScopeKindForInsert(key)
	if err != nil {
		return err
	}
	if err := guardAPIKeyExpiresAt(scopeKind, key); err != nil {
		return err
	}

	unlock := acquireAPIKeyIssuanceLock(key.TenantID)
	defer unlock()
	err = s.InTx(ctx, func(tx *sql.Tx) error {
		if lockErr := lockTenantForKeyIssuance(ctx, tx, key.TenantID, TenantActive); lockErr != nil {
			return lockErr
		}
		n, countErr := countActiveAPIKeysTx(ctx, tx, key.TenantID)
		if countErr != nil {
			return countErr
		}
		if n >= maxKeys {
			return ErrAPIKeyLimitExceeded
		}
		_, insertErr := tx.ExecContext(ctx, `INSERT INTO tenant_api_keys
			(id, tenant_id, key_name, jwt_ciphertext, jwt_hash, token_version, status, scope_kind,
			 issued_by_provider, issued_by_subject_key, issued_by_metadata_json,
			 issued_at, revoked_at, expires_at, created_at, updated_at)
			VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
			key.ID, key.TenantID, key.KeyName, key.JWTCiphertext, key.JWTHash, key.TokenVersion, key.Status, scopeKind,
			key.IssuedByProvider, key.IssuedBySubjectKey, nullableBytes(key.IssuedByMetadataJSON),
			key.IssuedAt.UTC(), key.RevokedAt, nullTimeFromPtr(key.ExpiresAt), key.CreatedAt.UTC(), key.UpdatedAt.UTC())
		if isDuplicateEntry(insertErr) {
			return ErrDuplicate
		}
		return insertErr
	})
	return err
}

// RotateIssuerAPIKey revokes existing keys for an issuer and inserts next under
// a single tenant-row lock, so rotation never exceeds the active-key cap.
//
// The locked tenant is always tenantID. next.TenantID must be empty or equal to
// tenantID; otherwise ErrAPIKeyTenantMismatch is returned and neither tenant is
// mutated. The insert always uses tenantID (never a divergent next.TenantID).
func (s *Store) RotateIssuerAPIKey(ctx context.Context, tenantID, provider, subjectKey string, next *APIKey) (err error) {
	start := time.Now()
	defer observeMeta(ctx, "rotate_issuer_api_key", start, &err)
	if next == nil {
		return fmt.Errorf("next api key is required")
	}
	if next.TenantID != "" && next.TenantID != tenantID {
		return fmt.Errorf("%w: next=%q locked=%q", ErrAPIKeyTenantMismatch, next.TenantID, tenantID)
	}
	// Local copy so we never insert into a different tenant than we locked/counted.
	rec := *next
	rec.TenantID = tenantID
	scopeKind, err := apiKeyScopeKindForInsert(&rec)
	if err != nil {
		return err
	}
	if err := guardAPIKeyExpiresAt(scopeKind, &rec); err != nil {
		return err
	}
	now := time.Now().UTC()

	unlock := acquireAPIKeyIssuanceLock(tenantID)
	defer unlock()
	err = s.InTx(ctx, func(tx *sql.Tx) error {
		// Slock rotates issuer keys for active and provisioning tenants.
		// Still reject deleting/deleted (and other non-issuable statuses) under
		// the lock so concurrent delete cannot race past a handler precheck.
		if lockErr := lockTenantForKeyIssuance(ctx, tx, tenantID, TenantActive, TenantProvisioning); lockErr != nil {
			return lockErr
		}

		// Revoke all active/disabled keys for this issuer first (freeing slots).
		if _, revErr := tx.ExecContext(ctx, `UPDATE tenant_api_keys
			SET status = ?, revoked_at = COALESCE(revoked_at, ?), updated_at = ?
			WHERE tenant_id = ? AND issued_by_provider = ? AND issued_by_subject_key = ?
				AND status IN (?, ?)`,
			APIKeyRevoked, now, now, tenantID, provider, subjectKey, APIKeyActive, APIKeyDisabled); revErr != nil {
			return revErr
		}

		n, countErr := countActiveAPIKeysTx(ctx, tx, tenantID)
		if countErr != nil {
			return countErr
		}
		if n >= DefaultMaxActiveAPIKeys {
			return ErrAPIKeyLimitExceeded
		}

		if _, insertErr := tx.ExecContext(ctx, `INSERT INTO tenant_api_keys
			(id, tenant_id, key_name, jwt_ciphertext, jwt_hash, token_version, status, scope_kind,
			 issued_by_provider, issued_by_subject_key, issued_by_metadata_json,
			 issued_at, revoked_at, expires_at, created_at, updated_at)
			VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
			rec.ID, rec.TenantID, rec.KeyName, rec.JWTCiphertext, rec.JWTHash, rec.TokenVersion, rec.Status, scopeKind,
			rec.IssuedByProvider, rec.IssuedBySubjectKey, nullableBytes(rec.IssuedByMetadataJSON),
			rec.IssuedAt.UTC(), rec.RevokedAt, nullTimeFromPtr(rec.ExpiresAt), rec.CreatedAt.UTC(), rec.UpdatedAt.UTC()); insertErr != nil {
			if isDuplicateEntry(insertErr) {
				return ErrDuplicate
			}
			return insertErr
		}
		return insertTenantNotifyTx(ctx, tx, tenantID, TenantNotifyWorkAPIKeyCacheCleanup)
	})
	if err != nil {
		return err
	}
	s.apiKeys.evictTenant(tenantID)
	return nil
}

func isTransitionableAPIKeyStatus(s APIKeyStatus) bool {
	return s == APIKeyActive || s == APIKeyDisabled
}

// Optional test hooks for deterministic delete/issuance schedules. Production
// keeps these nil; tests install and clear via t.Cleanup.
var (
	testHookBeforeTenantKeyIssuanceLock func(tenantID string)
	testHookAfterTenantKeyIssuanceLock  func(tenantID string)
)

// acquireAPIKeyIssuanceLock serializes insert/rotate for one tenant in this
// process. Unistore does not serialize a read-only SELECT FOR UPDATE, so the
// mutex is held across InTx commit. The before-issuance test hook runs first
// so delete-first handshake tests can pause every issuer before the mutex.
func acquireAPIKeyIssuanceLock(tenantID string) func() {
	if hook := testHookBeforeTenantKeyIssuanceLock; hook != nil {
		hook(tenantID)
	}
	mu := apiKeyIssuanceMutex(tenantID)
	mu.Lock()
	return mu.Unlock
}

// lockTenantForKeyIssuance FOR UPDATEs the tenant row and requires its status
// to be in allowed. Call under an open transaction before counting/inserting
// keys so a concurrent delete (status→deleting + RevokeTenantAPIKeys) cannot
// race past a handler precheck and leave a newly issued active key behind.
// Anything outside allowed — including deleting/deleted — fails closed with
// ErrTenantNotActive.
func lockTenantForKeyIssuance(ctx context.Context, tx *sql.Tx, tenantID string, allowed ...TenantStatus) error {
	var id, status string
	if err := tx.QueryRowContext(ctx, `SELECT id, status FROM tenants WHERE id = ? FOR UPDATE`, tenantID).Scan(&id, &status); err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return ErrNotFound
		}
		return err
	}
	for _, want := range allowed {
		if TenantStatus(status) == want {
			if hook := testHookAfterTenantKeyIssuanceLock; hook != nil {
				hook(tenantID)
			}
			return nil
		}
	}
	return fmt.Errorf("%w: status %q", ErrTenantNotActive, status)
}

type apiKeyScanner interface {
	Scan(dest ...any) error
}

func scanAPIKeyRow(row apiKeyScanner) (*APIKey, error) {
	var rec APIKey
	var revokedAt sql.NullTime
	var expiresAt sql.NullTime
	var issuedByProvider sql.NullString
	var issuedBySubjectKey sql.NullString
	var issuedByMetadataJSON []byte
	if err := row.Scan(
		&rec.ID, &rec.TenantID, &rec.KeyName, &rec.JWTCiphertext,
		&rec.JWTHash, &rec.TokenVersion, &rec.Status, &rec.ScopeKind,
		&issuedByProvider, &issuedBySubjectKey, &issuedByMetadataJSON, &rec.IssuedAt,
		&revokedAt, &expiresAt, &rec.CreatedAt, &rec.UpdatedAt,
	); err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return nil, ErrNotFound
		}
		return nil, err
	}
	if revokedAt.Valid {
		t := revokedAt.Time.UTC()
		rec.RevokedAt = &t
	}
	if expiresAt.Valid {
		t := expiresAt.Time.UTC()
		rec.ExpiresAt = &t
	}
	if issuedByProvider.Valid {
		rec.IssuedByProvider = issuedByProvider.String
	}
	if issuedBySubjectKey.Valid {
		rec.IssuedBySubjectKey = issuedBySubjectKey.String
	}
	rec.IssuedByMetadataJSON = issuedByMetadataJSON
	return &rec, nil
}

// scanAPIKeyListRow scans a list projection without jwt_ciphertext / jwt_hash.
func scanAPIKeyListRow(row apiKeyScanner) (*APIKey, error) {
	var rec APIKey
	var revokedAt sql.NullTime
	var expiresAt sql.NullTime
	var issuedByProvider sql.NullString
	var issuedBySubjectKey sql.NullString
	var issuedByMetadataJSON []byte
	if err := row.Scan(
		&rec.ID, &rec.TenantID, &rec.KeyName,
		&rec.TokenVersion, &rec.Status, &rec.ScopeKind,
		&issuedByProvider, &issuedBySubjectKey, &issuedByMetadataJSON, &rec.IssuedAt,
		&revokedAt, &expiresAt, &rec.CreatedAt, &rec.UpdatedAt,
	); err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return nil, ErrNotFound
		}
		return nil, err
	}
	if revokedAt.Valid {
		t := revokedAt.Time.UTC()
		rec.RevokedAt = &t
	}
	if expiresAt.Valid {
		t := expiresAt.Time.UTC()
		rec.ExpiresAt = &t
	}
	if issuedByProvider.Valid {
		rec.IssuedByProvider = issuedByProvider.String
	}
	if issuedBySubjectKey.Valid {
		rec.IssuedBySubjectKey = issuedBySubjectKey.String
	}
	rec.IssuedByMetadataJSON = issuedByMetadataJSON
	return &rec, nil
}
