package store

import (
	"context"
	"database/sql"
	"encoding/json"
	"errors"
	"time"

	"github.com/google/uuid"
	"github.com/jackc/pgx/v5"
)

var (
	ErrSecurityEventConnectionNotFound = errors.New("security event connection not found")
	ErrSecurityEventConnectionConflict = errors.New("security event connection conflicts with current state")
	ErrIdentitySecretCleanupConflict   = errors.New("identity secret cleanup conflicts with current state")
)

type SecurityEventConnection struct {
	ConnectionID            string     `json:"connectionId"`
	TransmitterIssuer       string     `json:"transmitterIssuer"`
	EndpointURL             string     `json:"endpointUrl"`
	Audience                *string    `json:"audience,omitempty"`
	StreamID                *string    `json:"streamId,omitempty"`
	CredentialRef           string     `json:"-"`
	NextCredentialRef       *string    `json:"-"`
	ManagementClientID      string     `json:"managementClientId"`
	ManagementCredentialRef *string    `json:"-"`
	LifecycleStatus         string     `json:"lifecycleStatus"`
	Version                 int64      `json:"version"`
	LastErrorCategory       *string    `json:"lastErrorCategory,omitempty"`
	IdempotencyKey          string     `json:"-"`
	CreatedAt               time.Time  `json:"createdAt"`
	UpdatedAt               time.Time  `json:"updatedAt"`
	LastVerificationAt      *time.Time `json:"lastVerificationAt,omitempty"`
	HealthMode              string     `json:"healthMode"`
}

type CreateSecurityEventConnectionInput struct {
	ConnectionID, TransmitterIssuer, EndpointURL, CredentialRef, ManagementClientID, ManagementCredentialRef, IdempotencyKey string
}

type SecurityEventConnectionIdempotency struct {
	RequestHash string
	Response    json.RawMessage
}

type IdentitySecretCleanupClaim struct {
	Reference      string
	ClaimToken     string
	LeaseExpiresAt time.Time
}

func (s *Store) QueueIdentitySecretCleanup(ctx context.Context, reference string, notBefore time.Time) error {
	tag, err := s.pool.Exec(ctx, `INSERT INTO gateway_identity_secret_cleanup_queue(secret_ref,not_before)
VALUES($1,$2) ON CONFLICT(secret_ref) DO UPDATE
SET not_before=LEAST(gateway_identity_secret_cleanup_queue.not_before,EXCLUDED.not_before),updated_at=now()
WHERE gateway_identity_secret_cleanup_queue.status='pending'`, reference, notBefore)
	if err != nil {
		return err
	}
	if tag.RowsAffected() != 1 {
		return ErrIdentitySecretCleanupConflict
	}
	return nil
}

func (s *Store) RemovePendingIdentitySecretCleanup(ctx context.Context, reference string) (bool, error) {
	tag, err := s.pool.Exec(ctx, `DELETE FROM gateway_identity_secret_cleanup_queue
WHERE secret_ref=$1 AND status='pending'`, reference)
	if err != nil {
		return false, err
	}
	return tag.RowsAffected() == 1, nil
}

func (s *Store) ClaimIdentitySecretCleanups(ctx context.Context, limit int, lease time.Duration) ([]IdentitySecretCleanupClaim, error) {
	if limit <= 0 || limit > 1000 {
		limit = 100
	}
	if lease <= 0 || lease > time.Hour {
		lease = 2 * time.Minute
	}
	leaseSeconds := int64((lease + time.Second - 1) / time.Second)
	rows, err := s.pool.Query(ctx, `WITH candidates AS MATERIALIZED (
  SELECT secret_ref FROM gateway_identity_secret_cleanup_queue
  WHERE (status='pending' AND not_before <= now())
     OR (status='claimed' AND lease_expires_at <= now())
  ORDER BY COALESCE(lease_expires_at,not_before),created_at
  FOR UPDATE SKIP LOCKED
  LIMIT $1
)
UPDATE gateway_identity_secret_cleanup_queue queued
SET status='claimed',claim_token=gen_random_uuid(),
    lease_expires_at=now()+make_interval(secs => $2::int),updated_at=now()
FROM candidates
WHERE queued.secret_ref=candidates.secret_ref
RETURNING queued.secret_ref,queued.claim_token::text,queued.lease_expires_at`, limit, leaseSeconds)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	claims := make([]IdentitySecretCleanupClaim, 0)
	for rows.Next() {
		var claim IdentitySecretCleanupClaim
		if err := rows.Scan(&claim.Reference, &claim.ClaimToken, &claim.LeaseExpiresAt); err != nil {
			return nil, err
		}
		claims = append(claims, claim)
	}
	return claims, rows.Err()
}

