package credentialstore

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"strings"
	"time"

	"github.com/google/uuid"
)

var (
	// StatusDraft is the initial state of a credential slot. A draft cannot
	// serve a route until a compatible binding validates its envelope key.
	StatusDraft = "draft"

	// ErrSlotNotFound is returned when a requested credential slot does not exist.
	ErrSlotNotFound = errors.New("credentialstore: slot not found")

	// ErrSlotAliasAlreadyExists is returned when a slot alias is already in use for a principal.
	ErrSlotAliasAlreadyExists = errors.New("credentialstore: slot alias already exists for principal")

	// ErrSlotRevoked is returned when attempting to modify a revoked slot.
	ErrSlotRevoked = errors.New("credentialstore: revoked slot cannot be modified")

	// ErrSlotNotActive is returned when an operation requires an active slot.
	ErrSlotNotActive = errors.New("credentialstore: slot is not active")
)

const (
	CredentialKindBearer = "bearer"
	CredentialKindAPIKey = "api_key"
)

func normalizeCredentialPart(value string) string {
	return strings.ToLower(strings.TrimSpace(value))
}

func normalizeAlias(value string) string {
	return strings.TrimSpace(value)
}

func knownCredentialKind(kind string) bool {
	switch kind {
	case CredentialKindBearer, CredentialKindAPIKey:
		return true
	default:
		return false
	}
}

// CreateSlotInput holds parameters for creating a new credential slot.
//
// SlotID is an additive, optional caller-generated UUID. When set it lets the
// caller bind create-time secret AAD to a stable slot id before sealing; when
// empty (the legacy zero value) the store generates the id.
type CreateSlotInput struct {
	SlotID         string
	PrincipalID    string
	Vendor         string
	CredentialKind string
	Alias          string
	Envelope       SecretEnvelope
}

// RotateSlotSecretInput holds parameters for rotating secret envelope on an existing slot.
type RotateSlotSecretInput struct {
	PrincipalID     string
	SlotID          string
	CurrentRevision int64
	Envelope        SecretEnvelope
}

// CredentialSlotRecord represents a persisted credential slot row.
type CredentialSlotRecord struct {
	ID             string
	PrincipalID    string
	Vendor         string
	CredentialKind string
	Alias          string
	Status         string
	Revision       int64
	Envelope       SecretEnvelope
	CreatedAt      time.Time
	UpdatedAt      time.Time
	RevokedAt      *time.Time
}

// SlotRevisionRecord represents a persisted historical secret revision for a slot.
type SlotRevisionRecord struct {
	ID        string
	SlotID    string
	Revision  int64
	Envelope  SecretEnvelope
	CreatedAt time.Time
}

