package main

import (
	"context"
	"fmt"
	"os"
	"path/filepath"
	"runtime"
	"strings"

	"github.com/spf13/cobra"
	"gitea.dockr.co/kev/go-jdenticon/jdenticon"
)

// isRootPath checks if the given path is a filesystem root.
// On Unix, this is "/". On Windows, this is a drive root like "C:\" or a UNC root.
func isRootPath(path string) bool {
	if path == "/" {
		return true
	}
	// On Windows, check for drive roots (e.g., "C:\") or when Dir(path) == path
	if runtime.GOOS == "windows" {
		// filepath.VolumeName returns "C:" for "C:\", empty for Unix paths
		vol := filepath.VolumeName(path)
		if vol != "" && (path == vol || path == vol+string(os.PathSeparator)) {
			return true
		}
	}
	// Generic check: if going up doesn't change the path, we're at root
	return filepath.Dir(path) == path
}

// validateAndResolveOutputPath ensures the target path is within or is the base directory.
// It returns the absolute, cleaned, and validated path, or an error.
// The baseDir is typically os.Getwd() for a CLI tool, representing the trusted root.
func validateAndResolveOutputPath(baseDir, outputPath string) (string, error) {
	// 1. Get absolute and cleaned path of the base directory.
	// This ensures we have a canonical, absolute reference for our allowed root.
	absBaseDir, err := filepath.Abs(baseDir)
	if err != nil {
		return "", fmt.Errorf("failed to get absolute path for base directory %q: %w", baseDir, err)
	}
	absBaseDir = filepath.Clean(absBaseDir)

	// 2. Get absolute and cleaned path of the user-provided output file.
	// This resolves all relative components (., ..) and converts to an absolute path.
	absOutputPath, err := filepath.Abs(outputPath)
	if err != nil {
		return "", fmt.Errorf("failed to get absolute path for output file %q: %w", outputPath, err)
	}
	absOutputPath = filepath.Clean(absOutputPath)

	// 3. Resolve any symlinks to ensure we're comparing canonical paths.
	// This handles cases like macOS where /var is symlinked to /private/var.
	absBaseDir, err = filepath.EvalSymlinks(absBaseDir)
	if err != nil {
		return "", fmt.Errorf("failed to resolve symlinks for base directory %q: %w", absBaseDir, err)
	}

	originalOutputPath := absOutputPath
	resolvedDir, err := filepath.EvalSymlinks(filepath.Dir(absOutputPath))
	if err != nil {
		// If the directory doesn't exist yet, try to resolve up to the existing parent
		parentDir := filepath.Dir(absOutputPath)
		for !isRootPath(parentDir) && parentDir != "." {
			if resolvedParent, err := filepath.EvalSymlinks(parentDir); err == nil {
				absOutputPath = filepath.Join(resolvedParent, filepath.Base(originalOutputPath))
				break
			}
			parentDir = filepath.Dir(parentDir)
		}
		// If we couldn't resolve any parent, keep the original path
		// (absOutputPath already equals originalOutputPath, nothing to do)
	} else {
		absOutputPath = filepath.Join(resolvedDir, filepath.Base(originalOutputPath))
	}

	absOutputPath = filepath.Clean(absOutputPath)

	// 4. Crucial Security Check: Ensure absOutputPath is within absBaseDir.
	//    a. If absOutputPath is identical to absBaseDir (e.g., user specified "."), it's allowed.
	//    b. Otherwise, absOutputPath must start with absBaseDir followed by a path separator.
	//       This prevents cases where absOutputPath is merely a prefix of absBaseDir
	//       (e.g., /home/user/project_other vs /home/user/project).
	if absOutputPath != absBaseDir && !strings.HasPrefix(absOutputPath, absBaseDir+string(os.PathSeparator)) {
		return "", fmt.Errorf("invalid output path: %q (resolved to %q) is outside allowed directory %q",
			outputPath, absOutputPath, absBaseDir)
	}

	return absOutputPath, nil
}

// generateCmd represents the generate command
var generateCmd = &cobra.Command{
	Use:   "generate <value>",
	Short: "Generate a single identicon",
	Long: `Generate a single identicon based on the provided value (e.g., a hash, username, or email).

The identicon will be written to stdout by default, or to a file if --output is specified.
All configuration options from the root command apply.

Examples:
  jdenticon generate "user@example.com" 
  jdenticon generate "user@example.com" --size 128 --format png --output avatar.png
  jdenticon generate "github-username" --color-saturation 0.7 --padding 0.1`,
	Args: cobra.ExactArgs(1), // Validate argument count at the Cobra level
	RunE: func(cmd *cobra.Command, args []string) error {
		// Get the input value
		value := args[0]

		// Get output file flag
		outputFile, _ := cmd.Flags().GetString("output")

		// Get format from viper
		format, err := getFormatFromViper()
		if err != nil {
			return err
		}

		// Populate library config from root persistent flags
		config, size, err := populateConfigFromFlags()
		if err != nil {
			return err
		}

		// Generate the identicon with custom config
		generator, err := jdenticon.NewGeneratorWithConfig(config, generatorCacheSize)
		if err != nil {
			return fmt.Errorf("failed to create generator: %w", err)
		}

		icon, err := generator.Generate(context.Background(), value, size)
		if err != nil {
			return fmt.Errorf("failed to generate identicon: %w", err)
		}

		// Generate output based on format
		result, err := renderIcon(icon, format)
		if err != nil {
			return err
		}

		// Output to file or stdout
		if outputFile != "" {
			// Determine the base directory for allowed writes. For a CLI, this is typically the CWD.
			baseDir, err := os.Getwd()
			if err != nil {
				return fmt.Errorf("failed to get current working directory: %w", err)
			}

			// Validate and resolve the user-provided output path.
			safeOutputPath, err := validateAndResolveOutputPath(baseDir, outputFile)
			if err != nil {
				// This is a security-related error, explicitly state it.
				return fmt.Errorf("security error: %w", err)
			}

			// Now use the safe and validated path for writing.
			// #nosec G306 -- 0644 is appropriate for generated image files (world-readable)
			if err := os.WriteFile(safeOutputPath, result, 0644); err != nil {
				return fmt.Errorf("failed to write output file %q: %w", safeOutputPath, err)
			}
			fmt.Fprintf(os.Stderr, "Identicon saved to %s\n", safeOutputPath)
		} else {
			// Write to stdout for piping
			if _, err := cmd.OutOrStdout().Write(result); err != nil {
				return fmt.Errorf("failed to write to stdout: %w", err)
			}
		}

		return nil
	},
}

func init() {
	rootCmd.AddCommand(generateCmd)

	// Define local flags specific to 'generate'
	generateCmd.Flags().StringP("output", "o", "", "Output file path. If empty, writes to stdout.")
}
