package scanner

import (
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"sort"
	"strconv"
	"strings"
)

type ScanResult struct {
	Path        string `json:"path"`
	Size        int64  `json:"size"`
	IsDirectory bool   `json:"isDirectory"`
}

type DiskUsage struct {
	TotalGB string `json:"totalGB"`
	UsedGB  string `json:"usedGB"`
	FreeGB  string `json:"freeGB"`
}

type CategorySizes struct {
	Documents int64 `json:"documents"` // Personal Docs only
	Downloads int64 `json:"downloads"`
	Desktop   int64 `json:"desktop"`
	Music     int64 `json:"music"`
	Movies    int64 `json:"movies"`
	System    int64 `json:"system"`
	Trash     int64 `json:"trash"`
	Apps      int64 `json:"apps"`
	Photos    int64 `json:"photos"`
	ICloud    int64 `json:"icloud"`
}

// GetDiskUsage uses diskutil for accurate APFS disk usage
func GetDiskUsage() (*DiskUsage, error) {
	cmd := exec.Command("diskutil", "info", "/")
	out, err := cmd.Output()
	if err != nil {
		return nil, err
	}

	lines := strings.Split(string(out), "\n")

	var containerTotal, containerFree int64

	for _, line := range lines {
		line = strings.TrimSpace(line)

		// Parse "Container Total Space:" line
		if strings.HasPrefix(line, "Container Total Space:") {
			// Format: "Container Total Space:     245.1 GB (245107195904 Bytes)"
			if start := strings.Index(line, "("); start != -1 {
				if end := strings.Index(line[start:], " Bytes)"); end != -1 {
					bytesStr := line[start+1 : start+end]
					containerTotal, _ = strconv.ParseInt(bytesStr, 10, 64)
				}
			}
		}

		// Parse "Container Free Space:" line
		if strings.HasPrefix(line, "Container Free Space:") {
			if start := strings.Index(line, "("); start != -1 {
				if end := strings.Index(line[start:], " Bytes)"); end != -1 {
					bytesStr := line[start+1 : start+end]
					containerFree, _ = strconv.ParseInt(bytesStr, 10, 64)
				}
			}
		}
	}

	// Calculate used space
	containerUsed := containerTotal - containerFree

	toGB := func(bytes int64) string {
		gb := float64(bytes) / 1024 / 1024 / 1024
		return fmt.Sprintf("%.2f", gb)
	}

	return &DiskUsage{
		TotalGB: toGB(containerTotal),
		UsedGB:  toGB(containerUsed),
		FreeGB:  toGB(containerFree),
	}, nil
}

// FindLargeFiles walks a directory and returns files > threshold
func FindLargeFiles(root string, threshold int64) ([]ScanResult, error) {
	var results []ScanResult

	err := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
		if err != nil {
			return nil // Skip errors
		}

		// Skip hidden files/dirs (except .Trash maybe, but let's skip all . for now)
		if strings.HasPrefix(d.Name(), ".") {
			if d.IsDir() {
				return filepath.SkipDir
			}
			return nil
		}

		// Skip node_modules explicitly
		if d.IsDir() && d.Name() == "node_modules" {
			return filepath.SkipDir
		}

		if !d.IsDir() {
			info, err := d.Info()
			if err == nil && info.Size() > threshold {
				results = append(results, ScanResult{
					Path:        path,
					Size:        info.Size(),
					IsDirectory: false,
				})
			}
		}
		return nil
	})

	// Sort by size desc
	sort.Slice(results, func(i, j int) bool {
		return results[i].Size > results[j].Size
	})

	// Return top 50
	if len(results) > 50 {
		return results[:50], err
	}
	return results, err
}

