package executor

import (
	"context"
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"sync"
)

// paletteMu serializes design-intelligence.json writes across the two
// callers that touch it concurrently: the HTTP theme-apply handler
// (RecordThemePalette below) and the agent's writeDesignIntelligence tool.
// Without it, a user clicking Apply Theme while an agent run is mid-
// writeDesignIntelligence can race: both do read → deep-merge → write, and
// the second writer's snapshot predates the first's write, so last-writer-
// wins clobbers the other's merge. A per-workspace mutex keeps the
// read-merge-write sequence atomic inside this process. Cross-process
// safety is not a concern because only one builder process owns a
// workspace at a time (one-to-one mapping of workspace to server).
var (
	paletteMuMap sync.Map // workspacePath (string) → *sync.Mutex
)

func paletteMutex(workspacePath string) *sync.Mutex {
	if existing, ok := paletteMuMap.Load(workspacePath); ok {
		return existing.(*sync.Mutex)
	}
	created := &sync.Mutex{}
	actual, _ := paletteMuMap.LoadOrStore(workspacePath, created)
	return actual.(*sync.Mutex)
}

// DesignIntelligenceExecutor handles persisting and reading design decisions
type DesignIntelligenceExecutor struct {
	workspacePath string
}

// NewDesignIntelligenceExecutor creates a new DesignIntelligenceExecutor
func NewDesignIntelligenceExecutor(workspacePath string) *DesignIntelligenceExecutor {
	return &DesignIntelligenceExecutor{workspacePath: workspacePath}
}

func (e *DesignIntelligenceExecutor) filePath() string {
	return filepath.Join(e.workspacePath, "design-intelligence.json")
}

// WriteDesignIntelligence deep-merges new data into design-intelligence.json.
// Args: { "data": { ... } }
func (e *DesignIntelligenceExecutor) WriteDesignIntelligence(ctx context.Context, args map[string]interface{}) (*ToolResult, error) {
	dataRaw, ok := args["data"]
	if !ok {
		return &ToolResult{
			Success: false,
			Content: "Error: 'data' parameter is required. Provide a JSON object with design decisions.",
		}, nil
	}

	newData, ok := dataRaw.(map[string]interface{})
	if !ok {
		return &ToolResult{
			Success: false,
			Content: "Error: 'data' must be a JSON object.",
		}, nil
	}

	// Serialize read-merge-write with RecordThemePalette (called from the
	// HTTP theme-apply handler). Without this lock, a user clicking Apply
	// Theme during an agent run can race with the agent's own
	// writeDesignIntelligence call: both read the same pre-merge snapshot
	// and the second writer clobbers the first's merge.
	mu := paletteMutex(e.workspacePath)
	mu.Lock()
	defer mu.Unlock()

	existing, err := ReadJSONFile(e.filePath())
	if err != nil {
		return &ToolResult{
			Success: false,
			Content: fmt.Sprintf("Error reading existing design intelligence: %s", err.Error()),
		}, nil
	}

	merged := DeepMergeJSON(existing, newData)

	if err := WriteJSONFileAtomic(e.filePath(), merged); err != nil {
		return &ToolResult{
			Success: false,
			Content: fmt.Sprintf("Error writing design intelligence: %s", err.Error()),
		}, nil
	}

	keys := make([]string, 0, len(newData))
	for k := range newData {
		keys = append(keys, k)
	}

	return &ToolResult{
		Success: true,
		Content: fmt.Sprintf("Design intelligence updated. Merged keys: %v. Total keys in file: %d.", keys, len(merged)),
	}, nil
}

