package exec

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net"
	"net/http"
	"net/url"
	"reflect"
	"strconv"
	"strings"
	"time"
)

// LocalClient calls one authenticated Exec action endpoint.
type LocalClient struct{ HTTPClient *http.Client }

// Call posts one action input and returns its JSON success envelope.
func (c LocalClient) Call(ctx context.Context, baseURL, token, domain, action string, input json.RawMessage) (json.RawMessage, error) {
	endpoint, err := ActionEndpoint(baseURL, domain, action)
	if err != nil {
		return nil, err
	}
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(input))
	if err != nil {
		return nil, err
	}
	req.Header.Set("Authorization", "Bearer "+strings.TrimSpace(token))
	req.Header.Set("Content-Type", "application/json")
	if key := requestIDFromPayload(input); key != "" {
		req.Header.Set("Idempotency-Key", key)
		req.Header.Set("X-Request-ID", key)
	}
	client := c.HTTPClient
	if client == nil {
		client = &http.Client{Timeout: 30 * time.Second}
	} else {
		// Do not mutate a caller-owned client while enforcing the local transport
		// boundary below.
		clone := *client
		client = &clone
	}
	// Exec credentials are scoped to the one canonical loopback endpoint. Never
	// replay Authorization through redirects, including to another local port.
	client.CheckRedirect = func(*http.Request, []*http.Request) error {
		return http.ErrUseLastResponse
	}
	resp, err := client.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	body, readErr := io.ReadAll(io.LimitReader(resp.Body, maxSerializedResultBytes+1))
	if readErr != nil {
		return nil, readErr
	}
	if len(body) > maxSerializedResultBytes {
		failure := resultBudgetExceededError(domain, action, len(body), nil)
		if builtinActionIsMutationForInput(domain, action, input) {
			failure.Hint = "Exec error response exceeded the size limit; inspect the authoritative resource before issuing another command."
			failure.Next = ""
			failure.Retryable = false
		}
		return nil, failure
	}
	if resp.StatusCode/100 != 2 {
		envelope, ok := decodeExecErrorEnvelope(body)
		if !ok {
			if len(bytes.TrimSpace(body)) == 0 || !json.Valid(body) {
				return nil, fmt.Errorf("Exec HTTP status %d", resp.StatusCode)
			}
			return nil, NewError(CodeInternal, "Exec error envelope is invalid")
		}
		mutationApplied, mutationReceipt := false, (*mutationSuccessReceipt)(nil)
		if envelope.Code == CodeResultBudgetExceeded && builtinActionIsMutationForInput(domain, action, input) {
			metadata, _ := json.Marshal(envelope.Metadata)
			mutationApplied, mutationReceipt = decodeMutationReceiptMetadata(metadata, domain+" "+action, input)
		}
		hint, next, retryable := envelope.Hint, envelope.Next, envelope.Retryable
		if builtinActionIsMutationForInput(domain, action, input) && envelope.Code == CodeResultBudgetExceeded && !mutationApplied {
			hint = "Mutation outcome is unknown; inspect the authoritative resource before issuing another command."
			next, retryable = "", false
		}
		return nil, &Error{
			Code: envelope.Code, Problem: envelope.Problem,
			Hint: hint, Next: next,
			Stage: envelope.Stage, Path: envelope.Path,
			Retryable: retryable, ServerCode: envelope.ServerCode,
			Diagnostics:     envelope.Diagnostics,
			Truncation:      envelope.Truncation,
			Truncated:       envelope.Truncated,
			mutationApplied: mutationApplied,
			mutationReceipt: mutationReceipt,
		}
	}
	if !json.Valid(body) {
		return nil, NewError(CodeInternal, "Exec action returned invalid JSON")
	}
	return json.RawMessage(bytes.TrimSpace(body)), nil
}