// CreateSlot creates a principal-scoped credential slot with an initial secret revision.
func (s *Store) CreateSlot(ctx context.Context, in CreateSlotInput) (*CredentialSlotRecord, error) {
	if strings.TrimSpace(in.PrincipalID) == "" {
		return nil, fmt.Errorf("credentialstore: CreateSlot: principal_id is required")
	}
	vendor := normalizeCredentialPart(in.Vendor)
	if vendor == "" {
		return nil, fmt.Errorf("credentialstore: CreateSlot: vendor is required")
	}
	kind := normalizeCredentialPart(in.CredentialKind)
	if !knownCredentialKind(kind) {
		return nil, fmt.Errorf("credentialstore: CreateSlot: credential_kind is not recognized")
	}
	alias := normalizeAlias(in.Alias)

	if err := s.validateEnvelopeKey(ctx, in.Envelope); err != nil {
		return nil, err
	}

	slotID := strings.TrimSpace(in.SlotID)
	if slotID == "" {
		slotID = uuid.New().String()
	} else if _, err := uuid.Parse(slotID); err != nil {
		return nil, fmt.Errorf("credentialstore: CreateSlot: slot_id must be a valid UUID")
	}
	revisionID := uuid.New().String()
	now := time.Now().UTC()

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

		if alias != "" {
			var sCount int
			if err := tx.QueryRowContext(ctx, s.bind(`SELECT COUNT(*) FROM credential_slots WHERE principal_id=? AND alias=?`), in.PrincipalID, alias).Scan(&sCount); err != nil {
				return fmt.Errorf("credentialstore: check slot alias: %w", err)
			}
			if sCount > 0 {
				return ErrSlotAliasAlreadyExists
			}
		}
		var aliasArg any
		if alias != "" {
			aliasArg = alias
		}

		if _, err := tx.ExecContext(ctx, s.bind(`
			INSERT INTO credential_slots (id, principal_id, vendor, credential_kind, alias, status, revision, algorithm, key_id, key_version, nonce, ciphertext, aad, created_at, updated_at)
			VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
		`), slotID, in.PrincipalID, vendor, kind, aliasArg, StatusDraft, 0,
			in.Envelope.Algorithm, in.Envelope.KeyID, in.Envelope.KeyVersion,
			in.Envelope.Nonce, in.Envelope.Ciphertext, in.Envelope.AAD,
			now, now,
		); err != nil {
			return fmt.Errorf("credentialstore: insert slot: %w", err)
		}

		if _, err := tx.ExecContext(ctx, s.bind(`
			INSERT INTO credential_slot_revisions (id, slot_id, revision, algorithm, key_id, key_version, nonce, ciphertext, aad, created_at)
			VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
		`), revisionID, slotID, 0,
			in.Envelope.Algorithm, in.Envelope.KeyID, in.Envelope.KeyVersion,
			in.Envelope.Nonce, in.Envelope.Ciphertext, in.Envelope.AAD,
			now,
		); err != nil {
			return fmt.Errorf("credentialstore: insert slot revision: %w", err)
		}
		if err := s.bumpProjectionGenerationTx(ctx, tx); err != nil {
			return err
		}

		record = CredentialSlotRecord{
			ID:             slotID,
			PrincipalID:    in.PrincipalID,
			Vendor:         vendor,
			CredentialKind: kind,
			Alias:          alias,
			Status:         StatusDraft,
			Revision:       0,
			Envelope:       in.Envelope,
			CreatedAt:      now,
			UpdatedAt:      now,
		}
		return nil
	})

	if err != nil {
		return nil, err
	}
	return &record, nil
}

// GetSlot retrieves a credential slot by principal ID and slot ID.
func (s *Store) GetSlot(ctx context.Context, principalID, slotID string) (*CredentialSlotRecord, error) {
	var r CredentialSlotRecord
	var alias sql.NullString
	var createdAt, updatedAt string
	var revokedAt sql.NullString
	err := s.db.QueryRowContext(ctx, s.bind(`
		SELECT id, principal_id, vendor, credential_kind, alias, status, revision, algorithm, key_id, key_version, nonce, ciphertext, aad, created_at, updated_at, revoked_at
		FROM credential_slots
		WHERE principal_id=? AND id=?
	`), principalID, slotID).Scan(
		&r.ID, &r.PrincipalID, &r.Vendor, &r.CredentialKind, &alias, &r.Status, &r.Revision,
		&r.Envelope.Algorithm, &r.Envelope.KeyID, &r.Envelope.KeyVersion,
		&r.Envelope.Nonce, &r.Envelope.Ciphertext, &r.Envelope.AAD,
		&createdAt, &updatedAt, &revokedAt,
	)
	if err != nil {
		if err == sql.ErrNoRows {
			return nil, ErrSlotNotFound
		}
		return nil, err
	}
	if alias.Valid {
		r.Alias = alias.String
	}
	r.CreatedAt, _ = parseTime(createdAt)
	r.UpdatedAt, _ = parseTime(updatedAt)
	if revokedAt.Valid {
		parsed, _ := parseTime(revokedAt.String)
		r.RevokedAt = &parsed
	}
	return &r, nil
}

