// Package execution owns the minimal durable execution projection for
// approved Role management requests. Generic Approval remains the sole request,
// payload, digest, idempotency, applicant, and decision ledger.
package execution

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

	"github.com/tidbcloud/AgentTeam/internal/core/identifiers"
	"github.com/tidbcloud/AgentTeam/internal/coredb"
)

var (
	// ErrNotImplemented remains available to later service/transport scaffolds.
	ErrNotImplemented = errors.New("role management execution store is not implemented")
	// ErrNotFound reports an execution outside the workspace/Approval fence.
	ErrNotFound = errors.New("role management execution not found")
	// ErrConflict reports an expired or replaced lease fence.
	ErrConflict = errors.New("role management execution conflict")
)

// ExecutionStatus is the private worker state. Public request-get projection
// maps queued/running to processing.
type ExecutionStatus string

const (
	ExecutionQueued    ExecutionStatus = "queued"
	ExecutionRunning   ExecutionStatus = "running"
	ExecutionSucceeded ExecutionStatus = "succeeded"
	ExecutionFailed    ExecutionStatus = "failed"
)

// Execution deliberately stores only execution facts. ApprovalID is both the
// identity and the one-to-one link to the authoritative Approval ledger.
type Execution struct {
	ApprovalID          string
	WorkspaceID         string
	Status              ExecutionStatus
	LeaseToken          string
	LeaseExpiresAt      *time.Time
	ResultRoleID        string
	ResultRoleVersionID string
	FailureCode         string
	FailureMessage      string
	CreatedAt           time.Time
	UpdatedAt           time.Time
}

// ExecutionFence authorizes a single live worker to settle or release work.
type ExecutionFence struct {
	WorkspaceID string
	ApprovalID  string
	LeaseToken  string
}

// ClaimedExecution pairs a running execution with its live fence.
type ClaimedExecution struct {
	Execution Execution
	Fence     ExecutionFence
}

// ClaimParams bounds one worker scan. Claims may select queued rows or reclaim
// running rows whose lease expired at or before Now.
type ClaimParams struct {
	WorkspaceID string
	Now         time.Time
	LeaseTTL    time.Duration
	Limit       int
}

// Success is the minimal stable public result projection.
type Success struct {
	RoleID        string
	RoleVersionID string
}

// Failure is a secret-free deterministic failure projection.
type Failure struct {
	Code    string
	Message string
}

// Store is the durable execution store.
type Store struct {
	db coredb.DB
}

// TxStore binds execution writes to a caller-owned Core transaction.
type TxStore struct {
	tx coredb.Tx
}

func New(db coredb.DB) *Store { return &Store{db: db} }

func (store *Store) InTx(tx coredb.Tx) *TxStore { return &TxStore{tx: tx} }

func (store *Store) validate() error {
	if store == nil || store.db == nil {
		return fmt.Errorf("role management execution store is not configured")
	}
	return nil
}

// Enqueue creates exactly one queued execution for a pending Role management
// Approval. It is intentionally called only from that Approval's authorized
// approve effect, before generic Approval writes its terminal approved facts.
// A replay inside the same pending transaction returns the existing row.
func (store *TxStore) Enqueue(ctx context.Context, workspaceID, approvalID string, now time.Time) (Execution, bool, error) {
	if store == nil || store.tx == nil {
		return Execution{}, false, fmt.Errorf("role management execution tx store is not configured")
	}
	workspaceID, approvalID = strings.TrimSpace(workspaceID), strings.TrimSpace(approvalID)
	if workspaceID == "" || approvalID == "" {
		return Execution{}, false, fmt.Errorf("workspaceId and approvalId are required")
	}
	now = normalizedTime(now)
	result, err := store.tx.Exec(ctx, `
INSERT IGNORE INTO role_management_executions (
  approval_id,workspace_id,status,lease_token,lease_expires_at,
  result_role_id,result_role_version_id,failure_code,failure_message,
  created_at,updated_at
)
SELECT approval.id,approval.workspace_id,'queued',NULL,NULL,'','','','',$3,$3
FROM approval_instances approval
WHERE approval.id=$1 AND approval.workspace_id=$2
  AND approval.subject_type='role_management' AND approval.state='pending'`,
		approvalID, workspaceID, now)
	if err != nil {
		return Execution{}, false, fmt.Errorf("enqueue Role management execution: %w", err)
	}
	value, err := loadExecution(ctx, store.tx, workspaceID, approvalID)
	if err != nil {
		return Execution{}, false, err
	}
	return value, result.RowsAffected() == 1, nil
}

// LoadByApprovalID is tenant-fenced by both workspace and Approval identity.
func (store *Store) LoadByApprovalID(ctx context.Context, workspaceID, approvalID string) (Execution, error) {
	if err := store.validate(); err != nil {
		return Execution{}, err
	}
	return loadExecution(ctx, store.db, strings.TrimSpace(workspaceID), strings.TrimSpace(approvalID))
}

