package config

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

	"gopkg.in/yaml.v3"
)

// ReposConfig represents the repos section of config.yaml
type ReposConfig struct {
	Primary    string   `yaml:"primary,omitempty"`
	Additional []string `yaml:"additional,omitempty,flow"`
}

// configFile represents the structure for reading/writing config.yaml
// We use yaml.Node to preserve comments and formatting
type configFile struct {
	root yaml.Node
}

// FindConfigYAMLPath finds the config.yaml file in .beads directory
// Walks up from CWD to find .beads/config.yaml
func FindConfigYAMLPath() (string, error) {
	cwd, err := os.Getwd()
	if err != nil {
		return "", fmt.Errorf("failed to get working directory: %w", err)
	}

	for dir := cwd; dir != filepath.Dir(dir); dir = filepath.Dir(dir) {
		beadsDir := filepath.Join(dir, ".beads")
		configPath := filepath.Join(beadsDir, "config.yaml")
		if _, err := os.Stat(configPath); err == nil {
			return configPath, nil
		}
	}

	return "", fmt.Errorf("no .beads/config.yaml found in current directory or parents")
}

// GetReposFromYAML reads the repos configuration from config.yaml
// Returns an empty ReposConfig if repos section doesn't exist
func GetReposFromYAML(configPath string) (*ReposConfig, error) {
	data, err := os.ReadFile(configPath) // #nosec G304 - config file path from caller
	if err != nil {
		if os.IsNotExist(err) {
			return &ReposConfig{}, nil
		}
		return nil, fmt.Errorf("failed to read config.yaml: %w", err)
	}

	// Parse into a generic map to extract repos section
	var cfg map[string]interface{}
	if err := yaml.Unmarshal(data, &cfg); err != nil {
		return nil, fmt.Errorf("failed to parse config.yaml: %w", err)
	}

	repos := &ReposConfig{}
	if reposRaw, ok := cfg["repos"]; ok && reposRaw != nil {
		reposMap, ok := reposRaw.(map[string]interface{})
		if !ok {
			return nil, fmt.Errorf("repos section is not a map")
		}

		if primary, ok := reposMap["primary"].(string); ok {
			repos.Primary = primary
		}

		if additional, ok := reposMap["additional"]; ok && additional != nil {
			switch v := additional.(type) {
			case []interface{}:
				for _, item := range v {
					if str, ok := item.(string); ok {
						repos.Additional = append(repos.Additional, str)
					}
				}
			}
		}
	}

	return repos, nil
}

// SetReposInYAML writes the repos configuration to config.yaml
// It preserves other config sections and comments where possible
func SetReposInYAML(configPath string, repos *ReposConfig) error {
	// Read existing config or create new
	data, err := os.ReadFile(configPath) // #nosec G304 - config file path from caller
	if err != nil && !os.IsNotExist(err) {
		return fmt.Errorf("failed to read config.yaml: %w", err)
	}

	// Parse existing config into yaml.Node to preserve structure
	var root yaml.Node
	if len(data) > 0 {
		if err := yaml.Unmarshal(data, &root); err != nil {
			return fmt.Errorf("failed to parse config.yaml: %w", err)
		}
	}

	// Handle empty or comment-only files by creating a valid document structure
	if root.Kind != yaml.DocumentNode || len(root.Content) == 0 {
		root = yaml.Node{
			Kind: yaml.DocumentNode,
			Content: []*yaml.Node{
				{Kind: yaml.MappingNode},
			},
		}
	}

	// Get the mapping node (first content of document)
	mapping := root.Content[0]
	if mapping.Kind != yaml.MappingNode {
		// If the document content isn't a mapping, replace it with one
		root.Content[0] = &yaml.Node{Kind: yaml.MappingNode}
		mapping = root.Content[0]
	}

	// Find or create repos section
	reposIndex := -1
	for i := 0; i < len(mapping.Content); i += 2 {
		if mapping.Content[i].Value == "repos" {
			reposIndex = i
			break
		}
	}

	// Build the repos node
	reposNode := buildReposNode(repos)

	if reposIndex >= 0 {
		// Update existing repos section
		if reposNode == nil {
			// Remove repos section entirely if empty
			mapping.Content = append(mapping.Content[:reposIndex], mapping.Content[reposIndex+2:]...)
		} else {
			mapping.Content[reposIndex+1] = reposNode
		}
	} else if reposNode != nil {
		// Add new repos section at the end
		mapping.Content = append(mapping.Content,
			&yaml.Node{Kind: yaml.ScalarNode, Value: "repos"},
			reposNode,
		)
	}

	// Marshal back to YAML
	var buf strings.Builder
	encoder := yaml.NewEncoder(&buf)
	encoder.SetIndent(2)
	if err := encoder.Encode(&root); err != nil {
		return fmt.Errorf("failed to encode config.yaml: %w", err)
	}
	if err := encoder.Close(); err != nil {
		return fmt.Errorf("failed to close encoder: %w", err)
	}

	// Write back to file
	if err := os.WriteFile(configPath, []byte(buf.String()), 0600); err != nil {
		return fmt.Errorf("failed to write config.yaml: %w", err)
	}

	// Reload viper config so changes take effect immediately
	if v != nil {
		if err := v.ReadInConfig(); err != nil {
			// Not fatal - config is on disk, will be picked up on next command
			_ = err
		}
	}

	return nil
}

