package exec

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"io"
	"reflect"
	"strings"
	"sync"

	compiledjsonschema "github.com/santhosh-tekuri/jsonschema/v6"

	"github.com/tidbcloud/AgentTeam/desktop/internal/wire/apiclient"
)

type workflowHandler struct{ receipt workflowReceiptTransport }

func (handler workflowHandler) remote(authorized runtimeAuthorization) workflowRemote {
	return workflowRemote{client: authorized.Server, receipt: handler.receipt}
}

func (handler workflowHandler) plan(ctx context.Context, request actionScopedJSON[workflowPlanRequest]) (actionResult[workflowPlanResult], error) {
	action := request.Body.Action
	result, err := (handler.remote(request.Authorized)).plan(ctx, request.Body)
	if err != nil {
		return actionResult[workflowPlanResult]{}, classifyWorkflowOperationError(err, "workflow plan "+strings.ReplaceAll(action, "_", " ")+" failed")
	}
	return actionResult[workflowPlanResult]{Data: result}, nil
}

func (handler workflowHandler) trigger(ctx context.Context, request actionScopedJSON[workflowTriggerRequest]) (actionResult[workflowTriggerResult], error) {
	action := request.Body.Action
	result, err := (handler.remote(request.Authorized)).trigger(ctx, request.Body)
	if err != nil {
		return actionResult[workflowTriggerResult]{}, classifyWorkflowOperationError(err, "workflow trigger "+strings.ReplaceAll(action, "_", " ")+" failed")
	}
	return actionResult[workflowTriggerResult]{Data: result}, nil
}

func (handler workflowHandler) run(ctx context.Context, request actionScopedJSON[workflowRunRequest]) (actionResult[workflowRunResult], error) {
	action := request.Body.Action
	result, err := (handler.remote(request.Authorized)).run(ctx, request.Body)
	if err != nil {
		return actionResult[workflowRunResult]{}, classifyWorkflowOperationError(err, "workflow run "+strings.ReplaceAll(action, "_", " ")+" failed")
	}
	if action == "cancel" && result.cancel != nil && result.cancel.Run.Status == "canceled" &&
		request.Authorized.WorkflowCompaction != nil {
		request.Authorized.WorkflowCompaction.WorkflowRunCanceled(request.Body.RunID)
	}
	return actionResult[workflowRunResult]{Data: result}, nil
}

func (handler workflowHandler) step(ctx context.Context, request actionScopedJSON[workflowStepCommand]) (actionResult[workflowStepOutcome], error) {
	action, payload, err := typedWorkflowAction(request.Body)
	if err != nil {
		return actionResult[workflowStepOutcome]{}, NewError(CodeValidation, "invalid workflow step arguments")
	}
	callback, err := validateWorkflowStep(action, payload)
	if err != nil {
		return actionResult[workflowStepOutcome]{}, err
	}
	if request.Authorized.Server == nil {
		return actionResult[workflowStepOutcome]{}, NewError(CodePermissionDenied, "workflow step requires an authenticated Agent session")
	}
	runContext := workflowRunContext{
		WorkspaceID: request.Authorized.Server.RouteWorkspaceID,
		AgentID:     request.Authorized.Server.AgentID,
		RunID:       callback.RunID,
		EffectID:    callback.StepID,
	}
	value, list, err := (handler.remote(request.Authorized)).step(ctx, callback, runContext)
	observeWorkflowStepCompactionOutcome(request.Authorized.WorkflowCompaction, action, callback.RunID, callback.StepID, err)
	if err != nil {
		return actionResult[workflowStepOutcome]{}, classifyWorkflowOperationError(err, "workflow step "+action+" failed")
	}
	return actionResult[workflowStepOutcome]{Data: workflowStepOutcome{step: value, list: list}}, nil
}

