// Package agentapproval implements the agent_management Approval subject:
// an Agent proposes a structural change (create/update/delete/duplicate) to
// other Agents owned by the same Human; the owner Human approves; on approve a
// queued operation (+ ordered items) is enqueued for the worker. Payloads are
// credential-free (the worker re-resolves LLM/profile at apply time).
package agentapproval

import (
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"sort"
	"strings"
)

const (
	// SubjectType is the agent_management approval subject registry key.
	SubjectType = "agent_management"
	// RequestType is the approval submit request type used by Agent Runtime routes.
	RequestType = "agent_management"
	// PayloadSchemaVersion is the frozen payload schema version.
	PayloadSchemaVersion = 1

	ActionCreate    = "create"
	ActionUpdate    = "update"
	ActionDelete    = "delete"
	ActionDuplicate = "duplicate"
)

// Payload is the immutable, credential-free proposal frozen into one approval.
type Payload struct {
	SchemaVersion   int                  `json:"schemaVersion"`
	ClientRequestID string               `json:"clientRequestId"`
	Applicant       Applicant            `json:"applicant"`
	Owner           OwnerRef             `json:"owner"`
	Action          string               `json:"action"`
	Create          *CreateProposal      `json:"create,omitempty"`
	Update          *UpdateProposal      `json:"update,omitempty"`
	Delete          *DeleteProposal      `json:"delete,omitempty"`
	Duplicate       *DuplicateProposal   `json:"duplicate,omitempty"`
	Presentation    PresentationSnapshot `json:"presentation,omitempty"`
}

// PresentationSnapshot freezes safe display identity for approval surfaces.
// Execution continues to use the typed proposals above.
type PresentationSnapshot struct {
	Agents []AgentIdentitySnapshot `json:"agents,omitempty"`
}

type AgentIdentitySnapshot struct {
	ID          string `json:"id"`
	DisplayName string `json:"displayName"`
}

// Applicant is the authenticated Agent submitting the request.
type Applicant struct {
	AgentID  string `json:"agentId"`
	MemberID string `json:"memberId"`
}

// OwnerRef is the trusted Human owner (machine creator) resolved at Submit and
// frozen into the payload so Decide can build the operation without re-resolving
// identity. It is set by the service from the authenticated principal, never
// supplied by the request payload.
type OwnerRef struct {
	UserID   string `json:"userId"`
	MemberID string `json:"memberId"`
}

// CreateProposal describes one new Agent. Raw LLM credentials are intentionally
// absent; the worker re-resolves model/profile at apply time. Per the frozen
// Exec contract the create surface carries only name (used as display).
type CreateProposal struct {
	Name            string   `json:"name"`
	Description     string   `json:"description,omitempty"`
	MachineID       string   `json:"machineId"`
	Runtime         string   `json:"runtime"`
	Model           string   `json:"model,omitempty"`
	ReasoningEffort string   `json:"reasoningEffort,omitempty"`
	ModelProfileID  string   `json:"modelProfileId,omitempty"`
	ChannelIDs      []string `json:"channelIds,omitempty"`
}

// UpdateProposal describes the frozen Exec contract update surface for one
// Agent (description / model / reasoningEffort / modelProfileId). No raw
// credentials; the worker re-resolves runtime/LLM/profile at apply time.
type UpdateProposal struct {
	AgentID         string  `json:"agentId"`
	Description     *string `json:"description,omitempty"`
	Model           *string `json:"model,omitempty"`
	ReasoningEffort *string `json:"reasoningEffort,omitempty"`
	ModelProfileID  *string `json:"modelProfileId,omitempty"`
}

// DeleteProposal lists 1-10 same-owner Agent IDs to delete.
type DeleteProposal struct {
	AgentIDs []string `json:"agentIds"`
}

// DuplicateProposal describes 1-10 copies of one same-owner source Agent.
// MachineID/Runtime are optional overrides; empty inherits the source Agent.
type DuplicateProposal struct {
	SourceAgentID string   `json:"sourceAgentId"`
	Count         int      `json:"count"`
	Mode          string   `json:"mode,omitempty"`
	MachineID     string   `json:"machineId,omitempty"`
	Runtime       string   `json:"runtime,omitempty"`
	ChannelIDs    []string `json:"channelIds,omitempty"`
}