// ClaimRunnable claims queued work or reclaims an expired running lease,
// replacing the token on every successful claim.
func (store *Store) ClaimRunnable(ctx context.Context, params ClaimParams) ([]ClaimedExecution, error) {
	if err := store.validate(); err != nil {
		return nil, err
	}
	params.WorkspaceID = strings.TrimSpace(params.WorkspaceID)
	if params.WorkspaceID == "" || params.LeaseTTL <= 0 || params.Limit <= 0 || params.Limit > 100 {
		return nil, fmt.Errorf("workspaceId, positive leaseTTL, and limit 1-100 are required")
	}
	params.Now = normalizedTime(params.Now)
	rows, err := store.db.Query(ctx, `
SELECT approval_id
FROM role_management_executions
WHERE workspace_id=$1
  AND (
    (status='queued' AND lease_token IS NULL AND lease_expires_at IS NULL)
    OR
    (status='running' AND lease_token IS NOT NULL AND lease_expires_at <= $2)
  )
ORDER BY created_at ASC,approval_id ASC
LIMIT $3`, params.WorkspaceID, params.Now, params.Limit)
	if err != nil {
		return nil, fmt.Errorf("list runnable Role management executions: %w", err)
	}
	candidates := make([]string, 0, params.Limit)
	for rows.Next() {
		var approvalID string
		if err := rows.Scan(&approvalID); err != nil {
			rows.Close()
			return nil, fmt.Errorf("scan runnable Role management execution: %w", err)
		}
		candidates = append(candidates, approvalID)
	}
	if err := rows.Err(); err != nil {
		rows.Close()
		return nil, fmt.Errorf("iterate runnable Role management executions: %w", err)
	}
	rows.Close()

	claimed := make([]ClaimedExecution, 0, len(candidates))
	for _, approvalID := range candidates {
		value, ok, err := store.claimCandidate(ctx, params.WorkspaceID, approvalID, params.Now, params.LeaseTTL)
		if err != nil {
			return nil, err
		}
		if ok {
			claimed = append(claimed, value)
		}
	}
	return claimed, nil
}

// ReleaseUncertain returns an uncertain outcome to queued without recording a
// terminal failure. Only the current live token may release it.
func (store *Store) ReleaseUncertain(ctx context.Context, fence ExecutionFence, now time.Time) (Execution, error) {
	if err := store.validate(); err != nil {
		return Execution{}, err
	}
	fence, err := normalizedFence(fence)
	if err != nil {
		return Execution{}, err
	}
	now = normalizedTime(now)
	return store.mutateFenced(ctx, fence, func(tx coredb.Tx) error {
		result, err := tx.Exec(ctx, `
UPDATE role_management_executions
SET status='queued',lease_token=NULL,lease_expires_at=NULL,
    failure_code='',failure_message='',updated_at=$4
WHERE workspace_id=$1 AND approval_id=$2
  AND status='running' AND lease_token=$3 AND lease_expires_at>$4`,
			fence.WorkspaceID, fence.ApprovalID, fence.LeaseToken, now)
		if err != nil {
			return fmt.Errorf("release uncertain Role management execution: %w", err)
		}
		if result.RowsAffected() != 1 {
			return ErrConflict
		}
		return nil
	})
}

// FailDeterministic records a terminal, secret-free business failure.
func (store *Store) FailDeterministic(ctx context.Context, fence ExecutionFence, failure Failure, now time.Time) (Execution, error) {
	if err := store.validate(); err != nil {
		return Execution{}, err
	}
	fence, err := normalizedFence(fence)
	if err != nil {
		return Execution{}, err
	}
	failure.Code = strings.TrimSpace(failure.Code)
	failure.Message = strings.TrimSpace(failure.Message)
	if failure.Code == "" || len(failure.Code) > 64 {
		return Execution{}, fmt.Errorf("bounded failure code is required")
	}
	now = normalizedTime(now)
	return store.mutateFenced(ctx, fence, func(tx coredb.Tx) error {
		result, err := tx.Exec(ctx, `
UPDATE role_management_executions
SET status='failed',lease_token=NULL,lease_expires_at=NULL,
    failure_code=$4,failure_message=$5,updated_at=$6
WHERE workspace_id=$1 AND approval_id=$2
  AND status='running' AND lease_token=$3 AND lease_expires_at>$6`,
			fence.WorkspaceID, fence.ApprovalID, fence.LeaseToken,
			failure.Code, failure.Message, now)
		if err != nil {
			return fmt.Errorf("fail Role management execution: %w", err)
		}
		if result.RowsAffected() != 1 {
			return ErrConflict
		}
		return nil
	})
}