func observeWorkflowStepCompactionOutcome(
	observer workflowCompactionObserver,
	action,
	runID,
	stepID string,
	err error,
) {
	if observer == nil {
		return
	}
	if err != nil {
		if isAuthoritativeWorkflowRunTerminalError(err) {
			observer.WorkflowStepUnsuccessful(runID, stepID)
		}
		return
	}
	switch action {
	case "complete":
		observer.WorkflowStepCompleted(runID, stepID)
	case "fail":
		observer.WorkflowStepUnsuccessful(runID, stepID)
	}
}

func typedWorkflowAction[I any](input I) (string, json.RawMessage, error) {
	raw, err := json.Marshal(input)
	if err != nil {
		return "", nil, err
	}
	return decodeWorkflowAction(raw)
}

type workflowOperationSchemaCache struct {
	mu       sync.Mutex
	compiled *compiledjsonschema.Schema
}

func (cache *workflowOperationSchemaCache) schema(
	t reflect.Type,
	build func(reflect.Type) (*compiledjsonschema.Schema, error),
) (*compiledjsonschema.Schema, error) {
	cache.mu.Lock()
	defer cache.mu.Unlock()
	if cache.compiled != nil {
		return cache.compiled, nil
	}
	compiled, err := build(t)
	if err != nil {
		return nil, err
	}
	cache.compiled = compiled
	return cache.compiled, nil
}

var workflowOperationSchemaCaches sync.Map

func buildWorkflowOperationSchema(t reflect.Type) (*compiledjsonschema.Schema, error) {
	schema, err := reflectContractSchema(t)
	if err != nil {
		return nil, err
	}
	return compileSchema(schema)
}

func decodeWorkflowOperationResult[T any](raw json.RawMessage) (*T, error) {
	value := new(T)
	decoder := json.NewDecoder(bytes.NewReader(raw))
	decoder.DisallowUnknownFields()
	if err := decoder.Decode(value); err != nil {
		return nil, invalidWorkflowOperationResult(err)
	}
	if err := decoder.Decode(new(struct{})); err != io.EOF {
		return nil, invalidWorkflowOperationResult(errors.New("unexpected trailing JSON"))
	}
	compiled, schemaErr := workflowSchemaFor(contractType[T]())
	if schemaErr != nil {
		return nil, invalidWorkflowOperationResult(schemaErr)
	}
	if schemaErr := validateInstance(compiled, raw); schemaErr != nil {
		return nil, invalidWorkflowOperationResult(schemaErr)
	}
	return value, nil
}

func workflowSchemaFor(t reflect.Type) (*compiledjsonschema.Schema, error) {
	raw, _ := workflowOperationSchemaCaches.LoadOrStore(t, new(workflowOperationSchemaCache))
	return raw.(*workflowOperationSchemaCache).schema(t, buildWorkflowOperationSchema)
}

func classifyWorkflowOperationError(err error, problem string) error {
	if typed, ok := err.(*Error); ok {
		return typed
	}
	classified := classifyAPIError(err, problem)
	// The ServerCode is an allowlisted byte-exact server code; compare it
	// exactly so a malformed/spaced variant can never unlock the invocation
	// input diagnostic.
	if classified.ServerCode == "invocation_input_invalid" {
		if diagnostic, ok := workflowInvocationInputDiagnostic(err); ok {
			classified.Problem = "workflow Run input invalid: " + diagnostic.Path + ": " + diagnostic.Message
			if classified.Hint == "" {
				classified.Hint = "Run `workflow plan show` and rebuild input from the top-level YAML input contract for the exact Plan version."
			}
		}
	}
	return classified
}

type workflowInvocationInputDiagnosticValue struct {
	Code    string
	Path    string
	Message string
}

// isASCIIFieldSegment reports whether value is a non-empty ASCII field-name
// segment for a diagnostic path: letters, digits, underscore, or hyphen, with no
// dots, brackets, spaces, or non-ASCII (invalid UTF-8) bytes. It rejects empty,
// dot-bearing, bracket-bearing, or malformed-UTF-8 segments.
func isASCIIFieldSegment(value string) bool {
	if value == "" {
		return false
	}
	for index := 0; index < len(value); index++ {
		character := value[index]
		if (character >= 'a' && character <= 'z') ||
			(character >= 'A' && character <= 'Z') ||
			(character >= '0' && character <= '9') ||
			character == '_' || character == '-' {
			continue
		}
		return false
	}
	return true
}

