package prompts

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"io/fs"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"time"

	brand "core/shared/config"
)

const (
	configDirName       = brand.ConfigDirName
	generatedDirName    = ".generated"
	recoveredDirName    = "recovered"
	generatedSkillsDir  = "skills"
	markerFileName      = brand.GeneratedMarkerName
	markerSchemaVersion = 1
	generatedRootPerm   = fs.FileMode(0o700)
	generatedDirPerm    = fs.FileMode(0o755)
	generatedFilePerm   = fs.FileMode(0o644)
	treeHashPrefix      = "sha256:"
	readmeFileName      = "README.md"
	timestampFormat     = "2006-01-02T15-04-05Z"

	// Default-layout paths referenced by the user-facing strings below,
	// assembled from the identity and directory constants at compile time so
	// they cannot drift from the real on-disk layout (and with no
	// init-time/runtime cost). Callers that know the active persistence root
	// render the equivalent strings from the resolved paths instead.
	generatedRootDir = brand.PersistenceRoot + "/" + generatedDirName
	recoveredDir     = brand.PersistenceRoot + "/" + recoveredDirName
	workspaceSkills  = "<workspace>/" + brand.ConfigDirName + "/" + generatedSkillsDir
)

// generatedReadme renders the generated-root README for a concrete on-disk
// layout. The home-skills and recovery paths are interpolated from the active
// persistence root so an isolated instance instructs users to customize and
// clean up that instance rather than the default <home>/.kent layout.
func generatedReadme(homeSkillsRoot, recoveryRoot string) string {
	return "Generated by " + brand.Product + ". Do not edit this directory. It may be deleted or replaced on startup. To customize skills, copy them to " + homeSkillsRoot + " or " + workspaceSkills + ". If " + brand.Product + " recovered generated edits, check " + recoveryRoot + ".\n"
}

// recoveredWarning renders the generated-root edit warning for a concrete
// on-disk layout. The paths are interpolated from the active persistence root so
// the remediation always points at the instance that actually recovered files,
// rather than the default <home>/.kent layout.
func recoveredWarning(generatedRoot, recoveryRoot string) string {
	return generatedRoot + " folder was edited. This shouldn't be done and is a user error. Your edited folder was recovered to " + recoveryRoot + ". Please clean and remove that folder to make this warning go away, and do not attempt to edit " + generatedRoot + " in the future as it is overwritten on every startup and intended for seeding files for usage by the agent only. If you wanted to disable skills there, disable them in config.toml according to documentation."
}

type GeneratedSyncOptions struct {
	HomeDir string
	// ConfigRoot is the absolute persistence root that owns generated assets.
	// When non-empty it is used directly (generated assets live under
	// <ConfigRoot>/.generated); empty falls back to <home>/.kent.
	ConfigRoot string
	Now        func() time.Time
	FS         fs.FS
}

type GeneratedSyncResult struct {
	GeneratedRoot         string
	GeneratedSkillsRoot   string
	RecoveryRoot          string
	Recovered             bool
	RecoveryPath          string
	RecoveredWarning      string
	RecoveredRootNonEmpty bool
}

type GeneratedLayout struct {
	GeneratedRoot       string
	GeneratedSkillsRoot string
	UserSkillsRoot      string
	RecoveryRoot        string
}

type marker struct {
	SchemaVersion int    `json:"schema_version"`
	AppVersion    string `json:"app_version"`
	TreeHash      string `json:"tree_hash"`
}

type treeEntryKind string

const (
	treeEntryDir  treeEntryKind = "dir"
	treeEntryFile treeEntryKind = "file"
)

type treeEntry struct {
	Path    string
	Kind    treeEntryKind
	Perm    fs.FileMode
	Content []byte
}