// decodeExecErrorEnvelope accepts only the exact JSON shape emitted by the
// daemon's ErrorEnvelope projector. The decoder rejects unknown/missing keys,
// duplicate object members, wrong nested field types, explicit zero-valued
// optional fields, and any non-canonical re-encoding. This keeps the
// daemon→LocalClient boundary fail-closed instead of silently accepting a
// larger future envelope.
func decodeExecErrorEnvelope(body []byte) (ErrorEnvelope, bool) {
	if !json.Valid(body) || !mutationReceiptJSONHasUniqueKeys(body) {
		return ErrorEnvelope{}, false
	}
	var top map[string]json.RawMessage
	if err := json.Unmarshal(body, &top); err != nil || len(top) != 1 {
		return ErrorEnvelope{}, false
	}
	errorRaw, ok := top["error"]
	if !ok {
		return ErrorEnvelope{}, false
	}
	var envelope ErrorEnvelope
	decoder := json.NewDecoder(bytes.NewReader(errorRaw))
	decoder.DisallowUnknownFields()
	if err := decoder.Decode(&envelope); err != nil || decoder.Decode(new(struct{})) != io.EOF || !envelope.Code.valid() {
		return ErrorEnvelope{}, false
	}
	var fields map[string]json.RawMessage
	if err := json.Unmarshal(errorRaw, &fields); err != nil || fields["code"] == nil || fields["problem"] == nil {
		return ErrorEnvelope{}, false
	}
	canonical, err := json.Marshal(envelope)
	if err != nil {
		return ErrorEnvelope{}, false
	}
	var gotValue, canonicalValue any
	if json.Unmarshal(errorRaw, &gotValue) != nil || json.Unmarshal(canonical, &canonicalValue) != nil || !reflect.DeepEqual(gotValue, canonicalValue) {
		return ErrorEnvelope{}, false
	}
	return envelope, true
}

// ActionEndpoint returns the canonical URL for one literal domain/action pair.
func ActionEndpoint(baseURL, domain, action string) (string, error) {
	baseURL = strings.TrimRight(strings.TrimSpace(baseURL), "/")
	if baseURL == "" {
		return "", NewError(CodePermissionDenied, "Exec base URL is required")
	}
	if err := validateName(domain); err != nil {
		return "", NewError(CodeValidation, "invalid domain")
	}
	if err := validateName(action); err != nil {
		return "", NewError(CodeValidation, "invalid action")
	}
	parsed, err := url.Parse(baseURL)
	if err != nil || parsed.Scheme != "http" || parsed.Host == "" || parsed.User != nil ||
		parsed.RawQuery != "" || parsed.Fragment != "" {
		return "", NewError(CodePermissionDenied, "invalid Exec base URL")
	}
	ip := net.ParseIP(parsed.Hostname())
	port, portErr := strconv.Atoi(parsed.Port())
	if ip == nil || !ip.IsLoopback() || portErr != nil || port < 1 || port > 65535 {
		return "", NewError(CodePermissionDenied, "Exec base URL must use a numeric loopback HTTP authority")
	}
	escapedBasePath := strings.TrimRight(parsed.EscapedPath(), "/")
	const execPathPrefix = "/api/internal/exec/servers/"
	rest := strings.TrimPrefix(escapedBasePath, execPathPrefix)
	serverID, agentID, found := strings.Cut(rest, "/agents/")
	if rest == escapedBasePath || !found || serverID == "" || agentID == "" ||
		strings.Contains(serverID, "/") || strings.Contains(agentID, "/") {
		return "", NewError(CodePermissionDenied, "invalid Exec base URL path")
	}
	decodedServerID, serverErr := url.PathUnescape(serverID)
	decodedAgentID, agentErr := url.PathUnescape(agentID)
	if serverErr != nil || agentErr != nil || strings.Contains(decodedServerID, "/") || strings.Contains(decodedAgentID, "/") {
		return "", NewError(CodePermissionDenied, "invalid Exec base URL path")
	}
	escapedPath := escapedBasePath + "/" + domain + "/" + action
	decodedPath, err := url.PathUnescape(escapedPath)
	if err != nil {
		return "", NewError(CodePermissionDenied, "invalid Exec base URL path")
	}
	parsed.Path = decodedPath
	parsed.RawPath = escapedPath
	return parsed.String(), nil
}