// inputDiagnosticPathValid reports whether a diagnostic path is `$.input`
// exactly or a segment-structural descendant of it. The grammar is rooted at the
// canonical `$.input` and walks only well-formed `.field` and `[index]` tokens:
//
//   - every field segment is a non-empty ASCII field name (letters/digits/
//     underscore/hyphen);
//   - every bracket token is `[` + one-or-more ASCII digits + `]` with no
//     leading zeros/plus/minus/spaces/nesting;
//   - empty and double-dot segments, malformed or unterminated brackets,
//     prefix lookalikes (for example `$.inputSecret`, which lacks the dot), and
//     invalid-UTF-8 / non-ASCII bytes all fail closed.
//
// A path that merely shares the `$.input` text or smuggles punctuation can
// never unlock the invocation input diagnostic.
func inputDiagnosticPathValid(path string) bool {
	// Root: exactly `$.input`.
	if path == "$.input" {
		return true
	}
	// Any descendant must begin with `$.input.`.
	if !strings.HasPrefix(path, "$.input.") {
		return false
	}
	rest := path[len("$.input."):]
	if rest == "" {
		// A trailing dot (`$.input.`) is an empty final segment.
		return false
	}
	// Walk the canonical `.field` / `[index]` grammar. The path is a sequence of
	// field and index tokens; a `.` is a separator that MUST be followed by a
	// field (never another `.`, never a `[`, never the end), and an index is
	// `[` + a canonical non-negative integer without leading zeros + `]`. A dot
	// after a field is consumed as the separator, and after an index the next
	// token must be `.field` or the end.
	for len(rest) > 0 {
		if rest[0] == '[' {
			closeBracket := strings.IndexByte(rest, ']')
			if closeBracket < 0 {
				// Unterminated index.
				return false
			}
			indexText := rest[1:closeBracket]
			if !canonicalIndexText(indexText) {
				return false
			}
			rest = rest[closeBracket+1:]
			// After an index the next token must be `.field` or the end. A `[`
			// (bracket chain) or any other character fails closed.
			if rest == "" {
				return true
			}
			if rest[0] != '.' {
				return false
			}
			// Consume the separator dot; the following field is parsed next and
			// must exist.
			rest = rest[1:]
			if rest == "" {
				// A dot after an index with no following field (`[0].`).
				return false
			}
			if rest[0] == '[' || rest[0] == '.' {
				// `.field` is required after the dot: `[0].[1]` and `[0]..` fail.
				return false
			}
			continue
		}
		if rest[0] == '.' {
			// A leading dot here is a double-dot / empty segment: the previous
			// token consumed its separator already.
			return false
		}
		// A field segment runs until the next dot or bracket.
		dot := strings.IndexByte(rest, '.')
		openBracket := strings.IndexByte(rest, '[')
		next := len(rest)
		if dot >= 0 && dot < next {
			next = dot
		}
		if openBracket >= 0 && openBracket < next {
			next = openBracket
		}
		segment := rest[:next]
		if !isASCIIFieldSegment(segment) {
			return false
		}
		rest = rest[next:]
		// After a field, if more remains it must be a separator dot followed by
		// a field (never a bracket directly, never the end on a dot).
		if rest == "" {
			return true
		}
		if rest[0] != '.' {
			// A bracket directly after a field with no separator (`a[0]` inside a
			// path) is invalid; the field+index forms are `a[0]` only when the
			// field is followed by `[`, which IS allowed — handle below.
			if rest[0] == '[' {
				// `field[index]` is the canonical form: the index token follows the
				// field without a dot. The loop continues and parses `[index]`.
				continue
			}
			return false
		}
		rest = rest[1:]
		if rest == "" {
			// A trailing separator dot (`field.`) with no following field.
			return false
		}
		if rest[0] == '.' || rest[0] == '[' {
			// `.field` is required after the separator: `..` and `.[` fail.
			return false
		}
	}
	return true
}