func GeneratedSync(ctx context.Context, opts GeneratedSyncOptions) (GeneratedSyncResult, error) {
	paths, err := resolvePathsForRoot(opts.HomeDir, opts.ConfigRoot)
	if err != nil {
		return GeneratedSyncResult{}, err
	}
	result := GeneratedSyncResult{
		GeneratedRoot:       paths.generatedRoot,
		GeneratedSkillsRoot: paths.generatedSkillsRoot,
		RecoveryRoot:        paths.recoveryRoot,
	}
	if err := ctx.Err(); err != nil {
		return result, err
	}
	expected, err := expectedTree(opts.FS, generatedReadme(paths.homeSkillsRoot, paths.recoveryRoot))
	if err != nil {
		return result, err
	}
	state, err := inspectGeneratedRoot(paths.generatedRoot)
	if err != nil {
		return result, err
	}
	needsSeed := false
	needsRecovery := false
	if state.missing {
		needsSeed = true
	} else if state.empty || state.unsupportedRoot {
		needsRecovery = true
	} else {
		clean, err := generatedRootMatchesMarker(paths.generatedRoot)
		if err != nil {
			needsRecovery = true
		} else if clean {
			needsSeed = true
		} else {
			needsRecovery = true
		}
	}
	if needsRecovery {
		recoveryPath, err := recoverGeneratedRoot(paths, opts.Now)
		if err != nil {
			return result, err
		}
		result.Recovered = true
		result.RecoveryPath = recoveryPath
		result.RecoveredWarning = recoveredWarning(paths.generatedRoot, paths.recoveryRoot)
		needsSeed = true
	}
	if needsSeed {
		if err := writeExpectedTree(paths.generatedRoot, expected); err != nil {
			return result, err
		}
	}
	if nonEmpty, err := recoveredRootNonEmpty(paths.recoveryRoot); err == nil {
		result.RecoveredRootNonEmpty = nonEmpty
		if nonEmpty {
			result.RecoveredWarning = recoveredWarning(paths.generatedRoot, paths.recoveryRoot)
		}
	}
	return result, nil
}

func GeneratedRoot() (string, error) {
	layout, err := GeneratedLayoutFor("")
	if err != nil {
		return "", err
	}
	return layout.GeneratedRoot, nil
}

func GeneratedSkillsRoot() (string, error) {
	layout, err := GeneratedLayoutFor("")
	if err != nil {
		return "", err
	}
	return layout.GeneratedSkillsRoot, nil
}

// GeneratedSkillsRootFor resolves the generated skills root under the supplied
// persistence config root. An empty configRoot falls back to the default
// (<home>/.kent), matching GeneratedSkillsRoot().
func GeneratedSkillsRootFor(configRoot string) (string, error) {
	layout, err := GeneratedLayoutFor(configRoot)
	if err != nil {
		return "", err
	}
	return layout.GeneratedSkillsRoot, nil
}

func RecoveryRoot() (string, error) {
	layout, err := GeneratedLayoutFor("")
	if err != nil {
		return "", err
	}
	return layout.RecoveryRoot, nil
}

func GeneratedLayoutFor(configRoot string) (GeneratedLayout, error) {
	paths, err := resolvePathsForRoot("", configRoot)
	if err != nil {
		return GeneratedLayout{}, err
	}
	return GeneratedLayout{
		GeneratedRoot:       paths.generatedRoot,
		GeneratedSkillsRoot: paths.generatedSkillsRoot,
		UserSkillsRoot:      paths.homeSkillsRoot,
		RecoveryRoot:        paths.recoveryRoot,
	}, nil
}

// RecoveredWarning returns the default-root recovery warning (referring to the
// <home>/.kent layout). Callers that know the active persistence root should
// prefer RecoveredWarningFor so the remediation points at the right instance.
func RecoveredWarning() string {
	return recoveredWarning(generatedRootDir, recoveredDir)
}

// RecoveredWarningFor renders the recovery warning for the generated/recovery
// layout under the supplied persistence config root. An empty configRoot falls
// back to the default (<home>/.kent), matching RecoveredWarning, so the warning
// follows the selected root the same way generated sync does.
func RecoveredWarningFor(configRoot string) (string, error) {
	paths, err := resolvePathsForRoot("", configRoot)
	if err != nil {
		return "", err
	}
	return recoveredWarning(paths.generatedRoot, paths.recoveryRoot), nil
}

func RecoveredRootNonEmpty() (bool, error) {
	return RecoveredRootNonEmptyFor("")
}

