package store

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

	"github.com/easyai/easyai-ai-gateway/apps/api/internal/identity"
	"github.com/jackc/pgx/v5"
)

const identityPairingColumns = `
id::text,revision_id::text,remote_exchange_id::text,exchange_token_ref,status,cleanup_status,remote_version,expires_at,version,
COALESCE(last_error_category,''),COALESCE(auth_center_audit_id,''),COALESCE(last_trace_id,''),cancelled_at,cleanup_completed_at,
created_at,updated_at`

func (s *Store) ReserveIdentityPairingStart(ctx context.Context, attemptID string, expiresAt time.Time) error {
	tx, err := s.beginIdentityConfigurationLifecycleTx(ctx)
	if err != nil {
		return err
	}
	defer rollbackTransaction(tx)
	if _, err := tx.Exec(ctx, `DELETE FROM gateway_identity_pairing_start_reservation
WHERE state='starting' AND expires_at <= now()`); err != nil {
		return err
	}
	var activeConfigurationExists bool
	if err := tx.QueryRow(ctx, `SELECT EXISTS(
  SELECT 1 FROM gateway_identity_configuration_revisions
  WHERE state='active'
)`).Scan(&activeConfigurationExists); err != nil {
		return err
	}
	if activeConfigurationExists {
		return identity.ErrActiveConfigurationHandoffRequired
	}
	var blocked bool
	if err := tx.QueryRow(ctx, `SELECT EXISTS(
  SELECT 1 FROM gateway_identity_configuration_revisions revision
  JOIN gateway_identity_onboarding_exchanges exchange ON exchange.revision_id=revision.id
  WHERE revision.state IN ('draft','validated','failed')
    AND NOT (exchange.status='cancelled' AND exchange.cleanup_status='completed')
)`).Scan(&blocked); err != nil {
		return err
	}
	if blocked {
		return identity.ErrPairingInProgress
	}
	if _, err := tx.Exec(ctx, `INSERT INTO gateway_identity_pairing_start_reservation(singleton,attempt_id,state,expires_at)
VALUES(true,$1::uuid,'starting',$2)`, attemptID, expiresAt); err != nil {
		if isUniqueViolation(err) {
			return identity.ErrPairingInProgress
		}
		return err
	}
	return tx.Commit(ctx)
}

func (s *Store) ReleaseIdentityPairingStart(ctx context.Context, attemptID string) error {
	_, err := s.pool.Exec(ctx, `DELETE FROM gateway_identity_pairing_start_reservation
WHERE attempt_id=$1::uuid AND state='starting'`, attemptID)
	return err
}