// canonicalIndexText reports whether value is a canonical non-negative integer
// index without leading zeros (the single digit `0` is allowed; `00`, `01`,
// `+1`, `-1`, and spaces fail closed).
func canonicalIndexText(value string) bool {
	if value == "" {
		return false
	}
	if value[0] < '0' || value[0] > '9' {
		return false
	}
	if value[0] == '0' && len(value) > 1 {
		// Leading zero (e.g. `00`, `01`).
		return false
	}
	for index := 0; index < len(value); index++ {
		character := value[index]
		if character < '0' || character > '9' {
			return false
		}
	}
	return true
}

// workflowInvocationInputDiagnostic exposes only the server's closed,
// path-aware Workflow Invocation validation shape. It consumes ONLY
// projectAPIErrorDiagnostics output (which gates both the structured
// apiclient.Diagnostics and the legacy details["diagnostics"] through the same
// byte-exact validator), and requires an `$.input` exactly or segment-correct
// descendant path with a canonical code and a bounded message. All other API
// details stay in Cause and remain unavailable to the model.
func workflowInvocationInputDiagnostic(err error) (workflowInvocationInputDiagnosticValue, bool) {
	var apiErr *apiclient.APIError
	if !errors.As(err, &apiErr) ||
		apiErr.Code != "invocation_input_invalid" {
		return workflowInvocationInputDiagnosticValue{}, false
	}
	// Normalize the legacy details["diagnostics"] shape into the same typed
	// apiclient.Diagnostic list so BOTH the structured and the legacy diagnostics
	// pass through the single byte-exact projection gate. The gate drops a
	// diagnostic whose code/path/field identity is malformed, spaced, or
	// secret-bearing.
	projected := projectAPIErrorDiagnostics(apiErr)
	if len(projected) == 0 {
		details, ok := apiErr.Details.(map[string]any)
		if !ok || len(details) != 1 {
			return workflowInvocationInputDiagnosticValue{}, false
		}
		rawDiagnostics, ok := details["diagnostics"].([]any)
		if !ok || len(rawDiagnostics) == 0 {
			return workflowInvocationInputDiagnosticValue{}, false
		}
		legacy := make([]apiclient.Diagnostic, 0, len(rawDiagnostics))
		for _, raw := range rawDiagnostics {
			diagnostic, ok := raw.(map[string]any)
			if !ok {
				continue
			}
			legacy = append(legacy, apiclient.Diagnostic{
				Severity: stringOf(diagnostic["severity"]),
				Code:     stringOf(diagnostic["code"]),
				Path:     stringOf(diagnostic["path"]),
				Field:    stringOf(diagnostic["field"]),
				Message:  stringOf(diagnostic["message"]),
			})
		}
		legacyErr := *apiErr
		legacyErr.Diagnostics = legacy
		projected = projectAPIErrorDiagnostics(&legacyErr)
	}
	if len(projected) == 0 {
		return workflowInvocationInputDiagnosticValue{}, false
	}
	diagnostic := projected[0]
	if diagnostic.Severity != "error" ||
		diagnostic.Code == "" ||
		!inputDiagnosticPathValid(diagnostic.Path) {
		return workflowInvocationInputDiagnosticValue{}, false
	}
	message := strings.TrimSpace(diagnostic.Message)
	if message == "" {
		return workflowInvocationInputDiagnosticValue{}, false
	}
	return workflowInvocationInputDiagnosticValue{
		Code: diagnostic.Code, Path: diagnostic.Path, Message: message,
	}, true
}

func stringOf(value any) string {
	text, ok := value.(string)
	if !ok {
		return ""
	}
	return text
}

func invalidWorkflowOperationResult(cause error) *Error {
	return &Error{Code: CodeInternal, Problem: "invalid workflow response", Cause: cause}
}