// FindHeavyFolders uses `du` to find large directories
func FindHeavyFolders(root string) ([]ScanResult, error) {
	// du -k -d 2 <root> | sort -nr | head -n 50
	cmd := exec.Command("bash", "-c", fmt.Sprintf("du -k -d 2 \"%s\" | sort -nr | head -n 50", root))
	out, err := cmd.Output()
	if err != nil {
		return nil, err
	}

	var results []ScanResult
	lines := strings.Split(string(out), "\n")
	for _, line := range lines {
		line = strings.TrimSpace(line)
		if line == "" {
			continue
		}

		// Attempt to parse first part as size
		firstSpace := strings.IndexAny(line, " \t")
		if firstSpace == -1 {
			continue
		}

		sizeStr := line[:firstSpace]
		pathStr := strings.TrimSpace(line[firstSpace:])

		if pathStr == root {
			continue
		}

		sizeK, err := strconv.ParseInt(sizeStr, 10, 64)
		if err == nil {
			results = append(results, ScanResult{
				Path:        pathStr,
				Size:        sizeK * 1024,
				IsDirectory: true,
			})
		}
	}

	return results, nil
}

func ScanUserDocuments() ([]ScanResult, error) {
	home, err := os.UserHomeDir()
	if err != nil {
		return nil, err
	}

	targets := []string{
		filepath.Join(home, "Documents"),
		filepath.Join(home, "Downloads"),
		filepath.Join(home, "Desktop"),
	}

	var allResults []ScanResult
	for _, t := range targets {
		// 10MB threshold
		res, _ := FindLargeFiles(t, 10*1024*1024)
		allResults = append(allResults, res...)
	}

	// Sort combined
	sort.Slice(allResults, func(i, j int) bool {
		return allResults[i].Size > allResults[j].Size
	})

	if len(allResults) > 50 {
		return allResults[:50], nil
	}
	return allResults, nil
}

func ScanSystemData() ([]ScanResult, error) {
	home, err := os.UserHomeDir()
	if err != nil {
		return nil, err
	}

	// System targets: Caches, Logs, Application Support (selective)
	targets := []string{
		filepath.Join(home, "Library", "Caches"),
		filepath.Join(home, "Library", "Logs"),
		filepath.Join(home, "Library", "Developer", "Xcode", "DerivedData"),
	}

	var allResults []ScanResult
	for _, t := range targets {
		// 10MB threshold
		res, _ := FindLargeFiles(t, 10*1024*1024)
		allResults = append(allResults, res...)
	}

	sort.Slice(allResults, func(i, j int) bool {
		return allResults[i].Size > allResults[j].Size
	})

	if len(allResults) > 50 {
		return allResults[:50], nil
	}
	return allResults, nil
}

func GetDirectorySize(path string) int64 {
	// du -s -k <path>
	cmd := exec.Command("du", "-s", "-k", path)
	out, err := cmd.Output()
	if err != nil {
		return 0
	}

	// Output is "size    path"
	parts := strings.Fields(string(out))
	if len(parts) > 0 {
		sizeK, _ := strconv.ParseInt(parts[0], 10, 64)
		return sizeK * 1024 // Bytes
	}
	return 0
}

