// Package workspace provides agent workspace lifecycle utilities: workspace
// materialization, local memory scaffolding/checkpoints, and review target/diff
// helpers.
package workspace

import (
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"strings"

	"github.com/tidbcloud/AgentTeam/desktop/internal/identitydoc"
	"github.com/tidbcloud/AgentTeam/desktop/internal/types"
)

const (
	materializationMarkerRelPath = ".agentteam/materialization.json"

	maxMaterializationSeedFiles         = 20
	maxMaterializationSeedFilePathRunes = 512
	maxMaterializationSeedFileBytes     = 1024 * 1024
	maxMaterializationSeedTotalBytes    = 4 * 1024 * 1024
)

type materializationMarker struct {
	RoleID               string `json:"roleId,omitempty"`
	SourceKind           string `json:"sourceKind,omitempty"`
	SourceRef            string `json:"sourceRef,omitempty"`
	ActiveDefinitionPath string `json:"activeDefinitionPath,omitempty"`
}

// MaterializeAgentWorkspace applies a server-provided workspace materialization
// plan to targetDir. Role materialization writes portable seed files only; it does
// not copy local memory from a source agent.
func MaterializeAgentWorkspace(targetDir string, plan types.WorkspaceMaterialization) error {
	if strings.TrimSpace(plan.RoleID) == "" && len(plan.SeedFiles) == 0 {
		return nil
	}
	if materializationMarkerExists(targetDir) {
		return nil
	}

	ready, err := workspaceReadyForMaterialization(targetDir)
	if err != nil {
		return err
	}
	if !ready {
		return nil
	}
	if err := os.MkdirAll(targetDir, 0o755); err != nil {
		return fmt.Errorf("mkdir materialization target: %w", err)
	}
	normalizedPlan, err := normalizedMaterializationPlan(targetDir, plan)
	if err != nil {
		return err
	}
	if err := writeSeedFiles(targetDir, normalizedPlan.SeedFiles); err != nil {
		return err
	}
	return writeMaterializationMarker(targetDir, normalizedPlan)
}

func materializationMarkerExists(targetDir string) bool {
	if _, err := os.Stat(filepath.Join(targetDir, materializationMarkerRelPath)); err == nil {
		return true
	}
	return false
}

func workspaceReadyForMaterialization(targetDir string) (bool, error) {
	entries, err := os.ReadDir(targetDir)
	if err != nil {
		if os.IsNotExist(err) {
			return true, nil
		}
		return false, err
	}
	for _, entry := range entries {
		name := entry.Name()
		if name == ".git" || name == ".agentteam" || name == "MEMORY.md" || name == "memory" {
			continue
		}
		return false, nil
	}
	return true, nil
}

func writeSeedFiles(targetDir string, seedFiles []types.WorkspaceSeedFile) error {
	targets, err := validateSeedFiles(targetDir, seedFiles)
	if err != nil {
		return err
	}
	for _, file := range seedFiles {
		relPath := strings.TrimSpace(file.Path)
		if relPath == "" {
			continue
		}
		targetPath := targets[relPath]
		if err := os.MkdirAll(filepath.Dir(targetPath), 0o755); err != nil {
			return err
		}
		if err := os.WriteFile(targetPath, []byte(file.Content), 0o644); err != nil {
			return err
		}
	}
	return nil
}

func validateSeedFiles(targetDir string, seedFiles []types.WorkspaceSeedFile) (map[string]string, error) {
	if len(seedFiles) > maxMaterializationSeedFiles {
		return nil, fmt.Errorf("seed files has %d items, max %d", len(seedFiles), maxMaterializationSeedFiles)
	}

	totalBytes := 0
	targets := make(map[string]string, len(seedFiles))
	for i, file := range seedFiles {
		relPath := strings.TrimSpace(file.Path)
		if relPath == "" {
			continue
		}
		if len([]rune(relPath)) > maxMaterializationSeedFilePathRunes {
			return nil, fmt.Errorf("seed file path[%d] exceeds %d runes", i, maxMaterializationSeedFilePathRunes)
		}
		targetPath, err := safeJoin(targetDir, relPath)
		if err != nil {
			return nil, err
		}
		contentBytes := len([]byte(file.Content))
		if contentBytes > maxMaterializationSeedFileBytes {
			return nil, fmt.Errorf("seed file %q exceeds %d bytes", relPath, maxMaterializationSeedFileBytes)
		}
		totalBytes += contentBytes
		if totalBytes > maxMaterializationSeedTotalBytes {
			return nil, fmt.Errorf("seed files exceed %d total bytes", maxMaterializationSeedTotalBytes)
		}
		targets[relPath] = targetPath
	}
	return targets, nil
}