// RecordThemePalette updates the workspace's design-intelligence.json to
// reflect a newly-applied theme preset. Without this, the `colors` object
// goes stale after a theme swap and misleads future agent runs that read
// design-intelligence for styling consistency.
//
// The write is a deep-merge into the existing file so other fields
// (typography, component_vocabulary, visual_personality, etc.) are
// preserved. The `light` and `dark` maps REPLACE any prior light/dark
// palette entries — switching from Mocha to Ocean must not leak stale
// Mocha-only keys into the Ocean record. Sibling color decisions
// (palette_description, semantic_strategy, anything else the AI has
// previously written under `colors`) are preserved.
//
// An empty presetID is treated as "don't set a palette name" (still records
// the light/dark values); this tolerates older Laravel clients that don't
// forward the preset name.
func RecordThemePalette(workspacePath, presetID string, light, dark map[string]string) error {
	// Serialize concurrent writers to the same workspace's
	// design-intelligence.json. See paletteMu comment for why.
	mu := paletteMutex(workspacePath)
	mu.Lock()
	defer mu.Unlock()

	// Auto-create the workspace dir if absent — WriteJSONFileAtomic does
	// MkdirAll-parent on its own, but we need the workspace dir to exist
	// first so the parent lookup succeeds in edge cases (the outermost
	// workspaces/ dir may be missing on fresh installs).
	if err := os.MkdirAll(workspacePath, 0755); err != nil {
		return fmt.Errorf("mkdir workspace: %w", err)
	}

	path := filepath.Join(workspacePath, "design-intelligence.json")

	// NOTE: ReadJSONFile returns a freshly-decoded map on every call; we
	// mutate it in place below (delete + DeepMergeJSON). If ReadJSONFile
	// ever grows a cache, this function must make a deep copy first —
	// otherwise the mutation would corrupt state observed by other callers.
	existing, err := ReadJSONFile(path)
	if err != nil {
		return fmt.Errorf("read design-intelligence.json: %w", err)
	}

	// Build a {colors: {...}} patch. The light/dark submaps are CONVERTED
	// to map[string]interface{} so DeepMergeJSON's type assertions work —
	// and we explicitly REPLACE the whole light/dark slot rather than
	// deep-merging their keys (see test TestRecordThemePalette_OverwritesPriorPaletteValues).
	colorsPatch := map[string]interface{}{}
	if presetID != "" {
		colorsPatch["palette"] = presetID
	}
	colorsPatch["light"] = stringMapToInterfaceMap(light)
	colorsPatch["dark"] = stringMapToInterfaceMap(dark)

	// If the existing file has colors.light / colors.dark, wipe those
	// specific slots before merging so the replacement is a clean swap
	// instead of a union-of-keys. Other colors.* fields survive.
	if existingColors, ok := existing["colors"].(map[string]interface{}); ok {
		delete(existingColors, "light")
		delete(existingColors, "dark")
	}

	patch := map[string]interface{}{
		"colors": colorsPatch,
	}
	merged := DeepMergeJSON(existing, patch)

	if err := WriteJSONFileAtomic(path, merged); err != nil {
		return fmt.Errorf("write design-intelligence.json: %w", err)
	}
	return nil
}

func stringMapToInterfaceMap(m map[string]string) map[string]interface{} {
	out := make(map[string]interface{}, len(m))
	for k, v := range m {
		out[k] = v
	}
	return out
}

// ReadDesignIntelligence reads and returns design-intelligence.json contents.
func (e *DesignIntelligenceExecutor) ReadDesignIntelligence(ctx context.Context, args map[string]interface{}) (*ToolResult, error) {
	data, err := ReadJSONFile(e.filePath())
	if err != nil {
		return &ToolResult{
			Success: false,
			Content: fmt.Sprintf("Error reading design intelligence: %s", err.Error()),
		}, nil
	}

	if len(data) == 0 {
		return &ToolResult{
			Success: true,
			Content: "No design intelligence found. This is a new project with no recorded design decisions.",
		}, nil
	}

	content, err := json.MarshalIndent(data, "", "  ")
	if err != nil {
		return &ToolResult{
			Success: false,
			Content: fmt.Sprintf("Error formatting design intelligence: %s", err.Error()),
		}, nil
	}

	return &ToolResult{
		Success: true,
		Content: string(content),
	}, nil
}