// GetSlotByAlias retrieves a credential slot by principal ID and alias.
func (s *Store) GetSlotByAlias(ctx context.Context, principalID, alias string) (*CredentialSlotRecord, error) {
	var r CredentialSlotRecord
	var aliasValue sql.NullString
	var createdAt, updatedAt string
	var revokedAt sql.NullString
	err := s.db.QueryRowContext(ctx, s.bind(`
		SELECT id, principal_id, vendor, credential_kind, alias, status, revision, algorithm, key_id, key_version, nonce, ciphertext, aad, created_at, updated_at, revoked_at
		FROM credential_slots
		WHERE principal_id=? AND alias=?
	`), principalID, normalizeAlias(alias)).Scan(
		&r.ID, &r.PrincipalID, &r.Vendor, &r.CredentialKind, &aliasValue, &r.Status, &r.Revision,
		&r.Envelope.Algorithm, &r.Envelope.KeyID, &r.Envelope.KeyVersion,
		&r.Envelope.Nonce, &r.Envelope.Ciphertext, &r.Envelope.AAD,
		&createdAt, &updatedAt, &revokedAt,
	)
	if err != nil {
		if err == sql.ErrNoRows {
			return nil, ErrSlotNotFound
		}
		return nil, err
	}
	if aliasValue.Valid {
		r.Alias = aliasValue.String
	}
	r.CreatedAt, _ = parseTime(createdAt)
	r.UpdatedAt, _ = parseTime(updatedAt)
	if revokedAt.Valid {
		parsed, _ := parseTime(revokedAt.String)
		r.RevokedAt = &parsed
	}
	return &r, nil
}

