package agentops

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

	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/store/model"
)

// Operation is one approval-driven structural Agent operation.
type Operation struct {
	ID               int64
	WorkspaceID      string
	ApplicantAgentID string
	OwnerUserID      string
	Action           string
	Payload          model.JSONDocument
	ApprovalID       string
	RequestID        string
	Status           string
	CreatedAt        time.Time
	UpdatedAt        time.Time
}

// OperationItem is one ordered item within an operation.
type OperationItem struct {
	ID          int64
	OperationID int64
	ItemIndex   int
	Input       model.JSONDocument
	Status      string
	Result      model.JSONDocument
	Error       string
}

// EnqueueInput is the data for creating an operation + items in a single tx.
type EnqueueInput struct {
	WorkspaceID      string
	ApplicantAgentID string
	OwnerUserID      string
	Action           string
	Payload          model.JSONDocument
	ApprovalID       string
	RequestID        string
	Items            []model.JSONDocument // per-item input JSON
}

// EnqueueOperationInTx creates the operation and its items inside an existing
// transaction (the approval DecideWithEffect tx on Approve). Returns the
// operation ID.
func (txs TxStore) EnqueueOperationInTx(ctx context.Context, input EnqueueInput) (int64, error) {
	if err := validateEnqueueInput(input); err != nil {
		return 0, err
	}
	_, err := txs.tx.Exec(ctx, `
INSERT INTO agent_operations (workspace_id, applicant_agent_id, owner_user_id, action, payload, approval_id, request_id, status)
VALUES ($1, $2, $3, $4, $5, $6, $7, 'queued')`,
		input.WorkspaceID, input.ApplicantAgentID, input.OwnerUserID, input.Action,
		[]byte(input.Payload), input.ApprovalID, input.RequestID)
	if err != nil {
		return 0, fmt.Errorf("enqueue agent operation: %w", err)
	}
	var id int64
	if err := txs.tx.QueryRow(ctx, `SELECT LAST_INSERT_ID()`).Scan(&id); err != nil {
		return 0, fmt.Errorf("enqueue agent operation: get id: %w", err)
	}
	var values strings.Builder
	args := make([]any, 0, len(input.Items)*3)
	for index, itemInput := range input.Items {
		if index > 0 {
			values.WriteString(", ")
		}
		arg := index*3 + 1
		fmt.Fprintf(&values, "($%d, $%d, $%d, 'pending')", arg, arg+1, arg+2)
		args = append(args, id, index, []byte(itemInput))
	}
	if _, err := txs.tx.Exec(ctx, `
INSERT INTO agent_operation_items (operation_id, item_index, input, status)
VALUES `+values.String(), args...); err != nil {
		return 0, fmt.Errorf(
			"enqueue agent operation items 0-%d: %w", len(input.Items)-1, err,
		)
	}
	return id, nil
}

// LookupOperationByApproval returns the unique operation created for one
// approved agent-management request.
func (store *Store) LookupOperationByApproval(
	ctx context.Context,
	workspaceID, approvalID string,
) (Operation, error) {
	workspaceID = strings.TrimSpace(workspaceID)
	approvalID = strings.TrimSpace(approvalID)
	if workspaceID == "" || approvalID == "" {
		return Operation{}, fmt.Errorf(
			"lookup agent operation: workspace and approval are required",
		)
	}
	var operation Operation
	var payload []byte
	err := store.db.QueryRow(ctx, `
SELECT id, workspace_id, applicant_agent_id, owner_user_id, action, payload,
       approval_id, request_id, status, created_at, updated_at
FROM agent_operations
WHERE workspace_id = $1 AND approval_id = $2
LIMIT 1`, workspaceID, approvalID).Scan(
		&operation.ID, &operation.WorkspaceID, &operation.ApplicantAgentID,
		&operation.OwnerUserID, &operation.Action, &payload,
		&operation.ApprovalID, &operation.RequestID, &operation.Status,
		&operation.CreatedAt, &operation.UpdatedAt,
	)
	if err != nil {
		return Operation{}, fmt.Errorf(
			"lookup agent operation by approval: %w", err,
		)
	}
	operation.Payload = model.JSONDocument(payload)
	return operation, nil
}

