package agent

import (
	"context"
	"fmt"
	"slices"
	"strings"
	"sync"

	"github.com/sipeed/picoclaw/pkg/logger"
	"github.com/sipeed/picoclaw/pkg/providers"
)

type PromptLayer string

const (
	PromptLayerKernel      PromptLayer = "kernel"
	PromptLayerInstruction PromptLayer = "instruction"
	PromptLayerCapability  PromptLayer = "capability"
	PromptLayerContext     PromptLayer = "context"
	PromptLayerTurn        PromptLayer = "turn"
)

type PromptSlot string

const (
	PromptSlotIdentity     PromptSlot = "identity"
	PromptSlotHierarchy    PromptSlot = "hierarchy"
	PromptSlotWorkspace    PromptSlot = "workspace"
	PromptSlotTooling      PromptSlot = "tooling"
	PromptSlotMCP          PromptSlot = "mcp"
	PromptSlotSkillCatalog PromptSlot = "skill_catalog"
	PromptSlotActiveSkill  PromptSlot = "active_skill"
	PromptSlotMemory       PromptSlot = "memory"
	PromptSlotRuntime      PromptSlot = "runtime"
	PromptSlotSummary      PromptSlot = "summary"
	PromptSlotMessage      PromptSlot = "message"
	PromptSlotSteering     PromptSlot = "steering"
	PromptSlotSubTurn      PromptSlot = "subturn"
	PromptSlotInterrupt    PromptSlot = "interrupt"
	PromptSlotOutput       PromptSlot = "output"
)

type PromptSourceID string

const (
	PromptSourceKernel         PromptSourceID = "runtime.kernel"
	PromptSourceHierarchy      PromptSourceID = "runtime.hierarchy"
	PromptSourceWorkspace      PromptSourceID = "workspace.definition"
	PromptSourceRuntime        PromptSourceID = "runtime.context"
	PromptSourceSummary        PromptSourceID = "context.summary"
	PromptSourceMemory         PromptSourceID = "memory:workspace"
	PromptSourceSkillCatalog   PromptSourceID = "skill:index"
	PromptSourceActiveSkills   PromptSourceID = "skill:active"
	PromptSourceAgentDiscovery PromptSourceID = "agent:discovery"
	PromptSourceToolRegistry   PromptSourceID = "tool_registry:native"
	PromptSourceToolDiscovery  PromptSourceID = "tool_registry:discovery"
	PromptSourceOutputPolicy   PromptSourceID = "runtime.output"
	PromptSourceSubTurnProfile PromptSourceID = "subturn.profile"
	PromptSourceUserMessage    PromptSourceID = "turn:user_message"
	PromptSourceSteering       PromptSourceID = "turn:steering"
	PromptSourceSubTurnResult  PromptSourceID = "turn:subturn_result"
	PromptSourceInterrupt      PromptSourceID = "turn:interrupt"
)

type PromptCachePolicy string

const (
	PromptCacheDefault   PromptCachePolicy = ""
	PromptCacheEphemeral PromptCachePolicy = "ephemeral"
	PromptCacheNone      PromptCachePolicy = "none"
)

type PromptPlacement struct {
	Layer PromptLayer
	Slot  PromptSlot
}

type PromptSourceDescriptor struct {
	ID              PromptSourceID
	Owner           string
	Description     string
	Allowed         []PromptPlacement
	StableByDefault bool
}

type PromptSource struct {
	ID   PromptSourceID
	Name string
	Path string
}

type PromptPart struct {
	ID      string
	Layer   PromptLayer
	Slot    PromptSlot
	Source  PromptSource
	Title   string
	Content string
	Stable  bool
	Cache   PromptCachePolicy
}

type PromptBuildRequest struct {
	History []providers.Message
	Summary string

	CurrentMessage string
	Media          []string

	Channel           string
	ChatID            string
	SenderID          string
	SenderDisplayName string

	ActiveSkills []string
	Overlays     []PromptPart
}

type PromptContributor interface {
	PromptSource() PromptSourceDescriptor
	ContributePrompt(ctx context.Context, req PromptBuildRequest) ([]PromptPart, error)
}

