package serving

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

	"github.com/pingcap-inc/nexuscrm-connectors/internal/sqltx"
)

// ProfiledEpochVerificationState is the durable result of the sole Profile V2
// pre-activation transition. Prepared is deliberately not a result: retryable
// external-owner failures return an error and leave the stored epoch prepared.
type ProfiledEpochVerificationState string

const (
	ProfiledEpochVerified ProfiledEpochVerificationState = verifiedState
	ProfiledEpochRejected ProfiledEpochVerificationState = rejectedState
)

// ProfiledExternalAuthorityVerificationResult exposes only the scalar durable
// result of fresh-owner verification. It carries neither the private candidate
// graph nor a reusable assessment that could authorize a later transition.
type ProfiledExternalAuthorityVerificationResult struct {
	epochID     string
	epochDigest string
	state       ProfiledEpochVerificationState
	applied     bool
}

func (result ProfiledExternalAuthorityVerificationResult) EpochID() string {
	return result.epochID
}

func (result ProfiledExternalAuthorityVerificationResult) EpochDigest() string {
	return result.epochDigest
}

func (result ProfiledExternalAuthorityVerificationResult) State() ProfiledEpochVerificationState {
	return result.state
}

// Applied reports whether this call performed the prepared-state CAS. False
// means the exact immutable epoch had already reached the same durable result.
func (result ProfiledExternalAuthorityVerificationResult) Applied() bool {
	return result.applied
}

// ProfiledExternalAuthorityVerificationService owns the only durable Profile
// V2 prepared -> verified|rejected transition. Callers supply only an exact
// epoch ID; the service fresh-assesses and consumes its private evidence in the
// same operation, so an old or caller-echoed assessment cannot be replayed.
type ProfiledExternalAuthorityVerificationService struct {
	repository *Repository
	assessor   *ProfiledExternalAuthorityAssessor
}

func NewProfiledExternalAuthorityVerificationService(
	repository *Repository,
	catalog CurrentIntegrationBindingViewReader,
	bindingOwners ExactBindingOwnerReader,
	serviceConfigurations ExactServiceConfigurationReader,
	credentialGenerations ExactCredentialGenerationReader,
) (*ProfiledExternalAuthorityVerificationService, error) {
	if repository == nil || repository.db == nil {
		return nil, fmt.Errorf("%w: profiled verification repository is nil", ErrDependencyMissing)
	}
	assessor, err := NewProfiledExternalAuthorityAssessor(
		repository,
		catalog,
		bindingOwners,
		serviceConfigurations,
		credentialGenerations,
	)
	if err != nil {
		return nil, err
	}
	return newProfiledExternalAuthorityVerificationService(repository, assessor)
}

func newProfiledExternalAuthorityVerificationService(
	repository *Repository,
	assessor *ProfiledExternalAuthorityAssessor,
) (*ProfiledExternalAuthorityVerificationService, error) {
	if repository == nil || repository.db == nil || assessor == nil {
		return nil, fmt.Errorf("%w: profiled verification service dependency is nil", ErrDependencyMissing)
	}
	return &ProfiledExternalAuthorityVerificationService{
		repository: repository,
		assessor:   assessor,
	}, nil
}

