package main

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

const version = "0.1.0"

func main() {
	if len(os.Args) < 2 {
		usage()
		os.Exit(1)
	}

	args := os.Args[1:]

	if args[0] == "-v" || args[0] == "--version" {
		fmt.Println("sigil", version)
		os.Exit(0)
	}

	var err error
	switch args[0] {
	case "apply":
		err = applyCmd(args[1:])
	case "add":
		err = addCmd(args[1:])
	case "unlink":
		err = unlinkCmd(args[1:])
	case "remove":
		err = removeCmd(args[1:])
	case "status":
		err = statusCmd()
	case "help", "-h", "--help":
		usage()
		return
	default:
		err = fmt.Errorf("unknown command %q", args[0])
	}

	if err != nil {
		fmt.Fprintln(os.Stderr, "sigil:", err)
		os.Exit(1)
	}
}

func usage() {
	fmt.Println("sigil: minimal dotfile symlink manager")
	fmt.Println("usage:")
	fmt.Println("  sigil apply [--prune]")
	fmt.Println("  sigil add <path>")
	fmt.Println("  sigil unlink <package> [--dry-run]")
	fmt.Println("  sigil remove <package> [--dry-run]")
	fmt.Println("  sigil status")
}

func applyCmd(args []string) error {
	prune := false
	for _, arg := range args {
		if arg == "--prune" {
			prune = true
			continue
		}
		return fmt.Errorf("unknown flag %q", arg)
	}

	repo, err := repoPath()
	if err != nil {
		return err
	}

	entries, err := os.ReadDir(repo)
	if err != nil {
		return err
	}

	var stales []string

	for _, entry := range entries {
		if !entry.IsDir() || strings.HasPrefix(entry.Name(), ".") {
			continue
		}

		pkgDir := filepath.Join(repo, entry.Name())
		configPath := filepath.Join(pkgDir, configFileName)
		if _, err := os.Stat(configPath); err != nil {
			if errors.Is(err, os.ErrNotExist) {
				continue
			}
			return err
		}

		cfg, err := loadConfig(configPath)
		if err != nil {
			return fmt.Errorf("%s: %w", entry.Name(), err)
		}

		targetRoot, err := selectTarget(cfg)
		if err != nil {
			if errors.Is(err, errTargetDisabled) {
				continue
			}
			return fmt.Errorf("%s: %w", entry.Name(), err)
		}

		filesDir := filepath.Join(pkgDir, filesDirName)
		if _, err := os.Stat(filesDir); err != nil {
			if errors.Is(err, os.ErrNotExist) {
				continue
			}
			return err
		}

		if err := applyPackage(filesDir, targetRoot, cfg); err != nil {
			return fmt.Errorf("%s: %w", entry.Name(), err)
		}

		stale, err := findStaleLinks(filesDir, targetRoot)
		if err != nil {
			return fmt.Errorf("%s: %w", entry.Name(), err)
		}
		stales = append(stales, stale...)
	}

	if len(stales) == 0 {
		return nil
	}

	if prune {
		return removeLinks(stales, false)
	}

	fmt.Printf("Stale links found: %d\n", len(stales))
	return handleStaleLinks(stales)
}

func addCmd(args []string) error {
	if len(args) < 1 {
		return errors.New("missing path")
	}

	argPath := args[0]
	if !filepath.IsAbs(argPath) {
		if cwd, err := os.Getwd(); err == nil {
			argPath = filepath.Join(cwd, argPath)
		}
	}

	absPath, err := filepath.Abs(argPath)
	if err != nil {
		return err
	}

	info, err := os.Lstat(absPath)
	if err != nil {
		return err
	}
	if info.Mode()&os.ModeSymlink != 0 {
		return fmt.Errorf("%s is a symlink; refusing to add", absPath)
	}

	repo, err := repoPath()
	if err != nil {
		return err
	}
	if err := os.MkdirAll(repo, 0o755); err != nil {
		return err
	}

	defaultTarget := absPath
	if !info.IsDir() {
		defaultTarget = filepath.Dir(absPath)
	}

	defaultPkg := filepath.Base(defaultTarget)
	if info.IsDir() {
		defaultPkg = filepath.Base(absPath)
	}

	var pkgName string

	matchedPkg, err := findPackageByTarget(repo, defaultTarget)
	if err != nil {
		return err
	}
	if matchedPkg != "" {
		ok, err := promptYesNo(fmt.Sprintf("Merge into existing package %q?", matchedPkg), true)
		if err != nil {
			return err
		}
		if ok {
			pkgName = matchedPkg
		}
	}

	if pkgName == "" {
		pkgName = promptWithDefault("Package name", defaultPkg)
		if pkgName == "" {
			return errors.New("package name cannot be empty")
		}
	}

	targetRootInput := defaultTarget
	if pkgName != matchedPkg || matchedPkg == "" {
		targetRootInput = promptWithDefault("Target path", defaultTarget)
	}
	targetRoot, err := filepath.Abs(expandHome(targetRootInput))
	if err != nil {
		return err
	}

	pkgDir := filepath.Join(repo, pkgName)
	pkgExists := false
	if _, err := os.Stat(pkgDir); err == nil {
		pkgExists = true
	} else if !errors.Is(err, os.ErrNotExist) {
		return err
	}

	filesDir := filepath.Join(pkgDir, filesDirName)
	if err := os.MkdirAll(filesDir, 0o755); err != nil {
		return err
	}

	configPath := filepath.Join(pkgDir, configFileName)
	if !pkgExists {
		if err := writeConfig(configPath, targetRoot); err != nil {
			return err
		}
	}
	if info.IsDir() {
		if pkgExists {
			return errors.New("cannot merge a directory into an existing package yet")
		}
		if err := moveDirContents(absPath, filesDir); err != nil {
			return err
		}
	} else {
		rel, err := filepath.Rel(targetRoot, absPath)
		if err != nil {
			return err
		}
		if strings.HasPrefix(rel, "..") {
			return fmt.Errorf("path %s is outside target %s", absPath, targetRoot)
		}

		destPath := filepath.Join(filesDir, rel)
		if err := os.MkdirAll(filepath.Dir(destPath), 0o755); err != nil {
			return err
		}
		if _, err := os.Stat(destPath); err == nil {
			return fmt.Errorf("destination already exists: %s", destPath)
		} else if !errors.Is(err, os.ErrNotExist) {
			return err
		}
		if err := os.Rename(absPath, destPath); err != nil {
			return err
		}
	}

	cfg, err := loadConfig(configPath)
	if err != nil {
		return err
	}

	return applyPackage(filesDir, targetRoot, cfg)
}