type PromptRegistry struct {
	mu           sync.RWMutex
	sources      map[PromptSourceID]PromptSourceDescriptor
	contributors []PromptContributor
	warned       map[PromptSourceID]struct{}
}

func NewPromptRegistry() *PromptRegistry {
	r := &PromptRegistry{
		sources: make(map[PromptSourceID]PromptSourceDescriptor),
		warned:  make(map[PromptSourceID]struct{}),
	}
	for _, desc := range builtinPromptSources() {
		if err := r.RegisterSource(desc); err != nil {
			logger.WarnCF("agent", "Failed to register builtin prompt source", map[string]any{
				"source": desc.ID,
				"error":  err.Error(),
			})
		}
	}
	return r
}

func builtinPromptSources() []PromptSourceDescriptor {
	return []PromptSourceDescriptor{
		{
			ID:              PromptSourceKernel,
			Owner:           "agent",
			Description:     "Core picoclaw identity and hard rules",
			Allowed:         []PromptPlacement{{Layer: PromptLayerKernel, Slot: PromptSlotIdentity}},
			StableByDefault: true,
		},
		{
			ID:              PromptSourceHierarchy,
			Owner:           "agent",
			Description:     "Prompt hierarchy rules",
			Allowed:         []PromptPlacement{{Layer: PromptLayerKernel, Slot: PromptSlotHierarchy}},
			StableByDefault: true,
		},
		{
			ID:              PromptSourceWorkspace,
			Owner:           "workspace",
			Description:     "Workspace and agent definition files",
			Allowed:         []PromptPlacement{{Layer: PromptLayerInstruction, Slot: PromptSlotWorkspace}},
			StableByDefault: true,
		},
		{
			ID:              PromptSourceToolDiscovery,
			Owner:           "tools",
			Description:     "Tool discovery instructions",
			Allowed:         []PromptPlacement{{Layer: PromptLayerCapability, Slot: PromptSlotTooling}},
			StableByDefault: true,
		},
		{
			ID:              PromptSourceToolRegistry,
			Owner:           "tools",
			Description:     "Native provider tool definitions",
			Allowed:         []PromptPlacement{{Layer: PromptLayerCapability, Slot: PromptSlotTooling}},
			StableByDefault: true,
		},
		{
			ID:              PromptSourceSkillCatalog,
			Owner:           "skills",
			Description:     "Installed skill catalog",
			Allowed:         []PromptPlacement{{Layer: PromptLayerCapability, Slot: PromptSlotSkillCatalog}},
			StableByDefault: true,
		},
		{
			ID:              PromptSourceActiveSkills,
			Owner:           "skills",
			Description:     "Active skill instructions for the current request",
			Allowed:         []PromptPlacement{{Layer: PromptLayerCapability, Slot: PromptSlotActiveSkill}},
			StableByDefault: false,
		},
		{
			ID:              PromptSourceAgentDiscovery,
			Owner:           "agent",
			Description:     "Structured multi-agent discovery registry",
			Allowed:         []PromptPlacement{{Layer: PromptLayerCapability, Slot: PromptSlotTooling}},
			StableByDefault: false,
		},
		{
			ID:              PromptSourceMemory,
			Owner:           "memory",
			Description:     "Workspace memory context",
			Allowed:         []PromptPlacement{{Layer: PromptLayerContext, Slot: PromptSlotMemory}},
			StableByDefault: true,
		},
		{
			ID:              PromptSourceRuntime,
			Owner:           "agent",
			Description:     "Per-request runtime context",
			Allowed:         []PromptPlacement{{Layer: PromptLayerContext, Slot: PromptSlotRuntime}},
			StableByDefault: false,
		},
		{
			ID:              PromptSourceSummary,
			Owner:           "context_manager",
			Description:     "Conversation summary context",
			Allowed:         []PromptPlacement{{Layer: PromptLayerContext, Slot: PromptSlotSummary}},
			StableByDefault: false,
		},
		{
			ID:              PromptSourceOutputPolicy,
			Owner:           "agent",
			Description:     "Output formatting policy",
			Allowed:         []PromptPlacement{{Layer: PromptLayerContext, Slot: PromptSlotOutput}},
			StableByDefault: true,
		},
		{
			ID:              PromptSourceSubTurnProfile,
			Owner:           "subturn",
			Description:     "Child agent profile instructions",
			Allowed:         []PromptPlacement{{Layer: PromptLayerInstruction, Slot: PromptSlotWorkspace}},
			StableByDefault: false,
		},
		{
			ID:              PromptSourceUserMessage,
			Owner:           "turn",
			Description:     "Current user message for this turn",
			Allowed:         []PromptPlacement{{Layer: PromptLayerTurn, Slot: PromptSlotMessage}},
			StableByDefault: false,
		},
		{
			ID:              PromptSourceSteering,
			Owner:           "turn",
			Description:     "Steering message injected into a running turn",
			Allowed:         []PromptPlacement{{Layer: PromptLayerTurn, Slot: PromptSlotSteering}},
			StableByDefault: false,
		},
		{
			ID:              PromptSourceSubTurnResult,
			Owner:           "turn",
			Description:     "SubTurn result injected into a parent turn",
			Allowed:         []PromptPlacement{{Layer: PromptLayerTurn, Slot: PromptSlotSubTurn}},
			StableByDefault: false,
		},
		{
			ID:              PromptSourceInterrupt,
			Owner:           "turn",
			Description:     "Graceful interrupt hint injected into the terminal LLM call",
			Allowed:         []PromptPlacement{{Layer: PromptLayerTurn, Slot: PromptSlotInterrupt}},
			StableByDefault: false,
		},
	}
}