func (s *Store) CompleteIdentitySecretCleanup(ctx context.Context, reference, claimToken string) (bool, error) {
	if _, err := uuid.Parse(claimToken); err != nil {
		return false, ErrIdentitySecretCleanupConflict
	}
	tag, err := s.pool.Exec(ctx, `DELETE FROM gateway_identity_secret_cleanup_queue
WHERE secret_ref=$1 AND status='claimed' AND claim_token=$2::uuid`, reference, claimToken)
	if err != nil {
		return false, err
	}
	return tag.RowsAffected() == 1, nil
}

func adoptPendingIdentitySecrets(ctx context.Context, tx pgx.Tx, references ...string) error {
	unique := make(map[string]struct{}, len(references))
	for _, reference := range references {
		if reference == "" {
			return ErrIdentitySecretCleanupConflict
		}
		unique[reference] = struct{}{}
	}
	values := make([]string, 0, len(unique))
	for reference := range unique {
		values = append(values, reference)
	}
	tag, err := tx.Exec(ctx, `DELETE FROM gateway_identity_secret_cleanup_queue
WHERE secret_ref=ANY($1) AND status='pending'`, values)
	if err != nil {
		return err
	}
	if tag.RowsAffected() != int64(len(values)) {
		return ErrIdentitySecretCleanupConflict
	}
	return nil
}

func queueIdentitySecretCleanupTx(ctx context.Context, tx pgx.Tx, reference string, notBefore time.Time) error {
	tag, err := tx.Exec(ctx, `INSERT INTO gateway_identity_secret_cleanup_queue(secret_ref,not_before)
VALUES($1,$2) ON CONFLICT(secret_ref) DO UPDATE
SET not_before=LEAST(gateway_identity_secret_cleanup_queue.not_before,EXCLUDED.not_before),updated_at=now()
WHERE gateway_identity_secret_cleanup_queue.status='pending'`, reference, notBefore)
	if err != nil {
		return err
	}
	if tag.RowsAffected() != 1 {
		return ErrIdentitySecretCleanupConflict
	}
	return nil
}

func (s *Store) SecurityEventConnectionIdempotency(ctx context.Context, operation, key string) (SecurityEventConnectionIdempotency, error) {
	var value SecurityEventConnectionIdempotency
	err := s.pool.QueryRow(ctx, `SELECT request_hash,response FROM gateway_security_event_connection_idempotency
WHERE operation=$1 AND idempotency_key=$2`, operation, key).Scan(&value.RequestHash, &value.Response)
	if errors.Is(err, pgx.ErrNoRows) {
		return SecurityEventConnectionIdempotency{}, ErrSecurityEventConnectionNotFound
	}
	return value, err
}

func (s *Store) RecordSecurityEventConnectionIdempotency(ctx context.Context, operation, key, requestHash string, response []byte) error {
	if !json.Valid(response) {
		return errors.New("security event idempotency response is invalid")
	}
	_, err := s.pool.Exec(ctx, `INSERT INTO gateway_security_event_connection_idempotency(operation,idempotency_key,request_hash,response)
VALUES($1,$2,$3,$4::jsonb) ON CONFLICT(operation,idempotency_key) DO NOTHING`, operation, key, requestHash, response)
	return err
}

func (s *Store) CreateSecurityEventConnection(ctx context.Context, input CreateSecurityEventConnectionInput) (SecurityEventConnection, error) {
	tx, err := s.pool.Begin(ctx)
	if err != nil {
		return SecurityEventConnection{}, err
	}
	defer rollbackTransaction(tx)
	_, err = tx.Exec(ctx, `
INSERT INTO gateway_security_event_connections(
  connection_id,transmitter_issuer,endpoint_url,credential_ref,management_client_id,management_credential_ref,lifecycle_status,idempotency_key
) VALUES($1::uuid,$2,$3,$4,$5,$6,'connecting',$7)
	`,
		input.ConnectionID, input.TransmitterIssuer, input.EndpointURL, input.CredentialRef,
		input.ManagementClientID, input.ManagementCredentialRef, input.IdempotencyKey,
	)
	if err != nil {
		if isUniqueViolation(err) {
			rollbackTransaction(tx)
			existing, getErr := s.SecurityEventConnection(ctx)
			if getErr == nil && existing.IdempotencyKey == input.IdempotencyKey && existing.TransmitterIssuer == input.TransmitterIssuer {
				return existing, nil
			}
			return SecurityEventConnection{}, ErrSecurityEventConnectionConflict
		}
		return SecurityEventConnection{}, err
	}
	if err := adoptPendingIdentitySecrets(ctx, tx, input.CredentialRef, input.ManagementCredentialRef); err != nil {
		return SecurityEventConnection{}, err
	}
	if err := tx.Commit(ctx); err != nil {
		return SecurityEventConnection{}, err
	}
	return s.SecurityEventConnection(ctx)
}