func (s *Store) CommitIdentityPairingStart(ctx context.Context, revision identity.Revision, exchange identity.PairingExchange) (identity.PairingExchange, error) {
	tx, err := s.beginIdentityConfigurationLifecycleTx(ctx)
	if err != nil {
		return identity.PairingExchange{}, err
	}
	defer rollbackTransaction(tx)
	var reserved bool
	if err := tx.QueryRow(ctx, `SELECT true FROM gateway_identity_pairing_start_reservation
WHERE singleton=true AND attempt_id=$1::uuid AND state='starting' FOR UPDATE`, exchange.ID).Scan(&reserved); errors.Is(err, pgx.ErrNoRows) {
		return identity.PairingExchange{}, identity.ErrPairingInProgress
	} else if err != nil {
		return identity.PairingExchange{}, err
	}
	if !reserved {
		return identity.PairingExchange{}, identity.ErrPairingInProgress
	}
	var activeConfigurationExists bool
	if err := tx.QueryRow(ctx, `SELECT EXISTS(
  SELECT 1 FROM gateway_identity_configuration_revisions WHERE state='active'
)`).Scan(&activeConfigurationExists); err != nil {
		return identity.PairingExchange{}, err
	}
	if activeConfigurationExists {
		return identity.PairingExchange{}, identity.ErrActiveConfigurationHandoffRequired
	}
	if err := adoptPendingIdentitySecrets(ctx, tx, exchange.ExchangeTokenRef); err != nil {
		return identity.PairingExchange{}, err
	}
	scopes, _ := json.Marshal(revision.Scopes)
	capabilities, _ := json.Marshal(revision.Capabilities)
	if _, err := tx.Exec(ctx, `INSERT INTO gateway_identity_configuration_revisions (
  id,state,schema_version,auth_center_url,role_prefix,local_tenant_key,public_base_url,web_base_url,
  jit_enabled,legacy_jwt_enabled,scopes,capabilities,session_idle_seconds,session_absolute_seconds,session_refresh_seconds,last_trace_id
) VALUES ($1::uuid,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11::jsonb,$12::jsonb,$13,$14,$15,NULLIF($16,''))`,
		revision.ID, revision.State, revision.SchemaVersion, revision.AuthCenterURL, revision.RolePrefix,
		revision.LocalTenantKey, revision.PublicBaseURL, revision.WebBaseURL, revision.JITEnabled, revision.LegacyJWTEnabled,
		string(scopes), string(capabilities), revision.SessionIdleSeconds, revision.SessionAbsoluteSeconds,
		revision.SessionRefreshSeconds, revision.LastTraceID,
	); err != nil {
		return identity.PairingExchange{}, err
	}
	created, err := scanIdentityPairing(tx.QueryRow(ctx, `INSERT INTO gateway_identity_onboarding_exchanges (
  id,revision_id,remote_exchange_id,exchange_token_ref,status,cleanup_status,remote_version,expires_at,last_trace_id
) VALUES ($1::uuid,$2::uuid,$3::uuid,$4,$5,$6,$7,$8,NULLIF($9,''))
RETURNING `+identityPairingColumns,
		exchange.ID, revision.ID, exchange.RemoteExchangeID, exchange.ExchangeTokenRef,
		exchange.Status, exchange.CleanupStatus, exchange.RemoteVersion, exchange.ExpiresAt, exchange.LastTraceID,
	))
	if err != nil {
		return identity.PairingExchange{}, err
	}
	tag, err := tx.Exec(ctx, `UPDATE gateway_identity_pairing_start_reservation
SET state='paired',revision_id=$2::uuid,expires_at=$3,updated_at=now()
WHERE attempt_id=$1::uuid AND state='starting'`, exchange.ID, revision.ID, exchange.ExpiresAt)
	if err != nil {
		return identity.PairingExchange{}, err
	}
	if tag.RowsAffected() != 1 {
		return identity.PairingExchange{}, identity.ErrPairingInProgress
	}
	if err := tx.Commit(ctx); err != nil {
		return identity.PairingExchange{}, err
	}
	return created, nil
}

func (s *Store) IdentityPairingStartBlocked(ctx context.Context) (bool, error) {
	var blocked bool
	err := s.pool.QueryRow(ctx, `
SELECT EXISTS(
	  SELECT 1 FROM gateway_identity_configuration_revisions revision
	  JOIN gateway_identity_onboarding_exchanges exchange ON exchange.revision_id=revision.id
	  WHERE revision.state IN ('draft','validated','failed')
	    AND NOT (exchange.status='cancelled' AND exchange.cleanup_status='completed')
  ) OR EXISTS (
    SELECT 1 FROM gateway_identity_pairing_start_reservation
    WHERE state='paired' OR expires_at > now()
)`).Scan(&blocked)
	return blocked, err
}

func (s *Store) CreateIdentityPairingExchange(ctx context.Context, exchange identity.PairingExchange) (identity.PairingExchange, error) {
	return scanIdentityPairing(s.pool.QueryRow(ctx, `
INSERT INTO gateway_identity_onboarding_exchanges (
  id,revision_id,remote_exchange_id,exchange_token_ref,status,cleanup_status,remote_version,expires_at,last_trace_id
) VALUES ($1::uuid,$2::uuid,$3::uuid,$4,$5,$6,$7,$8,NULLIF($9,''))
RETURNING `+identityPairingColumns,
		exchange.ID, exchange.RevisionID, exchange.RemoteExchangeID, exchange.ExchangeTokenRef,
		exchange.Status, exchange.CleanupStatus, exchange.RemoteVersion, exchange.ExpiresAt, exchange.LastTraceID,
	))
}

func (s *Store) IdentityPairingExchange(ctx context.Context, id string) (identity.PairingExchange, error) {
	exchange, err := scanIdentityPairing(s.pool.QueryRow(ctx, `SELECT `+identityPairingColumns+`
FROM gateway_identity_onboarding_exchanges WHERE id=$1::uuid`, id))
	if errors.Is(err, pgx.ErrNoRows) {
		return identity.PairingExchange{}, identity.ErrRevisionNotFound
	}
	return exchange, err
}