func (r *PromptRegistry) RegisterSource(desc PromptSourceDescriptor) error {
	if r == nil {
		return fmt.Errorf("prompt registry is nil")
	}
	desc.ID = PromptSourceID(strings.TrimSpace(string(desc.ID)))
	if desc.ID == "" {
		return fmt.Errorf("prompt source id is required")
	}
	if len(desc.Allowed) == 0 {
		return fmt.Errorf("prompt source %q must declare at least one placement", desc.ID)
	}

	r.mu.Lock()
	defer r.mu.Unlock()
	r.sources[desc.ID] = clonePromptSourceDescriptor(desc)
	return nil
}

func (r *PromptRegistry) RegisterContributor(contributor PromptContributor) error {
	if r == nil {
		return fmt.Errorf("prompt registry is nil")
	}
	if contributor == nil {
		return fmt.Errorf("prompt contributor is nil")
	}
	desc := contributor.PromptSource()
	desc.ID = PromptSourceID(strings.TrimSpace(string(desc.ID)))
	if err := r.RegisterSource(desc); err != nil {
		return err
	}

	r.mu.Lock()
	defer r.mu.Unlock()
	r.contributors = slices.DeleteFunc(r.contributors, func(existing PromptContributor) bool {
		return PromptSourceID(strings.TrimSpace(string(existing.PromptSource().ID))) == desc.ID
	})
	r.contributors = append(r.contributors, contributor)
	return nil
}

func (r *PromptRegistry) Collect(ctx context.Context, req PromptBuildRequest) ([]PromptPart, error) {
	if r == nil {
		return nil, nil
	}

	r.mu.RLock()
	contributors := append([]PromptContributor(nil), r.contributors...)
	r.mu.RUnlock()

	var parts []PromptPart
	for _, contributor := range contributors {
		contributed, err := contributor.ContributePrompt(ctx, req)
		if err != nil {
			return nil, err
		}
		for _, part := range contributed {
			if err := r.ValidatePart(part); err != nil {
				return nil, err
			}
			parts = append(parts, part)
		}
	}
	return parts, nil
}

func (r *PromptRegistry) ValidatePart(part PromptPart) error {
	if r == nil {
		return nil
	}
	sourceID := PromptSourceID(strings.TrimSpace(string(part.Source.ID)))
	if sourceID == "" {
		return fmt.Errorf("prompt part %q has empty source id", part.ID)
	}

	r.mu.Lock()
	defer r.mu.Unlock()

	desc, ok := r.sources[sourceID]
	if !ok {
		if _, warned := r.warned[sourceID]; !warned {
			r.warned[sourceID] = struct{}{}
			logger.WarnCF("agent", "Unregistered prompt source allowed in compatibility mode", map[string]any{
				"source": sourceID,
				"layer":  part.Layer,
				"slot":   part.Slot,
				"part":   part.ID,
			})
		}
		return nil
	}
	if promptPlacementAllowed(desc.Allowed, PromptPlacement{Layer: part.Layer, Slot: part.Slot}) {
		return nil
	}
	return fmt.Errorf("prompt source %q cannot write to %s/%s", sourceID, part.Layer, part.Slot)
}

