package infra

import (
	"fmt"
	"os"
	"path/filepath"
	"strings"
)

// LayoutReport describes what VaultLayoutEnsure did (or would do) to a vault directory.
type LayoutReport struct {
	VaultPath string   `json:"vault_path"`
	Created   []string `json:"created"`    // dirs created (relative paths)
	Migrated  []string `json:"migrated"`   // renames executed, format "src -> dst" (relative)
	AlreadyOK []string `json:"already_ok"` // dirs that already existed at the target location
	Skipped   []string `json:"skipped"`    // unrecognized root-level entries, left untouched
	DryRun    bool     `json:"dry_run"`
}

// dataBuckets are root-level directories that belong under data/.
var dataBuckets = []string{"raw", "processed", "exports"}

// knowledgeBuckets are root-level directories that belong under knowledge/.
var knowledgeBuckets = []string{"decisions", "domains", "models", "benchmarks", "test_documents"}

// knownRootFiles are root-level files that should be moved to knowledge/.
var knownRootFiles = []string{"README.md", "README.txt"}

// VaultLayoutEnsure ensures a vault directory uses the canonical hybrid layout:
//
//	data/{raw,processed,exports}
//	knowledge/{decisions,domains,models,benchmarks,test_documents}
//
// Legacy vaults that have these directories at the root are migrated by renaming
// (or merging when both src and dst already exist). The operation is idempotent:
// a second run returns everything in AlreadyOK.
//
// When dryRun is true the function computes the report but does not touch the disk.
func VaultLayoutEnsure(vaultPath string, dryRun bool) (LayoutReport, error) {
	report := LayoutReport{DryRun: dryRun}

	// --- resolve path ---
	vaultPath = strings.TrimRight(vaultPath, "/\\")

	var err error
	vaultPath, err = filepath.Abs(vaultPath)
	if err != nil {
		return report, fmt.Errorf("vault_layout_ensure: abs(%q): %w", vaultPath, err)
	}

	// Follow symlinks for the vault root itself.
	resolved, err := filepath.EvalSymlinks(vaultPath)
	if err != nil {
		return report, fmt.Errorf("vault_layout_ensure: eval symlinks %q: %w", vaultPath, err)
	}
	vaultPath = resolved
	report.VaultPath = vaultPath

	// --- check that vault exists and is a directory ---
	info, err := os.Stat(vaultPath)
	if err != nil {
		return report, fmt.Errorf("vault_layout_ensure: stat %q: %w", vaultPath, err)
	}
	if !info.IsDir() {
		return report, fmt.Errorf("vault_layout_ensure: %q is not a directory", vaultPath)
	}

	// --- ensure top-level containers ---
	for _, container := range []string{"data", "knowledge"} {
		dst := filepath.Join(vaultPath, container)
		if err := ensureDir(dst, dryRun, container, &report); err != nil {
			return report, err
		}
	}

	// --- build migration table: root name -> relative destination ---
	type migration struct {
		rootName string // name in vault root (dir or file)
		dstRel   string // relative destination path inside vault
		isFile   bool
	}

	var migrations []migration
	for _, b := range dataBuckets {
		migrations = append(migrations, migration{rootName: b, dstRel: filepath.Join("data", b)})
	}
	for _, b := range knowledgeBuckets {
		migrations = append(migrations, migration{rootName: b, dstRel: filepath.Join("knowledge", b)})
	}
	for _, rf := range knownRootFiles {
		migrations = append(migrations, migration{rootName: rf, dstRel: filepath.Join("knowledge", "README.md"), isFile: true})
	}

	// Track which root names are "known" so we can compute Skipped.
	knownNames := make(map[string]struct{})
	for _, m := range migrations {
		knownNames[strings.ToLower(m.rootName)] = struct{}{}
	}
	knownNames["data"] = struct{}{}
	knownNames["knowledge"] = struct{}{}

	// --- apply migrations ---
	for _, m := range migrations {
		src := filepath.Join(vaultPath, m.rootName)
		dst := filepath.Join(vaultPath, m.dstRel)
		srcRel := m.rootName
		dstRel := m.dstRel

		srcExists := pathExists(src)
		dstExists := pathExists(dst)

		switch {
		case srcExists && dstExists:
			// Both exist: merge if directory, error on file collision.
			if m.isFile {
				return report, fmt.Errorf("vault_layout_ensure: conflict: both %q and %q exist", srcRel, dstRel)
			}
			if err := mergeDirs(src, dst, srcRel, dstRel, dryRun, &report); err != nil {
				return report, err
			}

		case srcExists && !dstExists:
			// Only source exists: rename.
			report.Migrated = append(report.Migrated, fmt.Sprintf("%s -> %s", srcRel, dstRel))
			if !dryRun {
				if err := os.Rename(src, dst); err != nil {
					return report, fmt.Errorf("vault_layout_ensure: rename %q -> %q: %w", src, dst, err)
				}
			}

		case !srcExists && dstExists:
			// Already migrated.
			report.AlreadyOK = append(report.AlreadyOK, dstRel)

		default:
			// Neither exists: create empty destination directory (skip for files).
			if !m.isFile {
				report.Created = append(report.Created, dstRel)
				if !dryRun {
					if err := os.MkdirAll(dst, 0o755); err != nil {
						return report, fmt.Errorf("vault_layout_ensure: mkdir %q: %w", dst, err)
					}
				}
			}
		}
	}

	// --- collect skipped (unrecognized root entries) ---
	entries, err := os.ReadDir(vaultPath)
	if err != nil {
		return report, fmt.Errorf("vault_layout_ensure: readdir %q: %w", vaultPath, err)
	}
	for _, e := range entries {
		if _, known := knownNames[strings.ToLower(e.Name())]; !known {
			report.Skipped = append(report.Skipped, e.Name())
		}
	}

	return report, nil
}