func (s *Store) IdentityPairingExchangeForRevision(ctx context.Context, revisionID string) (identity.PairingExchange, error) {
	exchange, err := scanIdentityPairing(s.pool.QueryRow(ctx, `SELECT `+identityPairingColumns+`
FROM gateway_identity_onboarding_exchanges WHERE revision_id=$1::uuid`, revisionID))
	if errors.Is(err, pgx.ErrNoRows) {
		return identity.PairingExchange{}, identity.ErrRevisionNotFound
	}
	return exchange, err
}

func (s *Store) PendingIdentityPairingExchanges(ctx context.Context) ([]identity.PairingExchange, error) {
	rows, err := s.pool.Query(ctx, `SELECT `+identityPairingColumns+`
FROM gateway_identity_onboarding_exchanges exchange
WHERE exchange.id=(
  SELECT candidate_exchange.id
  FROM gateway_identity_onboarding_exchanges candidate_exchange
  JOIN gateway_identity_configuration_revisions revision ON revision.id=candidate_exchange.revision_id
  WHERE revision.state IN ('draft','validated','failed')
  ORDER BY NOT (candidate_exchange.status='cancelled' AND candidate_exchange.cleanup_status='completed') DESC,
    revision.created_at DESC,candidate_exchange.created_at DESC
  LIMIT 1
)
AND exchange.status IN ('metadata_pending','preparing','ready','credentials_saved')`)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	exchanges := make([]identity.PairingExchange, 0)
	for rows.Next() {
		exchange, scanErr := scanIdentityPairing(rows)
		if scanErr != nil {
			return nil, scanErr
		}
		exchanges = append(exchanges, exchange)
	}
	return exchanges, rows.Err()
}

func (s *Store) PendingIdentityPairingCleanups(ctx context.Context) ([]identity.PairingExchange, error) {
	rows, err := s.pool.Query(ctx, `SELECT `+identityPairingColumns+`
FROM gateway_identity_onboarding_exchanges
WHERE status='cancelled' AND cleanup_status='pending' ORDER BY updated_at`)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	exchanges := make([]identity.PairingExchange, 0)
	for rows.Next() {
		exchange, scanErr := scanIdentityPairing(rows)
		if scanErr != nil {
			return nil, scanErr
		}
		exchanges = append(exchanges, exchange)
	}
	return exchanges, rows.Err()
}

func (s *Store) CompletedIdentityPairingsAwaitingActivation(ctx context.Context) ([]identity.PairingExchange, error) {
	rows, err := s.pool.Query(ctx, `SELECT `+identityPairingColumns+`
FROM gateway_identity_onboarding_exchanges exchange
WHERE exchange.id=(
  SELECT candidate_exchange.id
  FROM gateway_identity_onboarding_exchanges candidate_exchange
  JOIN gateway_identity_configuration_revisions revision ON revision.id=candidate_exchange.revision_id
  WHERE revision.state IN ('draft','validated','failed')
  ORDER BY NOT (candidate_exchange.status='cancelled' AND candidate_exchange.cleanup_status='completed') DESC,
    revision.created_at DESC,candidate_exchange.created_at DESC
  LIMIT 1
)
AND exchange.status='completed' AND EXISTS (
  SELECT 1 FROM gateway_identity_configuration_revisions revision
  WHERE revision.id=exchange.revision_id AND revision.state IN ('draft','validated')
)`)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	exchanges := make([]identity.PairingExchange, 0)
	for rows.Next() {
		exchange, scanErr := scanIdentityPairing(rows)
		if scanErr != nil {
			return nil, scanErr
		}
		exchanges = append(exchanges, exchange)
	}
	return exchanges, rows.Err()
}