func promptPlacementAllowed(allowed []PromptPlacement, placement PromptPlacement) bool {
	return slices.ContainsFunc(allowed, func(candidate PromptPlacement) bool {
		return candidate.Layer == placement.Layer && candidate.Slot == placement.Slot
	})
}

func clonePromptSourceDescriptor(desc PromptSourceDescriptor) PromptSourceDescriptor {
	desc.Allowed = append([]PromptPlacement(nil), desc.Allowed...)
	return desc
}

type PromptStack struct {
	registry *PromptRegistry
	parts    []PromptPart
	sealed   bool
}

func NewPromptStack(registry *PromptRegistry) *PromptStack {
	return &PromptStack{registry: registry}
}

func (s *PromptStack) Add(part PromptPart) error {
	if s == nil {
		return fmt.Errorf("prompt stack is nil")
	}
	if s.sealed {
		return fmt.Errorf("prompt stack is sealed")
	}
	if strings.TrimSpace(part.Content) == "" {
		return nil
	}
	if strings.TrimSpace(part.ID) == "" {
		return fmt.Errorf("prompt part id is required")
	}
	if s.registry != nil {
		if err := s.registry.ValidatePart(part); err != nil {
			return err
		}
	}
	s.parts = append(s.parts, part)
	return nil
}

func (s *PromptStack) Seal() {
	if s != nil {
		s.sealed = true
	}
}

func (s *PromptStack) Parts() []PromptPart {
	if s == nil || len(s.parts) == 0 {
		return nil
	}
	return append([]PromptPart(nil), s.parts...)
}

func renderPromptPartsLegacy(parts []PromptPart) string {
	textParts := make([]string, 0, len(parts))
	for _, part := range sortPromptParts(parts) {
		if strings.TrimSpace(part.Content) == "" {
			continue
		}
		textParts = append(textParts, part.Content)
	}
	return strings.Join(textParts, "\n\n---\n\n")
}

func sortPromptParts(parts []PromptPart) []PromptPart {
	sorted := append([]PromptPart(nil), parts...)
	slices.SortStableFunc(sorted, func(a, b PromptPart) int {
		if d := layerPriority(b.Layer) - layerPriority(a.Layer); d != 0 {
			return d
		}
		if d := slotPriority(b.Slot) - slotPriority(a.Slot); d != 0 {
			return d
		}
		if a.Source.ID != b.Source.ID {
			return strings.Compare(string(a.Source.ID), string(b.Source.ID))
		}
		return strings.Compare(a.ID, b.ID)
	})
	return sorted
}

func layerPriority(layer PromptLayer) int {
	switch layer {
	case PromptLayerKernel:
		return 100
	case PromptLayerInstruction:
		return 80
	case PromptLayerCapability:
		return 60
	case PromptLayerContext:
		return 40
	case PromptLayerTurn:
		return 20
	default:
		return 0
	}
}

func slotPriority(slot PromptSlot) int {
	switch slot {
	case PromptSlotIdentity:
		return 1000
	case PromptSlotHierarchy:
		return 990
	case PromptSlotWorkspace:
		return 900
	case PromptSlotTooling:
		return 800
	case PromptSlotMCP:
		return 790
	case PromptSlotSkillCatalog:
		return 780
	case PromptSlotActiveSkill:
		return 770
	case PromptSlotMemory:
		return 700
	case PromptSlotOutput:
		return 695
	case PromptSlotRuntime:
		return 690
	case PromptSlotSummary:
		return 680
	case PromptSlotMessage:
		return 600
	case PromptSlotSteering:
		return 590
	case PromptSlotSubTurn:
		return 580
	case PromptSlotInterrupt:
		return 570
	default:
		return 0
	}
}