// Verify fresh-assesses one exact locally persisted Profile V2 candidate and
// durably converges it from prepared to verified or rejected. A valid terminal
// epoch is returned idempotently without rereading mutable external owners.
func (service *ProfiledExternalAuthorityVerificationService) Verify(
	ctx context.Context,
	exactEpochID string,
) (ProfiledExternalAuthorityVerificationResult, error) {
	if service == nil || service.repository == nil || service.repository.db == nil || service.assessor == nil {
		return ProfiledExternalAuthorityVerificationResult{}, fmt.Errorf(
			"%w: profiled verification service is unavailable",
			ErrDependencyMissing,
		)
	}
	if ctx == nil {
		return ProfiledExternalAuthorityVerificationResult{}, fmt.Errorf("%w: context is nil", ErrDependencyMissing)
	}
	if !validOpaqueIdentifierWithin(exactEpochID, connectorDigestProfileV2MaxIdentifierBytes) {
		return ProfiledExternalAuthorityVerificationResult{}, invalidCandidate("epoch ID is not canonical")
	}
	if err := service.repository.VerifySchema(ctx); err != nil {
		return ProfiledExternalAuthorityVerificationResult{}, err
	}

	current, found, err := loadProfiledPreActivationEpoch(
		ctx,
		service.repository.db,
		exactEpochID,
		false,
	)
	if err != nil {
		return ProfiledExternalAuthorityVerificationResult{}, err
	}
	if !found {
		return ProfiledExternalAuthorityVerificationResult{}, profiledPreparedEpochNotFound(exactEpochID)
	}
	if current.state != preparedState {
		return profiledVerificationResult(current, false)
	}

	evidence, err := service.assessor.assessCandidateEvidence(ctx, current.candidate)
	if err != nil {
		return ProfiledExternalAuthorityVerificationResult{}, err
	}

	target, err := profiledVerificationTarget(evidence.assessment.disposition)
	if err != nil {
		return ProfiledExternalAuthorityVerificationResult{}, err
	}
	return service.repository.transitionProfiledPrepared(
		ctx,
		evidence.candidate,
		target,
	)
}

func profiledVerificationTarget(
	disposition ProfiledExternalAuthorityDisposition,
) (ProfiledEpochVerificationState, error) {
	switch disposition {
	case ProfiledExternalAuthorityExactMatch:
		return ProfiledEpochVerified, nil
	case ProfiledExternalAuthorityRejected:
		return ProfiledEpochRejected, nil
	default:
		return "", fmt.Errorf("external authority assessment returned disposition %q", disposition)
	}
}

func (repository *Repository) transitionProfiledPrepared(
	ctx context.Context,
	expected ProfiledPreparedEpoch,
	target ProfiledEpochVerificationState,
) (ProfiledExternalAuthorityVerificationResult, error) {
	if repository == nil || repository.db == nil {
		return ProfiledExternalAuthorityVerificationResult{}, fmt.Errorf(
			"%w: profiled verification repository is nil",
			ErrDependencyMissing,
		)
	}
	if ctx == nil {
		return ProfiledExternalAuthorityVerificationResult{}, fmt.Errorf("%w: context is nil", ErrDependencyMissing)
	}
	if err := expected.validateStored(); err != nil {
		return ProfiledExternalAuthorityVerificationResult{}, fmt.Errorf("%w: %v", ErrInvalidCandidate, err)
	}
	if target != ProfiledEpochVerified && target != ProfiledEpochRejected {
		return ProfiledExternalAuthorityVerificationResult{}, fmt.Errorf(
			"%w: unsupported profiled verification target %q",
			ErrProfiledEpochStateConflict,
			target,
		)
	}
	if err := repository.VerifySchema(ctx); err != nil {
		return ProfiledExternalAuthorityVerificationResult{}, err
	}

	tx, cleanup, err := sqltx.BeginPessimisticTx(ctx, repository.db)
	if err != nil {
		return ProfiledExternalAuthorityVerificationResult{}, fmt.Errorf(
			"begin profiled verification transition: %w",
			err,
		)
	}
	rollback := func(cause error) (ProfiledExternalAuthorityVerificationResult, error) {
		return ProfiledExternalAuthorityVerificationResult{}, errors.Join(cause, tx.Rollback(), cleanup())
	}
	commit := func() error {
		return errors.Join(tx.Commit(), cleanup())
	}

	locked, found, err := loadProfiledPreActivationEpoch(ctx, tx, expected.id, true)
	if err != nil {
		return rollback(err)
	}
	if !found {
		return rollback(profiledPreparedEpochNotFound(expected.id))
	}
	if !locked.candidate.equalImmutable(expected) {
		return rollback(fmt.Errorf(
			"%w: assessed epoch no longer matches the locked immutable graph",
			ErrIdentityConflict,
		))
	}

	targetState := string(target)
	switch locked.state {
	case targetState:
		if err := commit(); err != nil {
			return ProfiledExternalAuthorityVerificationResult{}, fmt.Errorf(
				"commit exact profiled verification convergence: %w",
				err,
			)
		}
		return profiledVerificationResult(locked, false)
	case preparedState:
		// Continue to the sole guarded state write below.
	case verifiedState, rejectedState:
		return rollback(fmt.Errorf(
			"%w: assessed target=%q stored state=%q",
			ErrProfiledEpochStateConflict,
			target,
			locked.state,
		))
	default:
		return rollback(storedCandidateInvalid(
			"profiled verification locked an unsupported state %q",
			locked.state,
		))
	}

	if err := updateProfiledPreparedState(ctx, tx, expected, target); err != nil {
		return rollback(err)
	}
	readback, found, err := loadProfiledPreActivationEpoch(ctx, tx, expected.id, true)
	if err != nil {
		return rollback(err)
	}
	if !found || readback.state != targetState ||
		!readback.candidate.equalImmutable(expected) {
		return rollback(storedCandidateInvalid(
			"profiled verification readback disagrees with the guarded transition",
		))
	}
	if err := commit(); err != nil {
		return ProfiledExternalAuthorityVerificationResult{}, fmt.Errorf(
			"commit profiled verification transition: %w",
			err,
		)
	}
	return profiledVerificationResult(readback, true)
}