// buildReposNode creates a yaml.Node for the repos configuration
// Returns nil if repos is empty (no primary and no additional)
func buildReposNode(repos *ReposConfig) *yaml.Node {
	if repos == nil || (repos.Primary == "" && len(repos.Additional) == 0) {
		return nil
	}

	node := &yaml.Node{Kind: yaml.MappingNode}

	if repos.Primary != "" {
		node.Content = append(node.Content,
			&yaml.Node{Kind: yaml.ScalarNode, Value: "primary"},
			&yaml.Node{Kind: yaml.ScalarNode, Value: repos.Primary, Style: yaml.DoubleQuotedStyle},
		)
	}

	if len(repos.Additional) > 0 {
		additionalNode := &yaml.Node{Kind: yaml.SequenceNode}
		for _, path := range repos.Additional {
			additionalNode.Content = append(additionalNode.Content,
				&yaml.Node{Kind: yaml.ScalarNode, Value: path, Style: yaml.DoubleQuotedStyle},
			)
		}
		node.Content = append(node.Content,
			&yaml.Node{Kind: yaml.ScalarNode, Value: "additional"},
			additionalNode,
		)
	}

	return node
}

// AddRepo adds a repository to the repos.additional list in config.yaml
// If primary is not set, it defaults to "."
func AddRepo(configPath, repoPath string) error {
	repos, err := GetReposFromYAML(configPath)
	if err != nil {
		return fmt.Errorf("failed to get repos config: %w", err)
	}

	// Set primary to "." if not already set (standard multi-repo convention)
	if repos.Primary == "" {
		repos.Primary = "."
	}

	// Check if repo already exists
	for _, existing := range repos.Additional {
		if existing == repoPath {
			return fmt.Errorf("repository already configured: %s", repoPath)
		}
	}

	// Add the new repo
	repos.Additional = append(repos.Additional, repoPath)

	return SetReposInYAML(configPath, repos)
}

// RemoveRepo removes a repository from the repos.additional list in config.yaml
func RemoveRepo(configPath, repoPath string) error {
	repos, err := GetReposFromYAML(configPath)
	if err != nil {
		return fmt.Errorf("failed to get repos config: %w", err)
	}

	// Find and remove the repo
	found := false
	newAdditional := make([]string, 0, len(repos.Additional))
	for _, existing := range repos.Additional {
		if existing == repoPath {
			found = true
			continue
		}
		newAdditional = append(newAdditional, existing)
	}

	if !found {
		return fmt.Errorf("repository not found: %s", repoPath)
	}

	repos.Additional = newAdditional

	// If no repos left, clear primary too
	if len(repos.Additional) == 0 {
		repos.Primary = ""
	}

	return SetReposInYAML(configPath, repos)
}

// ListRepos returns the current repos configuration from YAML
func ListRepos(configPath string) (*ReposConfig, error) {
	return GetReposFromYAML(configPath)
}
