package permissions

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

	appErrors "github.com/nexuscrm/kernel/pkg/errors"
	"github.com/nexuscrm/kernel/pkg/ports"
	"github.com/nexuscrm/kernel/pkg/schemacatalog"
	"github.com/nexuscrm/shared/pkg/constants"
)

const assignmentPolicyAuthorityQuery = "SELECT c.`Id`, COALESCE(s.`Suspended`, 0) " +
	"FROM `" + schemacatalog.AssignmentPolicyClaimTable + "` c " +
	"LEFT JOIN `" + schemacatalog.AssignmentPolicyTargetStateTable + "` s " +
	"ON s.`PermissionSetId` = c.`PermissionSetId` " +
	"WHERE c.`PermissionSetId` = ? LIMIT 1 FOR UPDATE"

// AssignmentPolicyDirectWriteAuthority is the sole active-claim authority used
// by ordinary PermissionSetAssignment writes. Records calls it only from the
// metadata-declared L0 security adapter after every old and new PermissionSet
// parent is locked in the caller-owned pessimistic transaction.
type AssignmentPolicyDirectWriteAuthority interface {
	RejectOrdinaryAssignmentMutation(context.Context, string) error
}

type SQLAssignmentPolicyDirectWriteAuthority struct {
	txManager ports.TransactionManager
}

func NewSQLAssignmentPolicyDirectWriteAuthority(
	txManager ports.TransactionManager,
) (*SQLAssignmentPolicyDirectWriteAuthority, error) {
	if txManager == nil {
		return nil, fmt.Errorf("assignment policy authority requires a transaction manager")
	}
	return &SQLAssignmentPolicyDirectWriteAuthority{txManager: txManager}, nil
}

func (authority *SQLAssignmentPolicyDirectWriteAuthority) RejectOrdinaryAssignmentMutation(
	ctx context.Context,
	permissionSetID string,
) error {
	permissionSetID = strings.TrimSpace(permissionSetID)
	if permissionSetID == "" {
		return appErrors.NewInternalError(
			"assignment policy authority requires PermissionSetId", nil,
		)
	}
	tx, err := authority.transaction(ctx)
	if err != nil {
		return err
	}

	// The PermissionSet parent lock serializes claim creation/release. This is a
	// locking current read because a snapshot read after the parent lock could
	// still miss a claim committed immediately before lock acquisition.
	var claimID string
	var suspended bool
	err = tx.QueryRowContext(ctx, assignmentPolicyAuthorityQuery, permissionSetID).Scan(&claimID, &suspended)
	if err == sql.ErrNoRows {
		return nil
	}
	if err != nil {
		return appErrors.NewInternalError("resolve assignment policy authority", err)
	}
	if strings.TrimSpace(claimID) == "" {
		return appErrors.NewInternalError("resolve assignment policy authority: claim is missing Id", nil)
	}
	message := fmt.Sprintf(
		"PermissionSetAssignment target %s is managed by an assignment policy",
		permissionSetID,
	)
	if suspended {
		message = fmt.Sprintf(
			"PermissionSetAssignment target %s is suspended and managed by an assignment policy",
			permissionSetID,
		)
	}
	return appErrors.NewValidationError(constants.FieldPermissionSetAssignment_PermissionSetID, message)
}

func (authority *SQLAssignmentPolicyDirectWriteAuthority) transaction(ctx context.Context) (*sql.Tx, error) {
	if authority == nil || authority.txManager == nil {
		return nil, appErrors.NewInternalError("assignment policy authority is unavailable", nil)
	}
	owner, ok := authority.txManager.(interface {
		OwnsPessimisticTx(context.Context) bool
	})
	if !ok || !owner.OwnsPessimisticTx(ctx) {
		return nil, appErrors.NewInternalError(
			"assignment policy authority requires an active pessimistic transaction owned by its transaction manager",
			nil,
		)
	}
	tx, ok := authority.txManager.ExtractTx(ctx).(*sql.Tx)
	if !ok || tx == nil {
		return nil, appErrors.NewInternalError(
			"assignment policy authority cannot extract the caller transaction", nil,
		)
	}
	return tx, nil
}
