package agent

import (
	"bytes"
	"crypto/sha256"
	"crypto/subtle"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"

	"github.com/gin-gonic/gin"

	"github.com/tidbcloud/AgentTeam/internal/api"
)

const (
	workflowCLIIdempotencyHeader       = "Idempotency-Key"
	workflowCLIIdempotencyKeyMaxLength = 128

	workflowCLIObjectPlan    = "plan"
	workflowCLIObjectTrigger = "trigger"
	workflowCLIObjectRun     = "run"
)

// workflowCLIContext is authored by the trusted daemon for every Workflow CLI
// object command. The authenticated request remains authoritative; these
// duplicated identifiers bind the forwarded command to that session.
type workflowCLIContext struct {
	AgentID     string `json:"agentId"`
	WorkspaceID string `json:"workspaceId"`
	CommandID   string `json:"commandId"`
}

// workflowCLIEnvelope is the strict transport shared by the Plan, Binding,
// Invocation, and Run object commands. Input is decoded a second time into the
// closed action union owned by each object.
type workflowCLIEnvelope struct {
	Context workflowCLIContext `json:"context"`
	Input   json.RawMessage    `json:"input"`
}

func decodeWorkflowCLIEnvelope(
	ctx *gin.Context,
	maxBytes int64,
	maxNesting int,
	mapError func(error) error,
) (workflowCLIEnvelope, error) {
	return decodeWorkflowCLIJSON[workflowCLIEnvelope](ctx, maxBytes, maxNesting, mapError)
}

func decodeWorkflowCLIJSON[T any](
	ctx *gin.Context,
	maxBytes int64,
	maxNesting int,
	mapError func(error) error,
) (T, error) {
	var empty T
	if ctx == nil || ctx.Request == nil || ctx.Request.Body == nil {
		return empty, mapWorkflowCLIJSONError(mapError, io.EOF)
	}
	ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, maxBytes)
	payload, err := io.ReadAll(ctx.Request.Body)
	if err != nil {
		return empty, mapWorkflowCLIJSONError(mapError, err)
	}
	return decodeWorkflowCLIJSONBytes[T](payload, maxBytes, maxNesting, mapError)
}

func decodeWorkflowCLIJSONBytes[T any](
	payload []byte,
	maxBytes int64,
	maxNesting int,
	mapError func(error) error,
) (T, error) {
	var result T
	trimmed := bytes.TrimSpace(payload)
	if len(trimmed) == 0 || trimmed[0] != '{' {
		return result, mapWorkflowCLIJSONError(mapError, errors.New("JSON request body must be an object"))
	}
	if int64(len(payload)) > maxBytes {
		return result, mapWorkflowCLIJSONError(mapError, &http.MaxBytesError{Limit: maxBytes})
	}
	if err := validateWorkflowCLIJSON(payload, maxNesting); err != nil {
		return result, mapWorkflowCLIJSONError(mapError, err)
	}
	decoder := json.NewDecoder(bytes.NewReader(payload))
	decoder.DisallowUnknownFields()
	if err := decoder.Decode(&result); err != nil {
		return result, mapWorkflowCLIJSONError(mapError, err)
	}
	var trailing any
	if err := decoder.Decode(&trailing); !errors.Is(err, io.EOF) {
		if err == nil {
			err = errors.New("multiple JSON values")
		}
		return result, mapWorkflowCLIJSONError(mapError, err)
	}
	return result, nil
}

func validateWorkflowCLIJSON(payload []byte, maxNesting int) error {
	decoder := json.NewDecoder(bytes.NewReader(payload))
	decoder.UseNumber()
	if err := consumeWorkflowCLIJSONValue(decoder, 0, maxNesting); err != nil {
		return err
	}
	var trailing any
	if err := decoder.Decode(&trailing); !errors.Is(err, io.EOF) {
		if err == nil {
			return errors.New("multiple JSON values")
		}
		return err
	}
	return nil
}

func consumeWorkflowCLIJSONValue(decoder *json.Decoder, depth int, maxNesting int) error {
	if depth > maxNesting {
		return errors.New("JSON nesting exceeds limit")
	}
	token, err := decoder.Token()
	if err != nil {
		return err
	}
	delimiter, ok := token.(json.Delim)
	if !ok {
		return nil
	}
	switch delimiter {
	case '{':
		seen := make(map[string]struct{})
		for decoder.More() {
			keyToken, err := decoder.Token()
			if err != nil {
				return err
			}
			key, ok := keyToken.(string)
			if !ok {
				return errors.New("JSON object key must be a string")
			}
			if _, duplicate := seen[key]; duplicate {
				return fmt.Errorf("duplicate JSON field %q", key)
			}
			seen[key] = struct{}{}
			if err := consumeWorkflowCLIJSONValue(decoder, depth+1, maxNesting); err != nil {
				return err
			}
		}
		_, err = decoder.Token()
	case '[':
		for decoder.More() {
			if err := consumeWorkflowCLIJSONValue(decoder, depth+1, maxNesting); err != nil {
				return err
			}
		}
		_, err = decoder.Token()
	default:
		return fmt.Errorf("unexpected JSON delimiter %q", delimiter)
	}
	return err
}

