// Package actions contains action implementations used by LocalAGI.
// This file implements the "webhook" action which can send an HTTP request
// to an external service with a configurable method, content type, and payload.
package actions

import (
	"bytes"
	"context"
	"fmt"
	"io"
	"net/http"
	"strings"

	"github.com/mudler/LocalAGI/core/types"
	"github.com/mudler/LocalAGI/pkg/config"
	"github.com/sashabaranov/go-openai/jsonschema"
)

// NewWebhook constructs a WebhookAction using provided configuration values:
//   - url: Destination endpoint for the HTTP request (required).
//   - method: HTTP method to use (GET, POST, PUT, DELETE, ...). Defaults to POST.
//   - contentType: Value for the Content-Type header (e.g., application/json).
//   - payloadTemplate: Optional template for the request body; the runtime parameter
//     "payload" (if provided) will replace the "{{payload}}" placeholder inside this template.
func NewWebhook(cfg map[string]string) *WebhookAction {
	wa := &WebhookAction{
		url:             strings.TrimSpace(cfg["url"]),
		method:          strings.ToUpper(strings.TrimSpace(cfg["method"])),
		contentType:     strings.TrimSpace(cfg["contentType"]),
		payloadTemplate: cfg["payloadTemplate"],
	}
	// Optional custom overrides
	if cfg != nil {
		wa.customName = cfg["custom_name"]
		wa.customDescription = cfg["custom_description"]
		wa.customPayloadDescription = cfg["custom_payload_description"]
	}
	if wa.method == "" {
		wa.method = http.MethodPost
	}
	return wa
}

// WebhookAction holds the static configuration for the webhook.
// These values come from the action configuration (UI/agent config),
// while the runtime parameter only carries the dynamic payload.
//   - url: Target endpoint for the request.
//   - method: HTTP method to use. Defaults to POST if not provided.
//   - contentType: Sets the Content-Type header when a body is sent.
//   - payloadTemplate: Optional template used to build the request body; occurrences
//     of "{{payload}}" get replaced with the runtime payload string.
//     If no placeholder is present, the template is used as-is.
//     For GET requests the body is omitted regardless of payload.
//
// Note: This action does not follow redirects!
type WebhookAction struct {
	url                      string
	method                   string
	contentType              string
	payloadTemplate          string
	customName               string
	customDescription        string
	customPayloadDescription string
}

// Run executes the webhook call.
// It reads the runtime parameter "payload" (optional), merges it into the
// configured payloadTemplate (if any), constructs an HTTP request using the
// configured URL, method and content type, and then returns a summary with the
// response status and body (truncated to 4KiB for safety).
func (a *WebhookAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
	// Runtime parameters: only payload
	type input struct {
		Payload string `json:"payload"`
	}
	var in input
	if err := params.Unmarshal(&in); err != nil {
		return types.ActionResult{}, err
	}

	// Validate essential configuration. The URL must be provided via the
	// action configuration (not via runtime parameters).
	if a.url == "" {
		return types.ActionResult{}, fmt.Errorf("configuration.url is required")
	}

	method := a.method

	// Build the request body based on template and payload:
	// - If a payloadTemplate is provided, replace occurrences of "{{payload}}"
	//   with the runtime payload value.
	// - If the template does not contain the placeholder but is provided, we use
	//   the template as-is (common for static JSON bodies prepared at config time).
	// - If no template is configured, we send the runtime payload as-is.
	// - For GET requests the body is omitted regardless of payload.
	var payload string
	if a.payloadTemplate != "" {
		payload = strings.ReplaceAll(a.payloadTemplate, "{{payload}}", in.Payload)
		if payload == a.payloadTemplate && in.Payload != "" {
			// If no placeholder found, fallback to template or payload alone
			payload = a.payloadTemplate
		}
	} else {
		payload = in.Payload
	}

	var body io.Reader
	if method != http.MethodGet && payload != "" {
		body = bytes.NewBufferString(payload)
	}

	// Create the HTTP request bound to the provided context so that cancellation
	// or timeouts from the caller propagate to the outbound call.
	req, err := http.NewRequestWithContext(ctx, method, a.url, body)
	if err != nil {
		return types.ActionResult{}, err
	}

	// Set Content-Type header if configured. For GET requests this header is
	// typically ignored by servers as there is no body.
	if a.contentType != "" {
		req.Header.Set("Content-Type", a.contentType)
	}

	// Use a new http.Client with default settings. Consider configuring timeouts
	// at the caller level via the context, or wiring a custom client if needed.
	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return types.ActionResult{}, err
	}
	defer resp.Body.Close()

	// Read and safely truncate the response body to avoid flooding the agent's
	// context with very large payloads. Errors on ReadAll are ignored here as
	// we already have the status code.
	respBytes, _ := io.ReadAll(resp.Body)
	respBody := string(respBytes)
	if len(respBody) > 4096 {
		respBody = respBody[:4096] + "... (truncated)"
	}

	return types.ActionResult{
		// Return the response body as the result.
		// If the response body is empty, use the status text as the result (e.g. "OK" for status code 200).
		Result: func() string {
			if respBody == "" {
				return http.StatusText(resp.StatusCode)
			}
			return respBody
		}(),
		// Include the response status code in the metadata.
		Metadata: map[string]interface{}{
			"statusCode": resp.StatusCode,
		},
	}, nil
}