func unlinkCmd(args []string) error {
	flags, pkgSpec, err := parsePackageFlags(args)
	if err != nil {
		return err
	}

	pkgName, relPath, err := resolvePackageSpec(pkgSpec)
	if err != nil {
		return err
	}

	repo, err := repoPath()
	if err != nil {
		return err
	}

	pkgDir := filepath.Join(repo, pkgName)
	configPath := filepath.Join(pkgDir, configFileName)
	cfg, err := loadConfig(configPath)
	if err != nil {
		return err
	}

	targetRoot, err := selectTarget(cfg)
	if err != nil {
		return err
	}

	filesDir := filepath.Join(pkgDir, filesDirName)
	if _, err := os.Stat(filesDir); err != nil {
		return err
	}

	if relPath == "" {
		if err := restorePackage(filesDir, targetRoot, flags.dryRun); err != nil {
			return err
		}
		if flags.dryRun {
			return nil
		}
		if err := os.RemoveAll(pkgDir); err != nil {
			return err
		}
		fmt.Printf("removed package %s\n", pkgName)
		return nil
	}

	if err := restorePath(filesDir, targetRoot, relPath, flags.dryRun); err != nil {
		return err
	}
	if flags.dryRun {
		return nil
	}
	if err := os.RemoveAll(filepath.Join(filesDir, relPath)); err != nil {
		return err
	}
	fmt.Printf("removed %s:%s\n", pkgName, relPath)
	return nil
}

func removeCmd(args []string) error {
	flags, pkgSpec, err := parsePackageFlags(args)
	if err != nil {
		return err
	}

	pkgName, relPath, err := resolvePackageSpec(pkgSpec)
	if err != nil {
		return err
	}

	repo, err := repoPath()
	if err != nil {
		return err
	}

	pkgDir := filepath.Join(repo, pkgName)
	configPath := filepath.Join(pkgDir, configFileName)
	cfg, err := loadConfig(configPath)
	if err != nil {
		return err
	}

	targetRoot, err := selectTarget(cfg)
	if err != nil {
		return err
	}

	filesDir := filepath.Join(pkgDir, filesDirName)
	if _, err := os.Stat(filesDir); err != nil {
		return err
	}

	if relPath == "" {
		if err := restorePackage(filesDir, targetRoot, flags.dryRun); err != nil {
			return err
		}
		if flags.dryRun {
			return nil
		}
		if err := os.RemoveAll(pkgDir); err != nil {
			return err
		}
		fmt.Printf("removed package %s\n", pkgName)
		return nil
	}

	if err := restorePath(filesDir, targetRoot, relPath, flags.dryRun); err != nil {
		return err
	}

	if flags.dryRun {
		return nil
	}

	if err := os.RemoveAll(filepath.Join(filesDir, relPath)); err != nil {
		return err
	}
	fmt.Printf("removed %s:%s\n", pkgName, relPath)
	return nil
}

func statusCmd() error {
	repo, err := repoPath()
	if err != nil {
		return err
	}

	entries, err := os.ReadDir(repo)
	if err != nil {
		return err
	}

	for _, entry := range entries {
		if !entry.IsDir() || strings.HasPrefix(entry.Name(), ".") {
			continue
		}

		pkgDir := filepath.Join(repo, entry.Name())
		configPath := filepath.Join(pkgDir, configFileName)
		cfg, err := loadConfig(configPath)
		if err != nil {
			return fmt.Errorf("%s: %w", entry.Name(), err)
		}

		targetRoot, err := selectTarget(cfg)
		if err != nil {
			if errors.Is(err, errTargetDisabled) {
				continue
			}
			return fmt.Errorf("%s: %w", entry.Name(), err)
		}

		filesDir := filepath.Join(pkgDir, filesDirName)
		stale, err := findStaleLinks(filesDir, targetRoot)
		if err != nil {
			return fmt.Errorf("%s: %w", entry.Name(), err)
		}
		if len(stale) == 0 {
			fmt.Printf("%s: ok\n", entry.Name())
			continue
		}

		fmt.Printf("%s: stale links (%d)\n", entry.Name(), len(stale))
		for _, path := range stale {
			fmt.Printf("  %s\n", path)
		}
	}

	return nil
}