// RecoveredRootNonEmptyFor reports whether the recovered directory under the
// supplied persistence config root holds any salvaged content. An empty
// configRoot falls back to the default (<home>/.kent), matching
// RecoveredRootNonEmpty, so the recovery warning follows the selected root the
// same way generated sync does.
func RecoveredRootNonEmptyFor(configRoot string) (bool, error) {
	paths, err := resolvePathsForRoot("", configRoot)
	if err != nil {
		return false, err
	}
	return recoveredRootNonEmpty(paths.recoveryRoot)
}

type generatedPaths struct {
	generatedRoot       string
	generatedSkillsRoot string
	// homeSkillsRoot is the persistence-root-level skills directory
	// (<root>/skills) where users place customized global skills.
	homeSkillsRoot string
	recoveryRoot   string
}

func resolvePaths(homeDir string) (generatedPaths, error) {
	return resolvePathsForRoot(homeDir, "")
}

// resolvePathsForRoot resolves the generated/recovery layout. When configRoot
// is non-empty it is used directly as the persistence root; otherwise the
// layout falls back to <home>/.kent (resolving homeDir or the OS home).
func resolvePathsForRoot(homeDir, configRoot string) (generatedPaths, error) {
	root := strings.TrimSpace(configRoot)
	if root == "" {
		home := strings.TrimSpace(homeDir)
		if home == "" {
			resolved, err := os.UserHomeDir()
			if err != nil {
				return generatedPaths{}, fmt.Errorf("resolve home dir: %w", err)
			}
			home = resolved
		}
		if strings.TrimSpace(home) == "" {
			return generatedPaths{}, errors.New("home dir is required")
		}
		root = filepath.Join(home, configDirName)
	}
	generatedRoot := filepath.Join(root, generatedDirName)
	return generatedPaths{
		generatedRoot:       generatedRoot,
		generatedSkillsRoot: filepath.Join(generatedRoot, generatedSkillsDir),
		homeSkillsRoot:      filepath.Join(root, generatedSkillsDir),
		recoveryRoot:        filepath.Join(root, recoveredDirName),
	}, nil
}

type generatedRootState struct {
	missing         bool
	empty           bool
	unsupportedRoot bool
}

func inspectGeneratedRoot(path string) (generatedRootState, error) {
	info, err := os.Lstat(path)
	if err != nil {
		if os.IsNotExist(err) {
			return generatedRootState{missing: true}, nil
		}
		return generatedRootState{}, fmt.Errorf("stat generated root: %w", err)
	}
	if !info.IsDir() {
		return generatedRootState{unsupportedRoot: true}, nil
	}
	entries, err := os.ReadDir(path)
	if err != nil {
		return generatedRootState{}, fmt.Errorf("read generated root: %w", err)
	}
	return generatedRootState{empty: len(entries) == 0}, nil
}

func generatedRootMatchesMarker(root string) (bool, error) {
	parsed, err := readMarker(filepath.Join(root, markerFileName))
	if err != nil {
		return false, err
	}
	actual, err := hashActualTree(root)
	if err != nil {
		return false, err
	}
	return actual == parsed.TreeHash, nil
}

func readMarker(path string) (marker, error) {
	data, err := os.ReadFile(path)
	if err != nil {
		return marker{}, err
	}
	var parsed marker
	if err := json.Unmarshal(data, &parsed); err != nil {
		return marker{}, err
	}
	if parsed.SchemaVersion != markerSchemaVersion || strings.TrimSpace(parsed.TreeHash) == "" {
		return marker{}, errors.New("invalid generated marker")
	}
	return parsed, nil
}