func (s *Store) SecurityEventConnection(ctx context.Context) (SecurityEventConnection, error) {
	var value SecurityEventConnection
	err := s.pool.QueryRow(ctx, `
SELECT connection.connection_id::text,connection.transmitter_issuer,connection.endpoint_url,connection.audience,
  connection.stream_id::text,connection.credential_ref,connection.next_credential_ref,
  COALESCE(connection.management_client_id,''),connection.management_credential_ref,connection.lifecycle_status,
  connection.version,connection.last_error_category,connection.idempotency_key,connection.created_at,connection.updated_at,
  state.last_verification_at,COALESCE(state.mode,'disabled')
FROM gateway_security_event_connections connection
LEFT JOIN gateway_security_event_stream_state state
  ON state.issuer=connection.transmitter_issuer AND state.audience=connection.audience
WHERE connection.singleton=true`).Scan(
		&value.ConnectionID, &value.TransmitterIssuer, &value.EndpointURL, &value.Audience, &value.StreamID,
		&value.CredentialRef, &value.NextCredentialRef, &value.ManagementClientID, &value.ManagementCredentialRef,
		&value.LifecycleStatus, &value.Version,
		&value.LastErrorCategory, &value.IdempotencyKey, &value.CreatedAt, &value.UpdatedAt,
		&value.LastVerificationAt, &value.HealthMode,
	)
	if errors.Is(err, pgx.ErrNoRows) {
		return SecurityEventConnection{}, ErrSecurityEventConnectionNotFound
	}
	return value, err
}

func (s *Store) BindSecurityEventConnection(ctx context.Context, connectionID, audience, streamID, lifecycle string, expectedVersion int64) (SecurityEventConnection, error) {
	tag, err := s.pool.Exec(ctx, `UPDATE gateway_security_event_connections
SET audience=$2,stream_id=$3::uuid,lifecycle_status=$4,last_error_category=NULL,version=version+1,updated_at=now()
WHERE connection_id=$1::uuid AND version=$5`, connectionID, audience, streamID, lifecycle, expectedVersion)
	if err != nil {
		return SecurityEventConnection{}, err
	}
	if tag.RowsAffected() == 0 {
		return SecurityEventConnection{}, ErrSecurityEventConnectionConflict
	}
	return s.SecurityEventConnection(ctx)
}

func (s *Store) SetSecurityEventManagementCredential(ctx context.Context, connectionID, clientID, reference string, expectedVersion int64) (SecurityEventConnection, error) {
	tx, err := s.pool.Begin(ctx)
	if err != nil {
		return SecurityEventConnection{}, err
	}
	defer rollbackTransaction(tx)
	var oldReference sql.NullString
	var lifecycle string
	var version int64
	if err := tx.QueryRow(ctx, `SELECT management_credential_ref,lifecycle_status,version FROM gateway_security_event_connections
WHERE connection_id=$1::uuid FOR UPDATE`, connectionID).Scan(&oldReference, &lifecycle, &version); errors.Is(err, pgx.ErrNoRows) {
		return SecurityEventConnection{}, ErrSecurityEventConnectionNotFound
	} else if err != nil {
		return SecurityEventConnection{}, err
	}
	if version != expectedVersion || lifecycle == "retiring" || lifecycle == "disconnect_pending" {
		return SecurityEventConnection{}, ErrSecurityEventConnectionConflict
	}
	tag, err := tx.Exec(ctx, `UPDATE gateway_security_event_connections
SET management_client_id=$2,management_credential_ref=$3,last_error_category=NULL,
  version=version+1,updated_at=now()
WHERE connection_id=$1::uuid AND version=$4
  AND lifecycle_status NOT IN ('retiring','disconnect_pending')`, connectionID, clientID, reference, expectedVersion)
	if err != nil {
		return SecurityEventConnection{}, err
	}
	if tag.RowsAffected() == 0 {
		return SecurityEventConnection{}, ErrSecurityEventConnectionConflict
	}
	if err := adoptPendingIdentitySecrets(ctx, tx, reference); err != nil {
		return SecurityEventConnection{}, err
	}
	if oldReference.Valid && oldReference.String != reference {
		if err := queueIdentitySecretCleanupTx(ctx, tx, oldReference.String, time.Now().UTC()); err != nil {
			return SecurityEventConnection{}, err
		}
	}
	if err := tx.Commit(ctx); err != nil {
		return SecurityEventConnection{}, err
	}
	return s.SecurityEventConnection(ctx)
}