// canonicalPayload normalizes the payload (sorted slices, trimmed identity) and
// returns the deterministic JSON bytes + sha256 digest used for idempotency replay.
func canonicalPayload(payload Payload) (Payload, []byte, string, error) {
	payload.SchemaVersion = PayloadSchemaVersion
	payload.ClientRequestID = strings.TrimSpace(payload.ClientRequestID)
	payload.Applicant.AgentID = strings.TrimSpace(payload.Applicant.AgentID)
	payload.Applicant.MemberID = strings.TrimSpace(payload.Applicant.MemberID)
	payload.Owner.UserID = strings.TrimSpace(payload.Owner.UserID)
	payload.Owner.MemberID = strings.TrimSpace(payload.Owner.MemberID)
	payload.Action = strings.TrimSpace(payload.Action)
	for index := range payload.Presentation.Agents {
		payload.Presentation.Agents[index].ID = strings.TrimSpace(
			payload.Presentation.Agents[index].ID,
		)
		payload.Presentation.Agents[index].DisplayName = strings.TrimSpace(
			payload.Presentation.Agents[index].DisplayName,
		)
	}
	if payload.Create != nil {
		payload.Create.Name = strings.TrimSpace(payload.Create.Name)
		payload.Create.Description = strings.TrimSpace(payload.Create.Description)
		payload.Create.MachineID = strings.TrimSpace(payload.Create.MachineID)
		payload.Create.Runtime = strings.TrimSpace(payload.Create.Runtime)
		payload.Create.Model = strings.TrimSpace(payload.Create.Model)
		payload.Create.ReasoningEffort = strings.TrimSpace(payload.Create.ReasoningEffort)
		payload.Create.ModelProfileID = strings.TrimSpace(payload.Create.ModelProfileID)
		payload.Create.ChannelIDs = sortedNonEmpty(payload.Create.ChannelIDs)
	}
	if payload.Duplicate != nil {
		payload.Duplicate.SourceAgentID = strings.TrimSpace(payload.Duplicate.SourceAgentID)
		payload.Duplicate.Mode = strings.TrimSpace(payload.Duplicate.Mode)
		payload.Duplicate.MachineID = strings.TrimSpace(payload.Duplicate.MachineID)
		payload.Duplicate.Runtime = strings.TrimSpace(payload.Duplicate.Runtime)
		payload.Duplicate.ChannelIDs = sortedNonEmpty(payload.Duplicate.ChannelIDs)
	}
	if payload.Update != nil {
		payload.Update.AgentID = strings.TrimSpace(payload.Update.AgentID)
		payload.Update.Description = trimStringPtr(payload.Update.Description)
		payload.Update.Model = trimStringPtr(payload.Update.Model)
		payload.Update.ReasoningEffort = trimStringPtr(payload.Update.ReasoningEffort)
		payload.Update.ModelProfileID = trimStringPtr(payload.Update.ModelProfileID)
	}
	// Delete AgentIDs are an ordered, caller-validated list: trim each but never
	// sort or de-duplicate (order is preserved into operation items + replay).
	if payload.Delete != nil {
		for index := range payload.Delete.AgentIDs {
			payload.Delete.AgentIDs[index] = strings.TrimSpace(payload.Delete.AgentIDs[index])
		}
	}
	snapshot, err := json.Marshal(payload)
	if err != nil {
		return Payload{}, nil, "", fmt.Errorf("marshal agent management payload: %w", err)
	}
	var canonical any
	decoder := json.NewDecoder(strings.NewReader(string(snapshot)))
	decoder.UseNumber()
	if err := decoder.Decode(&canonical); err != nil {
		return Payload{}, nil, "", fmt.Errorf("canonicalize agent management payload: %w", err)
	}
	snapshot, err = json.Marshal(canonical)
	if err != nil {
		return Payload{}, nil, "", fmt.Errorf("marshal canonical agent management payload: %w", err)
	}
	return payload, snapshot, digestBytes(snapshot), nil
}

// decodePayload verifies the stored payload against its digest and returns the
// normalized payload. A digest mismatch means the payload was tampered with or
// produced by an incompatible version.
func decodePayload(raw []byte, wantDigest string) (Payload, error) {
	if len(raw) == 0 {
		return Payload{}, fmt.Errorf("agent management payload is empty")
	}
	var payload Payload
	if err := json.Unmarshal(raw, &payload); err != nil {
		return Payload{}, fmt.Errorf("decode agent management payload: %w", err)
	}
	normalized, _, digest, err := canonicalPayload(payload)
	if err != nil {
		return Payload{}, err
	}
	if digest != wantDigest {
		return Payload{}, fmt.Errorf("agent management payload digest mismatch")
	}
	return normalized, nil
}

// itemInputs builds the per-item operation inputs in strict input order.
func (p Payload) itemInputs() ([]json.RawMessage, error) {
	switch p.Action {
	case ActionCreate:
		raw, err := json.Marshal(p.Create)
		if err != nil {
			return nil, fmt.Errorf("marshal create item: %w", err)
		}
		return []json.RawMessage{raw}, nil
	case ActionUpdate:
		raw, err := json.Marshal(p.Update)
		if err != nil {
			return nil, fmt.Errorf("marshal update item: %w", err)
		}
		return []json.RawMessage{raw}, nil
	case ActionDelete:
		items := make([]json.RawMessage, 0, len(p.Delete.AgentIDs))
		for _, id := range p.Delete.AgentIDs {
			raw, err := json.Marshal(map[string]string{"agentId": id})
			if err != nil {
				return nil, fmt.Errorf("marshal delete item: %w", err)
			}
			items = append(items, raw)
		}
		return items, nil
	case ActionDuplicate:
		items := make([]json.RawMessage, 0, p.Duplicate.Count)
		for i := 0; i < p.Duplicate.Count; i++ {
			raw, err := json.Marshal(p.Duplicate)
			if err != nil {
				return nil, fmt.Errorf("marshal duplicate item: %w", err)
			}
			items = append(items, raw)
		}
		return items, nil
	}
	return nil, fmt.Errorf("agent management action %q has no proposal", p.Action)
}

func digestBytes(value []byte) string      { sum := sha256.Sum256(value); return hex.EncodeToString(sum[:]) }
func digestStrings(parts ...string) string { return digestBytes([]byte(strings.Join(parts, "\x00"))) }

func sortedNonEmpty(values []string) []string {
	seen := make(map[string]struct{}, len(values))
	out := make([]string, 0, len(values))
	for _, value := range values {
		value = strings.TrimSpace(value)
		if value == "" {
			continue
		}
		if _, ok := seen[value]; ok {
			continue
		}
		seen[value] = struct{}{}
		out = append(out, value)
	}
	sort.Strings(out)
	return out
}

func trimStringPtr(value *string) *string {
	if value == nil {
		return nil
	}
	trimmed := strings.TrimSpace(*value)
	return &trimmed
}
