package command

import (
	"os"
	"path/filepath"
	"slices"
	"sort"
	"strings"
)

// CandidateStatus is the diagnostic disposition of one command file.
type CandidateStatus string

const (
	CandidateWinner   CandidateStatus = "winner"
	CandidateShadowed CandidateStatus = "shadowed"
	CandidateError    CandidateStatus = "error"
)

// Candidate is one command file considered during Load, including shadowed
// sources that later directories override.
type Candidate struct {
	Name        string
	Description string
	Path        string
	Root        string
	Status      CandidateStatus
	WinnerPath  string
	Error       string
}

// RootInfo is one scanned commands directory.
type RootInfo struct {
	Dir    string
	Status string // ok | missing | not-directory | unreadable
}

// Inspection is a read-only snapshot matching command.Load override semantics
// (later dir wins) without changing Load itself.
type Inspection struct {
	Roots      []RootInfo
	Candidates []Candidate
	Winners    []Command
}

// Inspect walks dirs in order (same as Load) and records every candidate.
// Missing dirs are listed as missing and produce no warnings.
func Inspect(dirs ...string) Inspection {
	var roots []RootInfo
	// Per-name list of candidates in scan order; last becomes winner.
	byName := map[string][]Candidate{}
	winners := map[string]Command{}

	for _, dir := range dirs {
		root, err := filepath.Abs(dir)
		if err != nil {
			roots = append(roots, RootInfo{Dir: dir, Status: "unreadable"})
			continue
		}
		st := rootStatus(root)
		roots = append(roots, RootInfo{Dir: root, Status: st})
		if st != "ok" {
			continue
		}
		visited := map[string]bool{}
		if real, err := filepath.EvalSymlinks(root); err == nil {
			visited[real] = true
		} else {
			visited[root] = true
		}
		walkCommands(root, root, visited, func(path string) {
			c, perr := parseFile(root, path)
			if perr != nil {
				name := guessName(root, path)
				byName[name] = append(byName[name], Candidate{
					Name: name, Path: path, Root: root,
					Status: CandidateError, Error: perr.Error(),
				})
				return
			}
			byName[c.Name] = append(byName[c.Name], Candidate{
				Name: c.Name, Description: c.Description, Path: path, Root: root,
				Status: CandidateWinner, // provisional; finalized below
			})
			winners[c.Name] = c
		})
	}

	var candidates []Candidate
	for name, list := range byName {
		// Find last non-error candidate as winner; errors stay as errors.
		winIdx := -1
		for i, v := range slices.Backward(list) {
			if v.Status != CandidateError {
				winIdx = i
				break
			}
		}
		for i, c := range list {
			if c.Status == CandidateError {
				candidates = append(candidates, c)
				continue
			}
			if i == winIdx {
				c.Status = CandidateWinner
				c.WinnerPath = ""
			} else if winIdx >= 0 {
				c.Status = CandidateShadowed
				c.WinnerPath = list[winIdx].Path
			}
			candidates = append(candidates, c)
		}
		_ = name
	}

	cmds := make([]Command, 0, len(winners))
	for _, c := range winners {
		cmds = append(cmds, c)
	}
	sort.Slice(cmds, func(i, j int) bool { return cmds[i].Name < cmds[j].Name })
	sort.SliceStable(candidates, func(i, j int) bool {
		if candidates[i].Name != candidates[j].Name {
			return candidates[i].Name < candidates[j].Name
		}
		order := map[CandidateStatus]int{CandidateWinner: 0, CandidateShadowed: 1, CandidateError: 2}
		return order[candidates[i].Status] < order[candidates[j].Status]
	})

	return Inspection{Roots: roots, Candidates: candidates, Winners: cmds}
}

func rootStatus(dir string) string {
	info, err := os.Stat(dir)
	if err != nil {
		if os.IsNotExist(err) {
			return "missing"
		}
		return "unreadable"
	}
	if !info.IsDir() {
		return "not-directory"
	}
	f, err := os.Open(dir)
	if err != nil {
		return "unreadable"
	}
	_ = f.Close()
	return "ok"
}

func guessName(root, path string) string {
	rel, err := filepath.Rel(root, path)
	if err != nil {
		rel = filepath.Base(path)
	}
	return strings.ReplaceAll(strings.TrimSuffix(filepath.ToSlash(rel), ".md"), "/", ":")
}