// Succeed records the minimal stable result.
func (store *Store) Succeed(ctx context.Context, fence ExecutionFence, success Success, now time.Time) (Execution, error) {
	if err := store.validate(); err != nil {
		return Execution{}, err
	}
	fence, err := normalizedFence(fence)
	if err != nil {
		return Execution{}, err
	}
	success.RoleID = strings.TrimSpace(success.RoleID)
	success.RoleVersionID = strings.TrimSpace(success.RoleVersionID)
	if success.RoleID == "" {
		return Execution{}, fmt.Errorf("result roleId is required")
	}
	now = normalizedTime(now)
	return store.mutateFenced(ctx, fence, func(tx coredb.Tx) error {
		result, err := tx.Exec(ctx, `
UPDATE role_management_executions
SET status='succeeded',lease_token=NULL,lease_expires_at=NULL,
    result_role_id=$4,result_role_version_id=$5,
    failure_code='',failure_message='',updated_at=$6
WHERE workspace_id=$1 AND approval_id=$2
  AND status='running' AND lease_token=$3 AND lease_expires_at>$6`,
			fence.WorkspaceID, fence.ApprovalID, fence.LeaseToken,
			success.RoleID, success.RoleVersionID, now)
		if err != nil {
			return fmt.Errorf("succeed Role management execution: %w", err)
		}
		if result.RowsAffected() != 1 {
			return ErrConflict
		}
		return nil
	})
}

func (store *Store) claimCandidate(ctx context.Context, workspaceID, approvalID string, now time.Time, leaseTTL time.Duration) (ClaimedExecution, bool, error) {
	token, err := identifiers.NewEntityID("rml")
	if err != nil {
		return ClaimedExecution{}, false, fmt.Errorf("generate Role management execution lease: %w", err)
	}
	expiresAt := normalizedTime(now.Add(leaseTTL))
	var claimed ClaimedExecution
	var won bool
	err = coredb.WithTx(ctx, store.db, func(tx coredb.Tx) error {
		result, err := tx.Exec(ctx, `
UPDATE role_management_executions
SET status='running',lease_token=$3,lease_expires_at=$4,updated_at=$2
WHERE workspace_id=$1 AND approval_id=$5
  AND (
    (status='queued' AND lease_token IS NULL AND lease_expires_at IS NULL)
    OR
    (status='running' AND lease_token IS NOT NULL AND lease_expires_at <= $2)
  )`, workspaceID, now, token, expiresAt, approvalID)
		if err != nil {
			return fmt.Errorf("claim Role management execution: %w", err)
		}
		if result.RowsAffected() == 0 {
			return nil
		}
		value, err := loadExecution(ctx, tx, workspaceID, approvalID)
		if err != nil {
			return err
		}
		claimed = ClaimedExecution{
			Execution: value,
			Fence: ExecutionFence{
				WorkspaceID: workspaceID, ApprovalID: approvalID, LeaseToken: token,
			},
		}
		won = true
		return nil
	})
	return claimed, won, err
}

func (store *Store) mutateFenced(
	ctx context.Context,
	fence ExecutionFence,
	mutate func(coredb.Tx) error,
) (Execution, error) {
	var value Execution
	err := coredb.WithTx(ctx, store.db, func(tx coredb.Tx) error {
		if err := mutate(tx); err != nil {
			return err
		}
		var err error
		value, err = loadExecution(ctx, tx, fence.WorkspaceID, fence.ApprovalID)
		return err
	})
	return value, err
}

const executionSelectSQL = `
SELECT approval_id,workspace_id,status,
       lease_token,lease_expires_at,
       result_role_id,result_role_version_id,
       failure_code,failure_message,created_at,updated_at
FROM role_management_executions`

func loadExecution(ctx context.Context, queryer coredb.QueryRower, workspaceID, approvalID string) (Execution, error) {
	value, err := scanExecution(queryer.QueryRow(ctx, executionSelectSQL+`
WHERE workspace_id=$1 AND approval_id=$2`, workspaceID, approvalID))
	if err != nil {
		if coredb.IsNotFound(err) {
			return Execution{}, ErrNotFound
		}
		return Execution{}, fmt.Errorf("load Role management execution: %w", err)
	}
	return value, nil
}

func scanExecution(scanner interface{ Scan(...any) error }) (Execution, error) {
	var value Execution
	var leaseToken sql.NullString
	var leaseExpires sql.NullTime
	if err := scanner.Scan(
		&value.ApprovalID, &value.WorkspaceID, &value.Status,
		&leaseToken, &leaseExpires,
		&value.ResultRoleID, &value.ResultRoleVersionID,
		&value.FailureCode, &value.FailureMessage,
		&value.CreatedAt, &value.UpdatedAt,
	); err != nil {
		return Execution{}, err
	}
	value.LeaseToken = leaseToken.String
	if leaseExpires.Valid {
		expires := normalizedTime(leaseExpires.Time)
		value.LeaseExpiresAt = &expires
	}
	value.CreatedAt = normalizedTime(value.CreatedAt)
	value.UpdatedAt = normalizedTime(value.UpdatedAt)
	return value, nil
}

func normalizedFence(fence ExecutionFence) (ExecutionFence, error) {
	fence.WorkspaceID = strings.TrimSpace(fence.WorkspaceID)
	fence.ApprovalID = strings.TrimSpace(fence.ApprovalID)
	fence.LeaseToken = strings.TrimSpace(fence.LeaseToken)
	if fence.WorkspaceID == "" || fence.ApprovalID == "" || fence.LeaseToken == "" {
		return ExecutionFence{}, fmt.Errorf("complete Role management execution fence is required")
	}
	return fence, nil
}

func normalizedTime(value time.Time) time.Time {
	if value.IsZero() {
		value = time.Now()
	}
	return value.UTC().Truncate(time.Microsecond)
}
