package formatter import ( "encoding/json" "fmt" "sort" "strings" "time" "git.db.org.ai/dborg/internal/models" "git.db.org.ai/dborg/internal/utils" ) func FormatBucketsResults(response *models.BucketsSearchResponse, asJSON bool) error { if asJSON { return utils.PrintJSON(response) } PrintSection("Bucket Search Results") if response.Credits.Unlimited { fmt.Printf("%s: %s\n", Dim("Credits"), Green("Unlimited")) } else { fmt.Printf("%s: %s\n", Dim("Credits Remaining"), FormatCredits(int64(response.Credits.Remaining))) } if response.Results == nil { PrintWarning("No results found") return nil } results, ok := response.Results.(map[string]any) if !ok { return fmt.Errorf("unexpected results format") } buckets, ok := results["buckets"].([]any) if !ok || len(buckets) == 0 { PrintWarning("No buckets found") return nil } fmt.Printf("%s: %s\n\n", Dim("Total Buckets"), Yellow(fmt.Sprintf("%d", len(buckets)))) bucketGroups := groupBucketsByType(buckets) for _, bType := range getOrderedTypes(bucketGroups) { formatBucketGroup(bType, bucketGroups[bType]) } printBucketStats(buckets) return nil } func groupBucketsByType(buckets []any) map[string][]map[string]any { groups := make(map[string][]map[string]any) for _, bucket := range buckets { if bucketMap, ok := bucket.(map[string]any); ok { bucketType := "unknown" if bt, ok := bucketMap["type"].(string); ok { bucketType = bt } groups[bucketType] = append(groups[bucketType], bucketMap) } } return groups } func getOrderedTypes(groups map[string][]map[string]any) []string { var types []string for t := range groups { types = append(types, t) } sort.Slice(types, func(i, j int) bool { order := map[string]int{"aws": 1, "gcp": 2, "azure": 3, "dos": 4, "unknown": 99} iOrder, iOk := order[types[i]] jOrder, jOk := order[types[j]] if !iOk { iOrder = 50 } if !jOk { jOrder = 50 } if iOrder != jOrder { return iOrder < jOrder } return len(groups[types[i]]) > len(groups[types[j]]) }) return types } func formatBucketGroup(bucketType string, buckets []map[string]any) { typeHeader := getBucketTypeHeader(bucketType) fmt.Printf("%s %s\n", typeHeader, Gray(fmt.Sprintf("(%d)", len(buckets)))) fmt.Println(Dim(strings.Repeat("─", 60))) sort.Slice(buckets, func(i, j int) bool { iCount := getFileCount(buckets[i]) jCount := getFileCount(buckets[j]) return iCount > jCount }) for i, bucket := range buckets { formatSingleBucket(bucket, i+1) } fmt.Println() } func formatSingleBucket(bucket map[string]any, index int) { bucketName := "unknown" if name, ok := bucket["bucket"].(string); ok { bucketName = name } fileCount := getFileCount(bucket) id := getID(bucket) fmt.Printf(" %s %s\n", Gray(fmt.Sprintf("%d.", index)), truncateBucketName(bucketName)) if fileCount > 0 { fileCountStr := formatFileCount(fileCount) fmt.Printf(" %s: %s", Cyan("Files"), fileCountStr) } else { fmt.Printf(" %s: %s", Cyan("Files"), Gray("Empty")) } if id > 0 { fmt.Printf(" %s %s", Dim("•"), Gray(fmt.Sprintf("ID: %d", id))) } fmt.Println() } func truncateBucketName(name string) string { maxLen := 50 if len(name) <= maxLen { return Bold(name) } parts := strings.Split(name, ".") if len(parts) > 2 { provider := parts[len(parts)-2] + "." + parts[len(parts)-1] remaining := maxLen - len(provider) - 4 if remaining > 0 { return Bold(TruncateString(strings.Join(parts[:len(parts)-2], "."), remaining) + "..." + provider) } } return Bold(TruncateString(name, maxLen)) } func formatFileCount(count int) string { switch { case count > 10000: return Red(fmt.Sprintf("%s", formatNumber(count))) case count > 1000: return Yellow(fmt.Sprintf("%s", formatNumber(count))) case count > 100: return Green(fmt.Sprintf("%s", formatNumber(count))) default: return Cyan(fmt.Sprintf("%d", count)) } } func formatNumber(n int) string { if n < 1000 { return fmt.Sprintf("%d", n) } str := fmt.Sprintf("%d", n) var result strings.Builder for i, r := range str { if i > 0 && (len(str)-i)%3 == 0 { result.WriteString(",") } result.WriteRune(r) } return result.String() } func getBucketTypeHeader(bucketType string) string { headers := map[string]string{ "aws": Bold(Yellow("☁ AWS S3 Buckets")), "gcp": Bold(Blue("☁ Google Cloud Storage")), "azure": Bold(Cyan("☁ Azure Storage")), "dos": Bold(Green("☁ DigitalOcean Spaces")), } if header, ok := headers[bucketType]; ok { return header } return Bold(Gray("☁ " + strings.Title(bucketType) + " Buckets")) } func getFileCount(bucket map[string]any) int { if count, ok := bucket["fileCount"].(float64); ok { return int(count) } return 0 } func getID(bucket map[string]any) int { if id, ok := bucket["id"].(float64); ok { return int(id) } return 0 } func printBucketStats(buckets []any) { totalFiles := 0 emptyBuckets := 0 largeBuckets := 0 typeCounts := make(map[string]int) for _, bucket := range buckets { if bucketMap, ok := bucket.(map[string]any); ok { fileCount := getFileCount(bucketMap) totalFiles += fileCount if fileCount == 0 { emptyBuckets++ } else if fileCount > 1000 { largeBuckets++ } if bucketType, ok := bucketMap["type"].(string); ok { typeCounts[bucketType]++ } } } fmt.Printf("%s\n", Bold("Summary Statistics")) fmt.Println(Dim(strings.Repeat("─", 60))) fmt.Printf(" %s: %s\n", Cyan("Total Files"), Yellow(formatNumber(totalFiles))) fmt.Printf(" %s: %s\n", Cyan("Average Files/Bucket"), Yellow(fmt.Sprintf("%.1f", float64(totalFiles)/float64(len(buckets))))) if emptyBuckets > 0 { fmt.Printf(" %s: %s\n", Cyan("Empty Buckets"), Gray(fmt.Sprintf("%d", emptyBuckets))) } if largeBuckets > 0 { fmt.Printf(" %s: %s\n", Cyan("Large Buckets (>1000 files)"), Red(fmt.Sprintf("%d", largeBuckets))) } fmt.Println() } func FormatBucketFilesResults(response *models.BucketsFilesSearchResponse, asJSON bool) error { if asJSON { return utils.PrintJSON(response) } PrintSection("Bucket Files Search Results") if response.Credits.Unlimited { fmt.Printf("%s: %s\n", Dim("Credits"), Green("Unlimited")) } else { fmt.Printf("%s: %s\n", Dim("Credits Remaining"), FormatCredits(int64(response.Credits.Remaining))) } if response.Results == nil { PrintWarning("No results found") return nil } results, ok := response.Results.(map[string]any) if !ok { resultsJSON, err := json.MarshalIndent(response.Results, "", " ") if err != nil { return fmt.Errorf("failed to format results: %w", err) } fmt.Println(string(resultsJSON)) return nil } files, ok := results["files"].([]any) if !ok || len(files) == 0 { PrintWarning("No files found") return nil } fmt.Printf("%s: %s\n\n", Dim("Total Files"), Yellow(fmt.Sprintf("%d", len(files)))) fileGroups := groupFilesByBucket(files) for bucket, bucketFiles := range fileGroups { formatFileGroup(bucket, bucketFiles) } return nil } func groupFilesByBucket(files []any) map[string][]map[string]any { groups := make(map[string][]map[string]any) for _, file := range files { if fileMap, ok := file.(map[string]any); ok { bucket := "unknown" if b, ok := fileMap["bucket"].(string); ok && b != "" { bucket = b } else if url, ok := fileMap["url"].(string); ok && url != "" { bucket = extractBucketFromURL(url) } groups[bucket] = append(groups[bucket], fileMap) } } return groups } func extractBucketFromURL(url string) string { url = strings.TrimPrefix(url, "https://") url = strings.TrimPrefix(url, "http://") parts := strings.Split(url, "/") if len(parts) > 0 { return parts[0] } return "unknown" } func formatFileGroup(bucket string, files []map[string]any) { fmt.Printf("%s %s\n", Bold(truncateBucketName(bucket)), Gray(fmt.Sprintf("(%d files)", len(files)))) fmt.Println(Dim(strings.Repeat("─", 60))) for i, file := range files { formatSingleFile(file, i+1) } fmt.Println() } func formatSingleFile(file map[string]any, index int) { fileName := "unknown" url := "" if name, ok := file["file"].(string); ok && name != "" { fileName = name } else if name, ok := file["filename"].(string); ok && name != "" { fileName = name } if u, ok := file["url"].(string); ok && u != "" { url = u if fileName == "unknown" { fileName = extractFileNameFromURL(u) } } fmt.Printf(" %s %s\n", Gray(fmt.Sprintf("%d.", index)), formatFileName(fileName)) if url != "" { fmt.Printf(" %s: %s\n", Cyan("URL"), Blue(url)) } if size, ok := file["size"].(float64); ok && size > 0 { fmt.Printf(" %s: %s\n", Cyan("Size"), formatFileSize(int64(size))) } if modified, ok := file["lastModified"].(float64); ok && modified > 0 { fmt.Printf(" %s: %s\n", Cyan("Modified"), formatLastModified(int64(modified))) } var metadata []string if bucketType, ok := file["type"].(string); ok && bucketType != "" { metadata = append(metadata, strings.ToUpper(bucketType)) } if container, ok := file["container"].(string); ok && container != "" { metadata = append(metadata, fmt.Sprintf("Container: %s", container)) } if bucketId, ok := file["bucketId"].(float64); ok && bucketId > 0 { metadata = append(metadata, fmt.Sprintf("Bucket ID: %d", int64(bucketId))) } if fileId, ok := file["id"].(string); ok && fileId != "" { metadata = append(metadata, fmt.Sprintf("File ID: %s", fileId)) } if len(metadata) > 0 { fmt.Printf(" %s: %s\n", Cyan("Info"), Gray(strings.Join(metadata, " • "))) } } func extractFileNameFromURL(url string) string { parts := strings.Split(url, "/") if len(parts) > 0 { fileName := parts[len(parts)-1] if qIndex := strings.Index(fileName, "?"); qIndex != -1 { fileName = fileName[:qIndex] } if hIndex := strings.Index(fileName, "#"); hIndex != -1 { fileName = fileName[:hIndex] } if fileName == "" { return "index" } return fileName } return "unknown" } func formatFileName(name string) string { if name == "" || name == "unknown" { return Gray("(unnamed file)") } name = decodeURLEncoding(name) ext := getFileExtension(name) color := getExtensionColor(ext) return Colorize(name, color) } func decodeURLEncoding(s string) string { decoded := strings.ReplaceAll(s, "%20", " ") decoded = strings.ReplaceAll(decoded, "%28", "(") decoded = strings.ReplaceAll(decoded, "%29", ")") decoded = strings.ReplaceAll(decoded, "_", " ") return decoded } func getFileExtension(filename string) string { parts := strings.Split(filename, ".") if len(parts) > 1 { return strings.ToLower(parts[len(parts)-1]) } return "" } func getExtensionColor(ext string) string { switch ext { case "pdf", "doc", "docx", "txt": return ColorBlue case "jpg", "jpeg", "png", "gif", "svg": return ColorGreen case "mp4", "avi", "mov", "mkv": return ColorMagenta case "zip", "tar", "gz", "rar": return ColorYellow case "sql", "db", "sqlite": return ColorRed case "json", "xml", "yaml", "yml": return ColorCyan default: return ColorWhite } } func formatFileSize(bytes int64) string { color := ColorWhite switch { case bytes > 1024*1024*100: color = ColorRed case bytes > 1024*1024*10: color = ColorYellow case bytes > 1024*1024: color = ColorGreen default: color = ColorCyan } return Colorize(FormatBytes(bytes), color) } func formatLastModified(timestamp int64) string { t := time.Unix(timestamp, 0) now := time.Now() duration := now.Sub(t) days := int(duration.Hours() / 24) var timeStr string var color string switch { case days == 0: timeStr = "Today" color = ColorGreen case days == 1: timeStr = "Yesterday" color = ColorGreen case days < 7: timeStr = fmt.Sprintf("%d days ago", days) color = ColorGreen case days < 30: weeks := days / 7 timeStr = fmt.Sprintf("%d weeks ago", weeks) color = ColorCyan case days < 365: months := days / 30 timeStr = fmt.Sprintf("%d months ago", months) color = ColorYellow default: years := days / 365 if years == 1 { timeStr = "1 year ago" } else { timeStr = fmt.Sprintf("%d years ago", years) } color = ColorRed } dateStr := t.Format("2006-01-02 15:04:05") return Colorize(timeStr, color) + Gray(fmt.Sprintf(" (%s)", dateStr)) }