func (s *Store) UpdateIdentityPairingExchange(ctx context.Context, id string, expectedVersion int64, update identity.PairingExchangeUpdate) (identity.PairingExchange, error) {
	tx, err := s.pool.Begin(ctx)
	if err != nil {
		return identity.PairingExchange{}, err
	}
	defer rollbackTransaction(tx)
	exchange, err := scanIdentityPairing(tx.QueryRow(ctx, `
UPDATE gateway_identity_onboarding_exchanges SET status=$3,remote_version=$4,last_error_category=NULLIF($5,''),
auth_center_audit_id=COALESCE(NULLIF($6,''),auth_center_audit_id),version=version+1,updated_at=now()
WHERE id=$1::uuid AND version=$2
RETURNING `+identityPairingColumns,
		id, expectedVersion, update.Status, update.RemoteVersion, update.LastErrorCategory, update.AuthCenterAuditID,
	))
	if errors.Is(err, pgx.ErrNoRows) {
		return identity.PairingExchange{}, identity.ErrRevisionConflict
	}
	if err != nil {
		return identity.PairingExchange{}, err
	}
	if update.Status == identity.PairingCompleted || update.Status == identity.PairingFailed || update.Status == identity.PairingExpired {
		if err := queueIdentitySecretCleanupTx(ctx, tx, exchange.ExchangeTokenRef, time.Now().UTC()); err != nil {
			return identity.PairingExchange{}, err
		}
	}
	if err := tx.Commit(ctx); err != nil {
		return identity.PairingExchange{}, err
	}
	return exchange, nil
}

func (s *Store) RecordIdentityPairingRetryFailure(ctx context.Context, id string, status identity.PairingStatus, category string) (identity.PairingExchange, error) {
	exchange, err := scanIdentityPairing(s.pool.QueryRow(ctx, `
UPDATE gateway_identity_onboarding_exchanges SET last_error_category=$3,updated_at=now()
WHERE id=$1::uuid AND status=$2 AND cleanup_status='none'
RETURNING `+identityPairingColumns, id, status, category))
	if errors.Is(err, pgx.ErrNoRows) {
		return identity.PairingExchange{}, identity.ErrRevisionConflict
	}
	return exchange, err
}

func (s *Store) CancelIdentityPairingExchange(ctx context.Context, id string, expectedVersion int64, traceID, auditID string) (identity.PairingExchange, error) {
	tx, err := s.pool.BeginTx(ctx, pgx.TxOptions{IsoLevel: pgx.Serializable})
	if err != nil {
		return identity.PairingExchange{}, err
	}
	defer rollbackTransaction(tx)
	exchange, err := scanIdentityPairing(tx.QueryRow(ctx, `SELECT `+identityPairingColumns+`
FROM gateway_identity_onboarding_exchanges WHERE id=$1::uuid FOR UPDATE`, id))
	if errors.Is(err, pgx.ErrNoRows) {
		return identity.PairingExchange{}, identity.ErrRevisionNotFound
	}
	if err != nil {
		return identity.PairingExchange{}, err
	}
	if exchange.Status == identity.PairingCancelled {
		return exchange, tx.Commit(ctx)
	}
	if exchange.Version != expectedVersion {
		return identity.PairingExchange{}, identity.ErrRevisionConflict
	}
	revision, err := scanIdentityRevision(tx.QueryRow(ctx, `SELECT `+identityRevisionColumns+`
FROM gateway_identity_configuration_revisions WHERE id=$1::uuid FOR UPDATE`, exchange.RevisionID))
	if err != nil {
		return identity.PairingExchange{}, normalizeIdentityRevisionError(err)
	}
	if revision.State != identity.RevisionDraft && revision.State != identity.RevisionValidated && revision.State != identity.RevisionFailed {
		return identity.PairingExchange{}, identity.ErrPairingNotCancellable
	}
	// Before Manifest delivery, issuer is NULL and the schema requires the
	// revision to remain draft. The cancelled pairing is the terminal record.
	if _, err := tx.Exec(ctx, `UPDATE gateway_identity_configuration_revisions SET
state=CASE WHEN issuer IS NULL AND state IN ('draft','failed') THEN 'draft' ELSE 'failed' END,
last_error_category='pairing_cancelled',last_trace_id=NULLIF($2,''),last_audit_id=NULLIF($3,''),
version=version+1,updated_at=now() WHERE id=$1::uuid`, exchange.RevisionID, traceID, auditID); err != nil {
		return identity.PairingExchange{}, err
	}
	exchange, err = scanIdentityPairing(tx.QueryRow(ctx, `
UPDATE gateway_identity_onboarding_exchanges SET status='cancelled',cleanup_status='pending',cancelled_at=now(),
cleanup_completed_at=NULL,version=version+1,updated_at=now() WHERE id=$1::uuid AND version=$2
RETURNING `+identityPairingColumns, id, expectedVersion))
	if errors.Is(err, pgx.ErrNoRows) {
		return identity.PairingExchange{}, identity.ErrRevisionConflict
	}
	if err != nil {
		return identity.PairingExchange{}, err
	}
	if err := queueIdentitySecretCleanupTx(ctx, tx, exchange.ExchangeTokenRef, time.Now().UTC()); err != nil {
		return identity.PairingExchange{}, err
	}
	if err := tx.Commit(ctx); err != nil {
		return identity.PairingExchange{}, err
	}
	return exchange, nil
}