// Definition returns the action schema exposed to the planner/runtime.
// Only the runtime parameter "payload" is accepted; all connection details
// are configured statically via the action configuration UI.
func (a *WebhookAction) Definition() types.ActionDefinition {
	name := "webhook"
	description := "Send an HTTP request to a configured URL/method/content-type. Accepts a runtime payload parameter optionally inserted into the configured payload template."
	if a.customName != "" {
		name = a.customName
	}
	if a.customDescription != "" {
		description = a.customDescription
	}
	payloadDesc := "Payload/body to send with the request at runtime. If a payloadTemplate is configured, '{{payload}}' will be replaced by this value."
	if a.customPayloadDescription != "" {
		payloadDesc = a.customPayloadDescription
	}
	return types.ActionDefinition{
		Name:        types.ActionDefinitionName(name),
		Description: description,
		Properties: map[string]jsonschema.Definition{
			"payload": {
				Type:        jsonschema.String,
				Description: payloadDesc,
			},
		},
	}
}

// Plannable indicates the action can be suggested/used by planners without
// requiring hidden context; inputs are straightforward and safe.
func (a *WebhookAction) Plannable() bool { return true }

// WebhookConfigMeta returns the metadata for Webhook action configuration fields:
//   - url: The endpoint to send requests to (required).
//   - method: One of GET/POST/PUT/DELETE. Defaults to POST.
//   - contentType: Common content types selectable from a dropdown.
//   - payloadTemplate: Optional body template. At runtime, "{{payload}}" is
//     replaced by the provided payload parameter. If missing, the template is used
//     as-is; for GET, no body is sent regardless.
func WebhookConfigMeta() []config.Field {
	return []config.Field{
		{
			Name:     "custom_name",
			Label:    "Custom Name",
			Type:     config.FieldTypeText,
			Required: false,
			HelpText: "Custom name for the action (optional, defaults to 'webhook')",
		},
		{
			Name:     "custom_description",
			Label:    "Custom Description",
			Type:     config.FieldTypeText,
			Required: false,
			HelpText: "Custom description for the action (optional)",
		},
		{
			Name:     "custom_payload_description",
			Label:    "Custom Payload Description",
			Type:     config.FieldTypeText,
			Required: false,
			HelpText: "Override the payload parameter description shown in the UI/schema (optional).",
		},
		{
			Name:     "url",
			Label:    "URL",
			Type:     config.FieldTypeText,
			Required: true,
			HelpText: "Destination URL for the webhook",
		},
		{
			Name:         "method",
			Label:        "HTTP Method",
			Type:         config.FieldTypeSelect,
			Options:      []config.FieldOption{{Value: http.MethodGet, Label: "GET"}, {Value: http.MethodPost, Label: "POST"}, {Value: http.MethodPut, Label: "PUT"}, {Value: http.MethodDelete, Label: "DELETE"}},
			DefaultValue: http.MethodPost,
			Required:     true,
			HelpText:     "HTTP method to use",
		},
		{
			Name:  "contentType",
			Label: "Content Type",
			Type:  config.FieldTypeSelect,
			Options: []config.FieldOption{
				{Value: "application/json", Label: "application/json"},
				{Value: "text/plain", Label: "text/plain"},
				{Value: "application/x-www-form-urlencoded", Label: "application/x-www-form-urlencoded"},
			},
			Required: true,
			HelpText: "Content-Type header to send",
		},
		{
			Name:     "payloadTemplate",
			Label:    "Payload Template",
			Type:     config.FieldTypeTextarea,
			HelpText: "Optional template used to craft the request body. Use '{{payload}}' as placeholder for the runtime payload.",
		},
	}
}