func GetCategorySizes() (*CategorySizes, error) {
	home, err := os.UserHomeDir()
	if err != nil {
		return nil, err
	}

	// Paths to check
	docPath := filepath.Join(home, "Documents")
	downPath := filepath.Join(home, "Downloads")
	deskPath := filepath.Join(home, "Desktop")
	musicPath := filepath.Join(home, "Music")
	moviesPath := filepath.Join(home, "Movies")

	caches := filepath.Join(home, "Library", "Caches")
	logs := filepath.Join(home, "Library", "Logs")
	xcode := filepath.Join(home, "Library", "Developer", "Xcode", "DerivedData")

	trash := filepath.Join(home, ".Trash")

	apps := "/Applications"
	photos := filepath.Join(home, "Pictures")
	icloud := filepath.Join(home, "Library", "Mobile Documents")

	// Run in parallel
	type result struct {
		name string
		size int64
	}
	// Increased buffer for new categories
	c := make(chan result)
	totalChecks := 12

	check := func(name, p string) {
		c <- result{name, GetDirectorySize(p)}
	}

	go check("docs", docPath)
	go check("down", downPath)
	go check("desk", deskPath)
	go check("music", musicPath)
	go check("movies", moviesPath)

	go check("caches", caches)
	go check("logs", logs)
	go check("xcode", xcode)

	go check("trash", trash)
	go check("apps", apps)
	go check("photos", photos)
	go check("icloud", icloud)

	sizes := &CategorySizes{}

	// Get total disk usage to calculate System Data remainder using diskutil (APFS aware)
	cmd := exec.Command("diskutil", "info", "/")
	out, err := cmd.Output()
	var totalUsed int64

	if err == nil {
		lines := strings.Split(string(out), "\n")
		var containerTotal, containerFree int64

		for _, line := range lines {
			line = strings.TrimSpace(line)
			if strings.HasPrefix(line, "Container Total Space:") {
				if start := strings.Index(line, "("); start != -1 {
					if end := strings.Index(line[start:], " Bytes)"); end != -1 {
						bytesStr := line[start+1 : start+end]
						containerTotal, _ = strconv.ParseInt(bytesStr, 10, 64)
					}
				}
			}
			if strings.HasPrefix(line, "Container Free Space:") {
				if start := strings.Index(line, "("); start != -1 {
					if end := strings.Index(line[start:], " Bytes)"); end != -1 {
						bytesStr := line[start+1 : start+end]
						containerFree, _ = strconv.ParseInt(bytesStr, 10, 64)
					}
				}
			}
		}
		totalUsed = containerTotal - containerFree // In Bytes
	}

	var systemSpecific int64

	for i := 0; i < totalChecks; i++ {
		res := <-c
		switch res.name {
		case "docs":
			sizes.Documents = res.size
		case "down":
			sizes.Downloads = res.size
		case "desk":
			sizes.Desktop = res.size
		case "music":
			sizes.Music = res.size
		case "movies":
			sizes.Movies = res.size
		case "caches", "logs", "xcode":
			systemSpecific += res.size
		case "trash":
			sizes.Trash += res.size
		case "apps":
			sizes.Apps = res.size
		case "photos":
			sizes.Photos = res.size
		case "icloud":
			sizes.ICloud = res.size
		}
	}

	// Calculate System Data
	// Ideally: System = Total - (Docs + Down + Desk + Music + Movies + Apps + Photos + iCloud + Trash)
	known := sizes.Documents + sizes.Downloads + sizes.Desktop + sizes.Music + sizes.Movies + sizes.Trash + sizes.Apps + sizes.Photos + sizes.ICloud

	var remainder int64 = 0
	if totalUsed > known {
		remainder = totalUsed - known
	}

	if remainder > systemSpecific {
		sizes.System = remainder
	} else {
		sizes.System = systemSpecific
	}

	return sizes, nil
}

type CleaningEstimates struct {
	FlashEst int64 `json:"flash_est"`
	DeepEst  int64 `json:"deep_est"`
}

func GetCleaningEstimates() (*CleaningEstimates, error) {
	home, err := os.UserHomeDir()
	if err != nil {
		return nil, err
	}

	// Paths for Flash Clean: Caches, Logs, Trash, Xcode
	caches := filepath.Join(home, "Library", "Caches")
	logs := filepath.Join(home, "Library", "Logs")
	trash := filepath.Join(home, ".Trash")
	xcode := filepath.Join(home, "Library", "Developer", "Xcode", "DerivedData")

	// Paths for Deep Clean (proxy): Downloads
	downloads := filepath.Join(home, "Downloads")

	type result struct {
		name string
		size int64
	}
	c := make(chan result)
	totalChecks := 5

	check := func(name, p string) {
		c <- result{name, GetDirectorySize(p)}
	}

	go check("caches", caches)
	go check("logs", logs)
	go check("trash", trash)
	go check("xcode", xcode)
	go check("downloads", downloads)

	estimates := &CleaningEstimates{}

	for i := 0; i < totalChecks; i++ {
		res := <-c
		switch res.name {
		case "caches", "logs", "trash", "xcode":
			estimates.FlashEst += res.size
		case "downloads":
			estimates.DeepEst += res.size
		}
	}

	return estimates, nil
}