func mapWorkflowCLIJSONError(mapError func(error) error, err error) error {
	if mapError != nil {
		return mapError(err)
	}
	return err
}

func validateWorkflowCLIContext(
	request api.AgentScopedRequest,
	provided workflowCLIContext,
	objectLabel string,
	errorPrefix string,
) error {
	principalAgentID := request.Principal.AgentID
	principalWorkspaceID := request.Principal.WorkspaceID
	if principalAgentID == "" || principalWorkspaceID == "" ||
		request.Gin.Param("agentId") != principalAgentID ||
		request.Gin.Param("workspaceId") != principalWorkspaceID ||
		provided.AgentID != principalAgentID || provided.WorkspaceID != principalWorkspaceID {
		return publicError(
			http.StatusForbidden,
			errorPrefix+"_agent_context_mismatch",
			fmt.Sprintf("Workflow %s command context does not match the authenticated Agent session", objectLabel),
		)
	}
	if !validWorkflowCLICommandID(provided.CommandID) {
		return publicError(
			http.StatusBadRequest,
			errorPrefix+"_invalid_command_id",
			"commandId is missing or invalid",
		)
	}
	return nil
}

func validWorkflowCLICommandID(commandID string) bool {
	if len(commandID) != len("cli:")+32 || commandID[:len("cli:")] != "cli:" {
		return false
	}
	for index := len("cli:"); index < len(commandID); index++ {
		character := commandID[index]
		if (character < '0' || character > '9') && (character < 'a' || character > 'f') {
			return false
		}
	}
	return true
}

func verifyWorkflowCLIIdempotency(
	request api.AgentScopedRequest,
	providedContext workflowCLIContext,
	object string,
	action string,
	errorPrefix string,
) (string, error) {
	provided := request.Gin.GetHeader(workflowCLIIdempotencyHeader)
	if provided == "" {
		return "", publicError(
			http.StatusBadRequest,
			errorPrefix+"_idempotency_key_required",
			"Idempotency-Key is required",
		)
	}
	if len(provided) > workflowCLIIdempotencyKeyMaxLength {
		return "", publicError(
			http.StatusBadRequest,
			errorPrefix+"_invalid_idempotency_key",
			fmt.Sprintf("Idempotency-Key must not exceed %d bytes", workflowCLIIdempotencyKeyMaxLength),
		)
	}
	expected := workflowCLIIdempotencyKey(
		request.Principal.AgentID,
		request.Principal.WorkspaceID,
		object,
		action,
		providedContext.CommandID,
	)
	if subtle.ConstantTimeCompare([]byte(provided), []byte(expected)) != 1 {
		return "", publicError(
			http.StatusBadRequest,
			errorPrefix+"_invalid_idempotency_key",
			"Idempotency-Key does not match the authenticated Workflow CLI command",
		)
	}
	return expected, nil
}

func workflowCLIIdempotencyKey(agentID string, workspaceID string, object string, action string, commandID string) string {
	digest := sha256.Sum256([]byte(agentID + "\x00" + workspaceID + "\x00" + object + "\x00" + action + "\x00" + commandID))
	return "workflow-cli:" + hex.EncodeToString(digest[:])
}

// workflowCLIBusinessRequestDigest canonicalizes only the model-authored,
// action-specific business object. Mutable principal/owner/member resolution is
// deliberately excluded so a committed receipt remains recoverable after
// those relationships change. Object and action domain-separate the digest;
// receipt lookup independently scopes it by workspace and authenticated Actor.
func workflowCLIBusinessRequestDigest(object string, action string, input json.RawMessage) (string, error) {
	decoder := json.NewDecoder(bytes.NewReader(input))
	decoder.UseNumber()
	var value any
	if err := decoder.Decode(&value); err != nil {
		return "", err
	}
	objectValue, ok := value.(map[string]any)
	if !ok || objectValue == nil {
		return "", fmt.Errorf("Workflow command input must be a JSON object")
	}
	canonical, err := json.Marshal(objectValue)
	if err != nil {
		return "", err
	}
	digest := sha256.Sum256(bytes.Join([][]byte{
		[]byte("loop.workflow.cli-business-request/v1"),
		[]byte(object),
		[]byte(action),
		canonical,
	}, []byte{0}))
	return hex.EncodeToString(digest[:]), nil
}