func (s *Store) TransitionSecurityEventConnectionLifecycle(ctx context.Context, connectionID, expectedLifecycle string, expectedVersion int64, lifecycle string, errorCategory *string) (SecurityEventConnection, error) {
	tag, err := s.pool.Exec(ctx, `UPDATE gateway_security_event_connections
SET lifecycle_status=$4,last_error_category=$5,version=version+1,updated_at=now()
WHERE connection_id=$1::uuid AND lifecycle_status=$2 AND version=$3
  AND (lifecycle_status NOT IN ('retiring','disconnect_pending')
    OR $4 IN ('retiring','disconnect_pending'))`,
		connectionID, expectedLifecycle, expectedVersion, lifecycle, errorCategory)
	if err != nil {
		return SecurityEventConnection{}, err
	}
	if tag.RowsAffected() == 0 {
		return SecurityEventConnection{}, ErrSecurityEventConnectionConflict
	}
	return s.SecurityEventConnection(ctx)
}

func (s *Store) SetSecurityEventNextCredential(ctx context.Context, connectionID, reference string, expectedVersion int64) (SecurityEventConnection, error) {
	tx, err := s.pool.Begin(ctx)
	if err != nil {
		return SecurityEventConnection{}, err
	}
	defer rollbackTransaction(tx)
	tag, err := tx.Exec(ctx, `UPDATE gateway_security_event_connections
SET next_credential_ref=$2,lifecycle_status='rotating',last_error_category=NULL,version=version+1,updated_at=now()
WHERE connection_id=$1::uuid AND version=$3 AND next_credential_ref IS NULL
  AND lifecycle_status IN ('connecting','verifying','bootstrap','enabled','degraded','error')`, connectionID, reference, expectedVersion)
	if err != nil {
		return SecurityEventConnection{}, err
	}
	if tag.RowsAffected() == 0 {
		return SecurityEventConnection{}, ErrSecurityEventConnectionConflict
	}
	if err := adoptPendingIdentitySecrets(ctx, tx, reference); err != nil {
		return SecurityEventConnection{}, err
	}
	if err := tx.Commit(ctx); err != nil {
		return SecurityEventConnection{}, err
	}
	return s.SecurityEventConnection(ctx)
}

func (s *Store) PromoteSecurityEventCredential(ctx context.Context, connectionID, nextReference string) (SecurityEventConnection, error) {
	tx, err := s.pool.Begin(ctx)
	if err != nil {
		return SecurityEventConnection{}, err
	}
	defer rollbackTransaction(tx)
	var oldReference string
	if err := tx.QueryRow(ctx, `SELECT credential_ref FROM gateway_security_event_connections
WHERE connection_id=$1::uuid AND next_credential_ref=$2 AND lifecycle_status='rotating' FOR UPDATE`, connectionID, nextReference).Scan(&oldReference); errors.Is(err, pgx.ErrNoRows) {
		return SecurityEventConnection{}, ErrSecurityEventConnectionConflict
	} else if err != nil {
		return SecurityEventConnection{}, err
	}
	tag, err := tx.Exec(ctx, `UPDATE gateway_security_event_connections
SET credential_ref=$2,next_credential_ref=NULL,lifecycle_status='bootstrap',last_error_category=NULL,
  version=version+1,updated_at=now()
WHERE connection_id=$1::uuid AND next_credential_ref=$2 AND lifecycle_status='rotating'`, connectionID, nextReference)
	if err != nil {
		return SecurityEventConnection{}, err
	}
	if tag.RowsAffected() == 0 {
		return SecurityEventConnection{}, ErrSecurityEventConnectionConflict
	}
	if oldReference != nextReference {
		if err := queueIdentitySecretCleanupTx(ctx, tx, oldReference, time.Now().UTC()); err != nil {
			return SecurityEventConnection{}, err
		}
	}
	if err := tx.Commit(ctx); err != nil {
		return SecurityEventConnection{}, err
	}
	return s.SecurityEventConnection(ctx)
}