func updateProfiledPreparedState(
	ctx context.Context,
	tx *sql.Tx,
	expected ProfiledPreparedEpoch,
	target ProfiledEpochVerificationState,
) error {
	result, err := tx.ExecContext(ctx, `
UPDATE _System_ConnectorServingEpoch
   SET State = ?, LastModifiedDate = CURRENT_TIMESTAMP(6)
 WHERE Id = ? AND BINARY Id = BINARY ?
   AND EpochDigest = ? AND BINARY EpochDigest = BINARY ?
   AND State = ? AND BINARY State = BINARY ?
   AND ActiveScopeDigest IS NULL`,
		string(target),
		expected.id,
		expected.id,
		expected.epochDigest,
		expected.epochDigest,
		preparedState,
		preparedState,
	)
	if err != nil {
		return fmt.Errorf("update profiled verification state: %w", err)
	}
	return requireOneProfiledVerificationRow(result)
}

func requireOneProfiledVerificationRow(result sql.Result) error {
	rows, err := result.RowsAffected()
	if err != nil {
		return fmt.Errorf("profiled verification rows affected: %w", err)
	}
	if rows != 1 {
		return fmt.Errorf(
			"%w: profiled verification rows affected = %d, want 1",
			ErrProfiledEpochStateConflict,
			rows,
		)
	}
	return nil
}

func profiledVerificationResult(
	epoch profiledPreActivationEpoch,
	applied bool,
) (ProfiledExternalAuthorityVerificationResult, error) {
	state := ProfiledEpochVerificationState(epoch.state)
	if state != ProfiledEpochVerified && state != ProfiledEpochRejected {
		return ProfiledExternalAuthorityVerificationResult{}, fmt.Errorf(
			"%w: profiled verification result state is %q",
			ErrProfiledEpochStateConflict,
			epoch.state,
		)
	}
	return ProfiledExternalAuthorityVerificationResult{
		epochID:     epoch.candidate.id,
		epochDigest: epoch.candidate.epochDigest,
		state:       state,
		applied:     applied,
	}, nil
}

func profiledPreparedEpochNotFound(id string) error {
	return fmt.Errorf("%w: %q", ErrProfiledPreparedEpochNotFound, id)
}