func expectedTree(customFS fs.FS, readme string) ([]treeEntry, error) {
	source := customFS
	if source == nil {
		source = GeneratedSkillsFS
	}
	entries := []treeEntry{
		{Path: readmeFileName, Kind: treeEntryFile, Perm: generatedFilePerm, Content: []byte(readme)},
		{Path: generatedSkillsDir, Kind: treeEntryDir, Perm: generatedDirPerm},
	}
	if err := fs.WalkDir(source, generatedSkillsDir, func(path string, entry fs.DirEntry, err error) error {
		if err != nil {
			return err
		}
		info, err := entry.Info()
		if err != nil {
			return err
		}
		mode := info.Mode()
		rel := filepath.ToSlash(path)
		if mode.IsDir() {
			if rel != generatedSkillsDir {
				entries = append(entries, treeEntry{Path: rel, Kind: treeEntryDir, Perm: generatedDirPerm})
			}
			return nil
		}
		if !mode.IsRegular() {
			return fmt.Errorf("generated asset %s has unsupported mode %s", rel, mode)
		}
		data, err := fs.ReadFile(source, path)
		if err != nil {
			return err
		}
		entries = append(entries, treeEntry{Path: rel, Kind: treeEntryFile, Perm: generatedFilePerm, Content: data})
		return nil
	}); err != nil {
		return nil, fmt.Errorf("walk generated assets: %w", err)
	}
	return sortedTree(entries), nil
}

func sortedTree(entries []treeEntry) []treeEntry {
	out := append([]treeEntry(nil), entries...)
	sort.Slice(out, func(i, j int) bool {
		if out[i].Path != out[j].Path {
			return out[i].Path < out[j].Path
		}
		return out[i].Kind < out[j].Kind
	})
	return out
}

func hashActualTree(root string) (string, error) {
	items := make([]actualTreeItem, 0)
	err := filepath.WalkDir(root, func(path string, entry fs.DirEntry, err error) error {
		if err != nil {
			return err
		}
		if path == root {
			info, err := os.Lstat(path)
			if err != nil {
				return err
			}
			mode := info.Mode()
			if !mode.IsDir() {
				return fmt.Errorf("generated root has unsupported mode %s", mode)
			}
			items = append(items, actualTreeItem{Path: ".", Kind: treeEntryDir, Perm: mode.Perm()})
			return nil
		}
		rel, err := filepath.Rel(root, path)
		if err != nil {
			return err
		}
		rel = filepath.ToSlash(rel)
		if rel == markerFileName {
			return nil
		}
		info, err := os.Lstat(path)
		if err != nil {
			return err
		}
		mode := info.Mode()
		if mode.IsDir() {
			items = append(items, actualTreeItem{Path: rel, Kind: treeEntryDir, Perm: mode.Perm()})
			return nil
		}
		if !mode.IsRegular() {
			return fmt.Errorf("generated entry %s has unsupported mode %s", rel, mode)
		}
		items = append(items, actualTreeItem{Path: rel, Kind: treeEntryFile, Perm: mode.Perm(), FilePath: path})
		return nil
	})
	if err != nil {
		return "", err
	}
	return hashActualTreeItems(items)
}

type actualTreeItem struct {
	Path     string
	Kind     treeEntryKind
	Perm     fs.FileMode
	FilePath string
}

func hashActualTreeItems(items []actualTreeItem) (string, error) {
	sorted := append([]actualTreeItem(nil), items...)
	sort.Slice(sorted, func(i, j int) bool {
		if sorted[i].Path != sorted[j].Path {
			return sorted[i].Path < sorted[j].Path
		}
		return sorted[i].Kind < sorted[j].Kind
	})
	h := sha256.New()
	buffer := make([]byte, 32*1024)
	for _, item := range sorted {
		h.Write([]byte(item.Path))
		h.Write([]byte{0})
		h.Write([]byte(item.Kind))
		h.Write([]byte{0})
		h.Write([]byte(item.Perm.String()))
		h.Write([]byte{0})
		if item.Kind == treeEntryFile {
			file, err := os.Open(item.FilePath)
			if err != nil {
				return "", err
			}
			_, copyErr := io.CopyBuffer(h, file, buffer)
			closeErr := file.Close()
			if copyErr != nil {
				return "", copyErr
			}
			if closeErr != nil {
				return "", closeErr
			}
		}
		h.Write([]byte{0})
	}
	return treeHashPrefix + hex.EncodeToString(h.Sum(nil)), nil
}