// DiscardPreparedSecurityEventConnection atomically retires the local Secrets
// and removes only the exact, still-unbound connection generation observed by
// the caller. A concurrent Bind or credential update changes the version and
// therefore preserves both the connection and its Secrets.
func (s *Store) DiscardPreparedSecurityEventConnection(ctx context.Context, connectionID, ownerKey string, expectedVersion int64) error {
	if _, err := uuid.Parse(connectionID); err != nil || ownerKey == "" || expectedVersion <= 0 {
		return ErrSecurityEventConnectionConflict
	}
	tx, err := s.pool.Begin(ctx)
	if err != nil {
		return err
	}
	defer rollbackTransaction(tx)

	var credentialReference string
	var nextReference, managementReference sql.NullString
	err = tx.QueryRow(ctx, `SELECT credential_ref,next_credential_ref,management_credential_ref
FROM gateway_security_event_connections
WHERE connection_id=$1::uuid AND idempotency_key=$2 AND version=$3 AND stream_id IS NULL
FOR UPDATE`, connectionID, ownerKey, expectedVersion).Scan(
		&credentialReference, &nextReference, &managementReference,
	)
	if errors.Is(err, pgx.ErrNoRows) {
		return ErrSecurityEventConnectionConflict
	}
	if err != nil {
		return err
	}

	references := map[string]struct{}{credentialReference: {}}
	if nextReference.Valid {
		references[nextReference.String] = struct{}{}
	}
	if managementReference.Valid {
		references[managementReference.String] = struct{}{}
	}
	now := time.Now().UTC()
	for reference := range references {
		if err := queueIdentitySecretCleanupTx(ctx, tx, reference, now); err != nil {
			return err
		}
	}
	tag, err := tx.Exec(ctx, `DELETE FROM gateway_security_event_connections
WHERE connection_id=$1::uuid AND idempotency_key=$2 AND version=$3 AND stream_id IS NULL`,
		connectionID, ownerKey, expectedVersion)
	if err != nil {
		return err
	}
	if tag.RowsAffected() != 1 {
		return ErrSecurityEventConnectionConflict
	}
	return tx.Commit(ctx)
}

// FinalizeRetiringSecurityEventConnection atomically transfers every Secret
// reference owned by the exact retiring generation to the durable cleanup
// queue before removing that generation. The SecretStore worker can therefore
// resume after a crash without the database losing its last cleanup intent.
func (s *Store) FinalizeRetiringSecurityEventConnection(ctx context.Context, connectionID string, expectedVersion int64) error {
	if _, err := uuid.Parse(connectionID); err != nil || expectedVersion <= 0 {
		return ErrSecurityEventConnectionConflict
	}
	tx, err := s.pool.Begin(ctx)
	if err != nil {
		return err
	}
	defer rollbackTransaction(tx)

	var credentialReference string
	var nextReference, managementReference sql.NullString
	err = tx.QueryRow(ctx, `SELECT credential_ref,next_credential_ref,management_credential_ref
FROM gateway_security_event_connections
WHERE connection_id=$1::uuid AND lifecycle_status='retiring' AND version=$2
FOR UPDATE`, connectionID, expectedVersion).Scan(
		&credentialReference, &nextReference, &managementReference,
	)
	if errors.Is(err, pgx.ErrNoRows) {
		return ErrSecurityEventConnectionConflict
	}
	if err != nil {
		return err
	}

	references := map[string]struct{}{credentialReference: {}}
	if nextReference.Valid {
		references[nextReference.String] = struct{}{}
	}
	if managementReference.Valid {
		references[managementReference.String] = struct{}{}
	}
	now := time.Now().UTC()
	for reference := range references {
		if err := queueIdentitySecretCleanupTx(ctx, tx, reference, now); err != nil {
			return err
		}
	}
	tag, err := tx.Exec(ctx, `DELETE FROM gateway_security_event_connections
WHERE connection_id=$1::uuid AND lifecycle_status='retiring' AND version=$2`, connectionID, expectedVersion)
	if err != nil {
		return err
	}
	if tag.RowsAffected() != 1 {
		return ErrSecurityEventConnectionConflict
	}
	return tx.Commit(ctx)
}