func (s *Store) CompleteIdentityPairingCleanup(ctx context.Context, id string, expectedVersion int64) (identity.PairingExchange, error) {
	tx, err := s.pool.BeginTx(ctx, pgx.TxOptions{IsoLevel: pgx.Serializable})
	if err != nil {
		return identity.PairingExchange{}, err
	}
	defer rollbackTransaction(tx)
	var revisionID string
	if err := tx.QueryRow(ctx, `SELECT revision_id::text FROM gateway_identity_onboarding_exchanges
WHERE id=$1::uuid AND version=$2 AND status='cancelled' AND cleanup_status='pending' FOR UPDATE`, id, expectedVersion).Scan(&revisionID); errors.Is(err, pgx.ErrNoRows) {
		return identity.PairingExchange{}, identity.ErrRevisionConflict
	} else if err != nil {
		return identity.PairingExchange{}, err
	}
	tag, err := tx.Exec(ctx, `UPDATE gateway_identity_configuration_revisions SET machine_credential_ref=NULL,
		session_encryption_key_ref=NULL,version=version+1,updated_at=now()
WHERE id=$1::uuid AND (
  state='failed' OR (state='draft' AND issuer IS NULL AND last_error_category='pairing_cancelled')
)`, revisionID)
	if err != nil {
		return identity.PairingExchange{}, err
	}
	if tag.RowsAffected() != 1 {
		return identity.PairingExchange{}, identity.ErrRevisionConflict
	}
	exchange, err := scanIdentityPairing(tx.QueryRow(ctx, `UPDATE gateway_identity_onboarding_exchanges
SET cleanup_status='completed',cleanup_completed_at=now(),version=version+1,updated_at=now()
WHERE id=$1::uuid AND version=$2 AND status='cancelled' AND cleanup_status='pending'
RETURNING `+identityPairingColumns, id, expectedVersion))
	if err != nil {
		return identity.PairingExchange{}, err
	}
	if _, err := tx.Exec(ctx, `DELETE FROM gateway_identity_pairing_start_reservation WHERE revision_id=$1::uuid`, revisionID); err != nil {
		return identity.PairingExchange{}, err
	}
	if err := tx.Commit(ctx); err != nil {
		return identity.PairingExchange{}, err
	}
	return exchange, nil
}

func (s *Store) RecordIdentityPairingCleanupFailure(ctx context.Context, id, category string) (identity.PairingExchange, error) {
	exchange, err := scanIdentityPairing(s.pool.QueryRow(ctx, `UPDATE gateway_identity_onboarding_exchanges
SET last_error_category=$2,updated_at=now() WHERE id=$1::uuid AND status='cancelled' AND cleanup_status='pending'
RETURNING `+identityPairingColumns, id, category))
	if errors.Is(err, pgx.ErrNoRows) {
		return identity.PairingExchange{}, identity.ErrRevisionConflict
	}
	return exchange, err
}

func scanIdentityPairing(row scanner) (identity.PairingExchange, error) {
	var exchange identity.PairingExchange
	var status string
	if err := row.Scan(
		&exchange.ID, &exchange.RevisionID, &exchange.RemoteExchangeID, &exchange.ExchangeTokenRef, &status, &exchange.CleanupStatus,
		&exchange.RemoteVersion, &exchange.ExpiresAt, &exchange.Version, &exchange.LastErrorCategory,
		&exchange.AuthCenterAuditID, &exchange.LastTraceID, &exchange.CancelledAt, &exchange.CleanupCompletedAt,
		&exchange.CreatedAt, &exchange.UpdatedAt,
	); err != nil {
		return identity.PairingExchange{}, err
	}
	exchange.Status = identity.PairingStatus(status)
	return exchange, nil
}
