// This file coordinates bounded concurrent O11Y acquisition across point-in-time cases.
package evaluation

import (
	"context"
	"fmt"
	"net/http"
	"path/filepath"
	"sort"
	"sync"
)

const DefaultO11YSuiteWorkers = 4

// O11YSuiteOptions configures case-level acquisition. Queries within each case
// remain sequential inside FetchO11Y.
type O11YSuiteOptions struct {
	Client             *http.Client
	BearerToken        string
	RequireBearerToken bool
	OutputDir          string
	CacheDir           string
	Workers            int
}

// O11YSuiteResult records one case outcome without stopping other cases.
type O11YSuiteResult struct {
	CaseID      string `json:"caseId"`
	ArchivePath string `json:"archivePath"`
	CacheHits   int    `json:"cacheHits"`
	QueryCount  int    `json:"queryCount"`
	Error       string `json:"error,omitempty"`
}

// FetchO11YSuite fetches cases with bounded case-level concurrency and returns
// every result in deterministic case ID order.
func FetchO11YSuite(ctx context.Context, specs []*O11YExportSpec, options O11YSuiteOptions) ([]O11YSuiteResult, error) {
	if len(specs) == 0 {
		return nil, fmt.Errorf("evaluation: O11Y suite specs are required")
	}
	if options.OutputDir == "" {
		return nil, fmt.Errorf("evaluation: O11Y suite output directory is required")
	}
	if options.Workers < 0 {
		return nil, fmt.Errorf("evaluation: O11Y suite workers cannot be negative")
	}
	workers := options.Workers
	if workers == 0 {
		workers = DefaultO11YSuiteWorkers
	}

	ordered := append([]*O11YExportSpec(nil), specs...)
	sort.Slice(ordered, func(i, j int) bool {
		return ordered[i].Case.ID < ordered[j].Case.ID
	})
	seen := make(map[string]struct{}, len(ordered))
	for i := range ordered {
		if err := validateO11YExportSpec(ordered[i]); err != nil {
			return nil, fmt.Errorf("evaluation: O11Y suite case %d: %w", i, err)
		}
		caseID := ordered[i].Case.ID
		if !isSafeQueryName(caseID) {
			return nil, fmt.Errorf("evaluation: O11Y suite case id %q is invalid", caseID)
		}
		if !isSafeQueryName(ordered[i].Case.Component) {
			return nil, fmt.Errorf("evaluation: O11Y suite component %q is invalid", ordered[i].Case.Component)
		}
		if _, duplicate := seen[caseID]; duplicate {
			return nil, fmt.Errorf("evaluation: O11Y suite case id %q is duplicated", caseID)
		}
		seen[caseID] = struct{}{}
	}
	if workers > len(ordered) {
		workers = len(ordered)
	}

	results := make([]O11YSuiteResult, len(ordered))
	jobs := make(chan int, len(ordered))
	for i := range ordered {
		jobs <- i
	}
	close(jobs)

	var waitGroup sync.WaitGroup
	waitGroup.Add(workers)
	for range workers {
		go func() {
			defer waitGroup.Done()
			for index := range jobs {
				spec := ordered[index]
				archivePath := filepath.Join(options.OutputDir, spec.Case.Component, PointInTimeCasesDirectory, spec.Case.ID+".json")
				_, archive, err := FetchO11Y(ctx, spec, O11YExportOptions{
					Client:             options.Client,
					BearerToken:        options.BearerToken,
					RequireBearerToken: options.RequireBearerToken,
					ArchivePath:        archivePath,
					CacheDir:           options.CacheDir,
				})

				result := O11YSuiteResult{CaseID: spec.Case.ID, ArchivePath: archivePath}
				if archive != nil {
					result.QueryCount = len(archive.Queries)
					for i := range archive.Queries {
						if archive.Queries[i].CacheHit {
							result.CacheHits++
						}
					}
				}
				if err != nil {
					result.Error = err.Error()
				}
				results[index] = result
			}
		}()
	}
	waitGroup.Wait()

	failureCount := 0
	for i := range results {
		if results[i].Error != "" {
			failureCount++
		}
	}
	if failureCount > 0 {
		return results, fmt.Errorf("evaluation: O11Y suite failed for %d/%d cases", failureCount, len(results))
	}
	return results, nil
}