func writeExpectedTree(root string, entries []treeEntry) error {
	if err := os.MkdirAll(filepath.Dir(root), 0o755); err != nil {
		return fmt.Errorf("create generated parent: %w", err)
	}
	tempRoot, err := os.MkdirTemp(filepath.Dir(root), ".generated-tmp-")
	if err != nil {
		return fmt.Errorf("create generated temp root: %w", err)
	}
	if err := os.Chmod(tempRoot, generatedRootPerm); err != nil {
		return fmt.Errorf("set generated temp root mode: %w", err)
	}
	cleanup := true
	defer func() {
		if cleanup {
			_ = os.RemoveAll(tempRoot)
		}
	}()
	for _, entry := range sortedTree(entries) {
		target := filepath.Join(tempRoot, filepath.FromSlash(entry.Path))
		if entry.Kind == treeEntryDir {
			if err := os.MkdirAll(target, entry.Perm); err != nil {
				return fmt.Errorf("create generated dir %s: %w", entry.Path, err)
			}
			continue
		}
		if err := os.MkdirAll(filepath.Dir(target), generatedDirPerm); err != nil {
			return fmt.Errorf("create generated file parent %s: %w", entry.Path, err)
		}
		if err := os.WriteFile(target, entry.Content, entry.Perm); err != nil {
			return fmt.Errorf("write generated file %s: %w", entry.Path, err)
		}
	}
	treeHash, err := hashActualTree(tempRoot)
	if err != nil {
		return fmt.Errorf("hash generated temp root: %w", err)
	}
	markerData, err := json.MarshalIndent(marker{SchemaVersion: markerSchemaVersion, AppVersion: brand.Version, TreeHash: treeHash}, "", "  ")
	if err != nil {
		return fmt.Errorf("encode generated marker: %w", err)
	}
	markerData = append(markerData, '\n')
	if err := os.WriteFile(filepath.Join(tempRoot, markerFileName), markerData, 0o644); err != nil {
		return fmt.Errorf("write generated marker: %w", err)
	}
	if err := os.RemoveAll(root); err != nil {
		return fmt.Errorf("replace generated root: remove old: %w", err)
	}
	if err := os.Rename(tempRoot, root); err != nil {
		return fmt.Errorf("replace generated root: %w", err)
	}
	cleanup = false
	return nil
}

func recoverGeneratedRoot(paths generatedPaths, now func() time.Time) (string, error) {
	info, err := os.Lstat(paths.generatedRoot)
	if err != nil {
		if os.IsNotExist(err) {
			return "", nil
		}
		return "", fmt.Errorf("stat generated root for recovery: %w", err)
	}
	if err := os.MkdirAll(paths.recoveryRoot, 0o755); err != nil {
		return "", fmt.Errorf("create recovery root: %w", err)
	}
	base, err := nextRecoveryDir(paths.recoveryRoot, now)
	if err != nil {
		return "", err
	}
	target := filepath.Join(base, generatedDirName)
	if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
		return "", fmt.Errorf("create recovery target parent: %w", err)
	}
	if !info.IsDir() && info.Mode()&os.ModeSymlink != 0 {
		if err := os.Rename(paths.generatedRoot, target); err != nil {
			return "", fmt.Errorf("recover generated symlink: %w", err)
		}
		return target, nil
	}
	if err := os.Rename(paths.generatedRoot, target); err != nil {
		return "", fmt.Errorf("recover generated root: %w", err)
	}
	return target, nil
}

func nextRecoveryDir(recoveryRoot string, now func() time.Time) (string, error) {
	current := time.Now().UTC()
	if now != nil {
		current = now().UTC()
	}
	stem := current.Format(timestampFormat)
	for idx := 1; ; idx++ {
		name := stem
		if idx > 1 {
			name = fmt.Sprintf("%s-%d", stem, idx)
		}
		candidate := filepath.Join(recoveryRoot, name)
		err := os.Mkdir(candidate, 0o755)
		if err == nil {
			return candidate, nil
		}
		if os.IsExist(err) {
			continue
		}
		return "", fmt.Errorf("create recovery dir: %w", err)
	}
}

func recoveredRootNonEmpty(path string) (bool, error) {
	entries, err := os.ReadDir(path)
	if err != nil {
		if os.IsNotExist(err) {
			return false, nil
		}
		return false, fmt.Errorf("read recovery root: %w", err)
	}
	return len(entries) > 0, nil
}