// ClaimNextOperation atomically selects the oldest operation eligible for work.
// Queued operations are claimed queued→running. A running operation is eligible
// only when it has items and none remain pending, allowing terminal projection
// retry without re-executing items. workspaceGroupID limits claims to routable
// placements; empty preserves global all-profile behavior.
func (store *Store) ClaimNextOperation(ctx context.Context, workspaceGroupID string) (*Operation, error) {
	workspaceGroupID = strings.TrimSpace(workspaceGroupID)
	var claimed *Operation
	err := coredb.WithTx(ctx, store.db, func(tx coredb.Tx) error {
		var row coredb.Row
		if workspaceGroupID == "" {
			row = tx.QueryRow(ctx, `
SELECT id, workspace_id, applicant_agent_id, owner_user_id, action, payload,
       approval_id, request_id, status, created_at, updated_at
FROM agent_operations operation
WHERE operation.status = 'queued'
   OR (
     operation.status = 'running'
     AND EXISTS (
       SELECT 1 FROM agent_operation_items item
       WHERE item.operation_id = operation.id
     )
     AND NOT EXISTS (
       SELECT 1 FROM agent_operation_items item
       WHERE item.operation_id = operation.id AND item.status = 'pending'
     )
   )
ORDER BY
  CASE WHEN operation.status = 'queued' THEN 0 ELSE 1 END,
  operation.id
LIMIT 1
FOR UPDATE`)
		} else {
			row = tx.QueryRow(ctx, `
SELECT operation.id, operation.workspace_id, operation.applicant_agent_id,
       operation.owner_user_id, operation.action, operation.payload,
       operation.approval_id, operation.request_id, operation.status,
       operation.created_at, operation.updated_at
FROM agent_operations operation
JOIN workspace_placements placement
  ON placement.workspace_id = operation.workspace_id
 AND placement.workspace_group_id = $1
 AND placement.state IN ('active', 'draining', 'moving')
WHERE (
       operation.status = 'queued'
       OR (
         operation.status = 'running'
         AND EXISTS (
           SELECT 1 FROM agent_operation_items item
           WHERE item.operation_id = operation.id
         )
         AND NOT EXISTS (
           SELECT 1 FROM agent_operation_items item
           WHERE item.operation_id = operation.id AND item.status = 'pending'
         )
       )
      )
ORDER BY
  CASE WHEN operation.status = 'queued' THEN 0 ELSE 1 END,
  operation.id
LIMIT 1
FOR UPDATE`, workspaceGroupID)
		}
		var op Operation
		var payload []byte
		err := row.Scan(&op.ID, &op.WorkspaceID, &op.ApplicantAgentID, &op.OwnerUserID,
			&op.Action, &payload, &op.ApprovalID, &op.RequestID, &op.Status, &op.CreatedAt, &op.UpdatedAt)
		if errors.Is(err, sql.ErrNoRows) {
			return nil
		}
		if err != nil {
			return err
		}
		op.Payload = model.JSONDocument(payload)
		if op.Status == "queued" {
			res, err := tx.Exec(ctx, `UPDATE agent_operations SET status = 'running' WHERE id = $1 AND status = 'queued'`, op.ID)
			if err != nil {
				return err
			}
			if res.RowsAffected() != 1 {
				return nil // someone else claimed it
			}
			op.Status = "running"
		}
		claimed = &op
		return nil
	})
	return claimed, err
}

// LoadItems returns operation items in strict index order. pendingOnly limits
// the result to work that has not yet been applied.
func (store *Store) LoadItems(ctx context.Context, operationID int64, pendingOnly bool) ([]OperationItem, error) {
	statusClause := ""
	if pendingOnly {
		statusClause = " AND status = 'pending'"
	}
	rows, err := store.db.Query(ctx, `
SELECT id, operation_id, item_index, input, status, result, error
FROM agent_operation_items
WHERE operation_id = $1`+statusClause+`
ORDER BY item_index`, operationID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []OperationItem
	for rows.Next() {
		var item OperationItem
		var input, result, errorBytes []byte
		if err := rows.Scan(&item.ID, &item.OperationID, &item.ItemIndex, &input, &item.Status, &result, &errorBytes); err != nil {
			return nil, err
		}
		item.Input = model.JSONDocument(input)
		if result != nil {
			item.Result = model.JSONDocument(result)
		}
		item.Error = string(errorBytes)
		items = append(items, item)
	}
	return items, rows.Err()
}

// CompleteItem CAS transitions an item pending→applied|failed and records the
// result/error. Returns an error on invalid status or CAS miss (item not pending).
func (store *Store) CompleteItem(ctx context.Context, operationID int64, itemIndex int, status string, result model.JSONDocument, itemError string) error {
	switch status {
	case "applied", "failed":
	default:
		return fmt.Errorf("agent operation item: invalid status %q", status)
	}
	res, err := store.db.Exec(ctx, `
UPDATE agent_operation_items SET status = $3, result = $4, error = $5
WHERE operation_id = $1 AND item_index = $2 AND status = 'pending'`,
		operationID, itemIndex, status, []byte(result), itemError)
	if err != nil {
		return err
	}
	if res.RowsAffected() != 1 {
		return fmt.Errorf("agent operation item %d: CAS miss (not pending)", itemIndex)
	}
	return nil
}

// CompleteOperation CAS transitions running→applied|partial|failed.
func (store *Store) CompleteOperation(ctx context.Context, operationID int64, status string) error {
	switch status {
	case "applied", "partial", "failed":
	default:
		return fmt.Errorf("agent operation: invalid terminal status %q", status)
	}
	res, err := store.db.Exec(ctx, `
UPDATE agent_operations SET status = $2 WHERE id = $1 AND status = 'running'`,
		operationID, status)
	if err != nil {
		return err
	}
	if res.RowsAffected() != 1 {
		return fmt.Errorf("agent operation %d: CAS miss (not running)", operationID)
	}
	return nil
}

func validateEnqueueInput(input EnqueueInput) error {
	if input.WorkspaceID == "" || input.ApplicantAgentID == "" || input.OwnerUserID == "" ||
		input.Action == "" || input.ApprovalID == "" || input.RequestID == "" {
		return errors.New("agent operation enqueue: required fields missing")
	}
	switch input.Action {
	case "create", "update", "delete", "duplicate":
	default:
		return fmt.Errorf("agent operation enqueue: invalid action %q", input.Action)
	}
	if len(input.Items) == 0 || len(input.Items) > 10 {
		return errors.New("agent operation enqueue: 1-10 items required")
	}
	if !model.IsValidJSON(input.Payload) {
		return errors.New("agent operation enqueue: invalid payload JSON")
	}
	for i, item := range input.Items {
		if !model.IsValidJSON(item) {
			return fmt.Errorf("agent operation enqueue: invalid item %d JSON", i)
		}
	}
	return nil
}