// ListSlots returns all credential slots belonging to a principal.
func (s *Store) ListSlots(ctx context.Context, principalID string) ([]CredentialSlotRecord, error) {
	rows, err := s.db.QueryContext(ctx, s.bind(`
		SELECT id, principal_id, vendor, credential_kind, alias, status, revision, algorithm, key_id, key_version, nonce, ciphertext, aad, created_at, updated_at, revoked_at
		FROM credential_slots
		WHERE principal_id=?
		ORDER BY alias
	`), principalID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var out []CredentialSlotRecord
	for rows.Next() {
		var r CredentialSlotRecord
		var alias sql.NullString
		var createdAt, updatedAt string
		var revokedAt sql.NullString
		if err := rows.Scan(
			&r.ID, &r.PrincipalID, &r.Vendor, &r.CredentialKind, &alias, &r.Status, &r.Revision,
			&r.Envelope.Algorithm, &r.Envelope.KeyID, &r.Envelope.KeyVersion,
			&r.Envelope.Nonce, &r.Envelope.Ciphertext, &r.Envelope.AAD,
			&createdAt, &updatedAt, &revokedAt,
		); err != nil {
			return nil, err
		}
		if alias.Valid {
			r.Alias = alias.String
		}
		r.CreatedAt, _ = parseTime(createdAt)
		r.UpdatedAt, _ = parseTime(updatedAt)
		if revokedAt.Valid {
			parsed, _ := parseTime(revokedAt.String)
			r.RevokedAt = &parsed
		}
		out = append(out, r)
	}
	return out, rows.Err()
}

// RotateSlotSecret replaces the secret envelope on a slot using CAS on revision.
func (s *Store) RotateSlotSecret(ctx context.Context, in RotateSlotSecretInput) (*CredentialSlotRecord, error) {
	if strings.TrimSpace(in.PrincipalID) == "" || strings.TrimSpace(in.SlotID) == "" {
		return nil, fmt.Errorf("credentialstore: RotateSlotSecret: principal_id and slot_id are required")
	}

	if err := s.validateEnvelopeKey(ctx, in.Envelope); err != nil {
		return nil, err
	}

	var updated CredentialSlotRecord
	err := s.RunInTransaction(ctx, func(ctx context.Context, tx *sql.Tx) error {
		var currentStatus string
		var currentRev int64
		err := tx.QueryRowContext(ctx, s.bind(`
			SELECT status, revision FROM credential_slots WHERE principal_id=? AND id=?
		`), in.PrincipalID, in.SlotID).Scan(&currentStatus, &currentRev)
		if err == sql.ErrNoRows {
			return ErrSlotNotFound
		}
		if err != nil {
			return err
		}
		if currentStatus == StatusRevoked {
			return ErrSlotRevoked
		}
		if currentRev != in.CurrentRevision {
			return fmt.Errorf("%w: have %d want %d", ErrRevisionMismatch, currentRev, in.CurrentRevision)
		}

		now := time.Now().UTC()
		nextRev := currentRev + 1
		res, err := tx.ExecContext(ctx, s.bind(`
			UPDATE credential_slots
			SET revision=?, algorithm=?, key_id=?, key_version=?, nonce=?, ciphertext=?, aad=?, updated_at=?
			WHERE principal_id=? AND id=? AND revision=?
		`), nextRev, in.Envelope.Algorithm, in.Envelope.KeyID, in.Envelope.KeyVersion,
			in.Envelope.Nonce, in.Envelope.Ciphertext, in.Envelope.AAD, now,
			in.PrincipalID, in.SlotID, in.CurrentRevision,
		)
		if err != nil {
			return err
		}
		affected, err := res.RowsAffected()
		if err != nil {
			return err
		}
		if affected != 1 {
			return fmt.Errorf("%w: failed to update slot revision", ErrRevisionMismatch)
		}

		revisionID := uuid.New().String()
		if _, err := tx.ExecContext(ctx, s.bind(`
			INSERT INTO credential_slot_revisions (id, slot_id, revision, algorithm, key_id, key_version, nonce, ciphertext, aad, created_at)
			VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
		`), revisionID, in.SlotID, nextRev,
			in.Envelope.Algorithm, in.Envelope.KeyID, in.Envelope.KeyVersion,
			in.Envelope.Nonce, in.Envelope.Ciphertext, in.Envelope.AAD, now,
		); err != nil {
			return fmt.Errorf("credentialstore: insert slot revision: %w", err)
		}

		if err := s.loadSlotTx(ctx, tx, in.PrincipalID, in.SlotID, &updated); err != nil {
			return err
		}
		return s.bumpProjectionGenerationTx(ctx, tx)
	})

	if err != nil {
		return nil, err
	}
	return &updated, nil
}

// DisableSlot disables an active credential slot using CAS on revision.
func (s *Store) DisableSlot(ctx context.Context, principalID, slotID string, currentRevision int64) (*CredentialSlotRecord, error) {
	return s.casSlotStatus(ctx, principalID, slotID, currentRevision, []string{StatusActive}, StatusDisabled)
}

// EnableSlot enables a disabled credential slot using CAS on revision. The
// stored envelope key and at least one non-revoked compatible binding must
// still be valid before the slot can become active again.
func (s *Store) EnableSlot(ctx context.Context, principalID, slotID string, currentRevision int64) (*CredentialSlotRecord, error) {
	return s.casSlotStatus(ctx, principalID, slotID, currentRevision, []string{StatusDisabled}, StatusActive)
}

// RevokeSlot revokes an active or disabled credential slot. Revocation is permanent.
func (s *Store) RevokeSlot(ctx context.Context, principalID, slotID string, currentRevision int64) (*CredentialSlotRecord, error) {
	return s.casSlotStatus(ctx, principalID, slotID, currentRevision, []string{StatusDraft, StatusActive, StatusDisabled}, StatusRevoked)
}

func (s *Store) casSlotStatus(ctx context.Context, principalID, slotID string, currentRevision int64, fromStatuses []string, toStatus string) (*CredentialSlotRecord, error) {
	var updated CredentialSlotRecord
	err := s.RunInTransaction(ctx, func(ctx context.Context, tx *sql.Tx) error {
		if toStatus == StatusActive {
			var slot CredentialSlotRecord
			if err := s.loadSlotTx(ctx, tx, principalID, slotID, &slot); err != nil {
				if errors.Is(err, sql.ErrNoRows) {
					return ErrSlotNotFound
				}
				return err
			}
			if err := s.validateEnvelopeKey(ctx, slot.Envelope); err != nil {
				return err
			}
			ok, err := s.slotHasCompatibleBindingTx(ctx, tx, slot)
			if err != nil {
				return err
			}
			if !ok {
				return fmt.Errorf("%w: slot has no compatible binding", ErrSlotNotActive)
			}
		}
		now := time.Now().UTC()
		placeholders := strings.TrimRight(strings.Repeat("?,", len(fromStatuses)), ",")
		query := `UPDATE credential_slots SET status=?, revision=revision+1, updated_at=?, revoked_at=?
			WHERE principal_id=? AND id=? AND revision=? AND status IN (` + placeholders + `)`
		var revokedArg interface{}
		if toStatus == StatusRevoked {
			revokedArg = now
		}
		args := []any{toStatus, now, revokedArg, principalID, slotID, currentRevision}
		for _, st := range fromStatuses {
			args = append(args, st)
		}
		res, err := tx.ExecContext(ctx, s.bind(query), args...)
		if err != nil {
			return err
		}
		affected, err := res.RowsAffected()
		if err != nil {
			return err
		}
		if affected != 1 {
			return s.classifySlotTransitionMiss(ctx, tx, principalID, slotID, currentRevision, fromStatuses)
		}
		if err := s.loadSlotTx(ctx, tx, principalID, slotID, &updated); err != nil {
			return err
		}
		return s.bumpProjectionGenerationTx(ctx, tx)
	})
	if err != nil {
		return nil, err
	}
	return &updated, nil
}

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

func (s *Store) loadSlotTx(ctx context.Context, tx *sql.Tx, principalID, slotID string, target *CredentialSlotRecord) error {
	var alias sql.NullString
	var createdAt, updatedAt string
	var revokedAt sql.NullString
	err := tx.QueryRowContext(ctx, s.bind(`
		SELECT id, principal_id, vendor, credential_kind, alias, status, revision, algorithm, key_id, key_version, nonce, ciphertext, aad, created_at, updated_at, revoked_at
		FROM credential_slots
		WHERE principal_id=? AND id=?
	`), principalID, slotID).Scan(
		&target.ID, &target.PrincipalID, &target.Vendor, &target.CredentialKind, &alias, &target.Status, &target.Revision,
		&target.Envelope.Algorithm, &target.Envelope.KeyID, &target.Envelope.KeyVersion,
		&target.Envelope.Nonce, &target.Envelope.Ciphertext, &target.Envelope.AAD,
		&createdAt, &updatedAt, &revokedAt,
	)
	if err != nil {
		return err
	}
	if alias.Valid {
		target.Alias = alias.String
	}
	target.CreatedAt, _ = parseTime(createdAt)
	target.UpdatedAt, _ = parseTime(updatedAt)
	if revokedAt.Valid {
		parsed, _ := parseTime(revokedAt.String)
		target.RevokedAt = &parsed
	}
	return nil
}

// ListSlotRevisions returns all historical secret envelope revisions for a slot.
func (s *Store) ListSlotRevisions(ctx context.Context, principalID, slotID string) ([]SlotRevisionRecord, error) {
	// First verify slot ownership
	if _, err := s.GetSlot(ctx, principalID, slotID); err != nil {
		return nil, err
	}

	rows, err := s.db.QueryContext(ctx, s.bind(`
		SELECT id, slot_id, revision, algorithm, key_id, key_version, nonce, ciphertext, aad, created_at
		FROM credential_slot_revisions
		WHERE slot_id=?
		ORDER BY revision ASC
	`), slotID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var out []SlotRevisionRecord
	for rows.Next() {
		var r SlotRevisionRecord
		var createdAt string
		if err := rows.Scan(
			&r.ID, &r.SlotID, &r.Revision,
			&r.Envelope.Algorithm, &r.Envelope.KeyID, &r.Envelope.KeyVersion,
			&r.Envelope.Nonce, &r.Envelope.Ciphertext, &r.Envelope.AAD,
			&createdAt,
		); err != nil {
			return nil, err
		}
		r.CreatedAt, _ = parseTime(createdAt)
		out = append(out, r)
	}
	return out, rows.Err()
}