func writeMaterializationMarker(targetDir string, plan types.WorkspaceMaterialization) error {
	markerPath := filepath.Join(targetDir, materializationMarkerRelPath)
	if err := os.MkdirAll(filepath.Dir(markerPath), 0o755); err != nil {
		return err
	}
	payload, err := json.MarshalIndent(materializationMarker{
		RoleID:               strings.TrimSpace(plan.RoleID),
		SourceKind:           strings.TrimSpace(plan.SourceKind),
		SourceRef:            strings.TrimSpace(plan.SourceRef),
		ActiveDefinitionPath: strings.TrimSpace(plan.ActiveDefinitionPath),
	}, "", "  ")
	if err != nil {
		return err
	}
	payload = append(payload, '\n')
	return os.WriteFile(markerPath, payload, 0o644)
}

func normalizedMaterializationPlan(targetDir string, plan types.WorkspaceMaterialization) (types.WorkspaceMaterialization, error) {
	normalized := plan
	normalized.ActiveDefinitionPath = identitydoc.ActiveIdentityPath
	seedFiles, err := normalizeSeedFiles(targetDir, normalized.SeedFiles, normalized.ActiveDefinitionPath)
	if err != nil {
		return types.WorkspaceMaterialization{}, err
	}
	normalized.SeedFiles = seedFiles
	return normalized, nil
}

func normalizeSeedFiles(targetDir string, seedFiles []types.WorkspaceSeedFile, activeDefinitionPath string) ([]types.WorkspaceSeedFile, error) {
	if len(seedFiles) == 0 {
		return nil, nil
	}
	normalized := make([]types.WorkspaceSeedFile, 0, len(seedFiles))
	for _, file := range seedFiles {
		path := strings.TrimSpace(file.Path)
		if path == "" {
			continue
		}
		if path == identitydoc.ActiveIdentityPath {
			path = activeDefinitionPath
		} else {
			if suspiciousPortableMaterializationPath(path) {
				return nil, fmt.Errorf("path traversal detected: %s", path)
			}
			if !portableMaterializationPath(path) {
				continue
			}
		}
		normalized = append(normalized, types.WorkspaceSeedFile{Path: path, Content: file.Content})
	}
	return normalized, nil
}

func suspiciousPortableMaterializationPath(path string) bool {
	raw := filepath.ToSlash(strings.TrimSpace(path))
	if !strings.HasPrefix(raw, ".agents/knowledge/") && !strings.HasPrefix(raw, ".agents/skills/") {
		return false
	}
	clean := filepath.ToSlash(filepath.Clean(raw))
	return clean != raw
}

func portableMaterializationPath(path string) bool {
	raw := filepath.ToSlash(strings.TrimSpace(path))
	clean := filepath.ToSlash(filepath.Clean(raw))
	if clean != raw {
		return false
	}
	if clean == "" {
		return false
	}
	if clean == identitydoc.ActiveIdentityPath {
		return true
	}
	if clean == "MEMORY.md" || clean == "state" || clean == "kb" || clean == "archive" ||
		strings.HasPrefix(clean, "state/") || strings.HasPrefix(clean, "kb/") || strings.HasPrefix(clean, "archive/") {
		return false
	}
	if strings.HasPrefix(clean, ".agents/knowledge/") && strings.HasSuffix(clean, ".md") {
		name := strings.TrimSuffix(strings.TrimPrefix(clean, ".agents/knowledge/"), ".md")
		if name == "" || name == "." || name == ".." || strings.Contains(name, "/") {
			return false
		}
		return true
	}
	if strings.HasPrefix(clean, ".agents/skills/") && strings.HasSuffix(clean, "/SKILL.md") {
		name := strings.TrimSuffix(strings.TrimPrefix(clean, ".agents/skills/"), "/SKILL.md")
		if name == "" || name == "." || name == ".." || strings.Contains(name, "/") {
			return false
		}
		return true
	}
	return false
}

func safeJoin(base, sub string) (string, error) {
	if sub == "" {
		return base, nil
	}
	resolved := filepath.Clean(filepath.Join(base, sub))
	if resolved != base && !strings.HasPrefix(resolved, base+string(os.PathSeparator)) {
		return "", fmt.Errorf("path traversal detected: %s", sub)
	}
	return resolved, nil
}