// ensureDir adds the dir to Created (and creates it) if it doesn't exist,
// or to AlreadyOK if it does. Used for top-level containers "data" and "knowledge".
func ensureDir(path string, dryRun bool, rel string, report *LayoutReport) error {
	if pathExists(path) {
		report.AlreadyOK = append(report.AlreadyOK, rel)
		return nil
	}
	report.Created = append(report.Created, rel)
	if dryRun {
		return nil
	}
	if err := os.MkdirAll(path, 0o755); err != nil {
		return fmt.Errorf("vault_layout_ensure: mkdir %q: %w", path, err)
	}
	return nil
}

// mergeDirs moves the contents of src into dst, then removes src if empty.
// Returns an error if any file in src already exists in dst (no overwrite policy).
func mergeDirs(src, dst, srcRel, dstRel string, dryRun bool, report *LayoutReport) error {
	children, err := os.ReadDir(src)
	if err != nil {
		return fmt.Errorf("vault_layout_ensure: readdir %q: %w", src, err)
	}

	for _, child := range children {
		childDst := filepath.Join(dst, child.Name())
		if pathExists(childDst) {
			return fmt.Errorf("vault_layout_ensure: merge conflict: %q already exists in %q (cannot overwrite %q)",
				child.Name(), dstRel, filepath.Join(srcRel, child.Name()))
		}
		childSrc := filepath.Join(src, child.Name())
		childSrcRel := filepath.Join(srcRel, child.Name())
		childDstRel := filepath.Join(dstRel, child.Name())
		report.Migrated = append(report.Migrated, fmt.Sprintf("%s -> %s", childSrcRel, childDstRel))
		if !dryRun {
			if err := os.Rename(childSrc, childDst); err != nil {
				return fmt.Errorf("vault_layout_ensure: rename %q -> %q: %w", childSrc, childDst, err)
			}
		}
	}

	// Remove the now-empty src directory.
	if !dryRun {
		// Re-check emptiness after renames.
		remaining, _ := os.ReadDir(src)
		if len(remaining) == 0 {
			if err := os.Remove(src); err != nil {
				return fmt.Errorf("vault_layout_ensure: remove empty src %q: %w", src, err)
			}
		}
	}
	return nil
}

// pathExists returns true if path exists (any type).
func pathExists(path string) bool {
	_, err := os.Lstat(path)
	return err == nil
}

// dirIsEmpty returns true if a directory exists and has no entries.
func dirIsEmpty(path string) bool {
	entries, err := os.ReadDir(path)
	if err != nil {
		return false
	}
	return len(entries) == 0
}

// _ prevents "declared but not used" if dirIsEmpty is only used in tests.
var _ = dirIsEmpty

// vaultLayoutKnownNames returns the set of root-level names managed by this function.
// Exported for use in tests.
func vaultLayoutKnownNames() map[string]struct{} {
	known := make(map[string]struct{})
	for _, b := range dataBuckets {
		known[b] = struct{}{}
	}
	for _, b := range knowledgeBuckets {
		known[b] = struct{}{}
	}
	for _, rf := range knownRootFiles {
		known[strings.ToLower(rf)] = struct{}{}
	}
	known["data"] = struct{}{}
	known["knowledge"] = struct{}{}
	return known
}

