// This file reconstructs point-in-time cases from committed raw O11Y responses.
package evaluation

import (
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"strings"
	"time"
)

// DecodeO11YArchive strictly decodes one committed raw-response archive.
func DecodeO11YArchive(r io.Reader) (*O11YArchive, error) {
	decoder := json.NewDecoder(r)
	decoder.DisallowUnknownFields()

	var archive O11YArchive
	if err := decoder.Decode(&archive); err != nil {
		return nil, fmt.Errorf("evaluation: decode O11Y archive: %w", err)
	}

	var extra any
	if err := decoder.Decode(&extra); !errors.Is(err, io.EOF) {
		if err == nil {
			return nil, fmt.Errorf("evaluation: decode O11Y archive: multiple JSON values")
		}
		return nil, fmt.Errorf("evaluation: decode O11Y archive trailing data: %w", err)
	}

	return &archive, nil
}

// ReplayO11YArchive derives a CaseBundle from committed raw responses without
// contacting O11Y or reading the request cache.
func ReplayO11YArchive(spec *O11YExportSpec, archive *O11YArchive) (*CaseBundle, error) {
	if err := validateO11YExportSpec(spec); err != nil {
		return nil, err
	}
	if err := validateO11YArchiveIdentity(spec, archive); err != nil {
		return nil, err
	}

	metrics := make([]MetricSeries, 0)
	quality := DataQuality{MissingSeries: make([]string, 0)}
	queryErrors := make([]string, 0)
	queryIndex := 0

	for i := range spec.Sources {
		source := &spec.Sources[i]
		for j := range source.Queries {
			query := &source.Queries[j]
			record := &archive.Queries[queryIndex]
			queryIndex++

			if err := validateO11YArchivedQuery(source, query, &spec.Window, spec.Case.DecisionAt, record); err != nil {
				return nil, err
			}

			response, err := decodePrometheusResponse(record.Response)
			if err != nil {
				queryErrors = append(queryErrors, fmt.Sprintf("%s: %v", query.Name, err))
				continue
			}
			if response.IsPartial {
				quality.IsPartial = true
				continue
			}

			series, _, err := normalizePrometheusResponse(response, query, &spec.Window, spec.Case.DecisionAt)
			if err != nil {
				queryErrors = append(queryErrors, fmt.Sprintf("%s: %v", query.Name, err))
				continue
			}
			maxGap := time.Duration(query.MaxGapSeconds) * time.Second
			if len(series) > 0 && maxGap > 0 {
				if err := validateMetricGaps(series, maxGap); err != nil {
					quality.GappedSeries = append(quality.GappedSeries, query.Name)
					continue
				}
			}
			if query.Required && len(series) == 0 {
				quality.MissingSeries = append(quality.MissingSeries, query.Name)
				continue
			}
			if query.Required && hasInstanceLabel(query) {
				live := countInstances(series)
				if maxGap > 0 {
					live = countLiveInstances(series, spec.Case.DecisionAt, maxGap)
				}
				if live < int(spec.Case.Current.Replicas) {
					quality.IncompleteSeries = append(quality.IncompleteSeries, query.Name)
					continue
				}
			}
			if hasInstanceLabel(query) && maxGap > 0 {
				series = liveMetricSeries(series, spec.Case.DecisionAt, maxGap)
			}
			metrics = append(metrics, series...)
		}
	}

	if quality.IsPartial {
		return nil, fmt.Errorf("evaluation: replay O11Y archive: partial Prometheus response")
	}
	if len(quality.MissingSeries) > 0 {
		return nil, fmt.Errorf("evaluation: replay O11Y archive: missing required series: %v", quality.MissingSeries)
	}
	if len(quality.GappedSeries) > 0 {
		return nil, fmt.Errorf("evaluation: replay O11Y archive: gapped required series: %v", quality.GappedSeries)
	}
	if len(quality.IncompleteSeries) > 0 {
		return nil, fmt.Errorf("evaluation: replay O11Y archive: incomplete required series: %v", quality.IncompleteSeries)
	}
	if len(queryErrors) > 0 {
		return nil, fmt.Errorf("evaluation: replay O11Y archive: query errors: %s", strings.Join(queryErrors, "; "))
	}

	sortMetricSeries(metrics)
	bundle := &CaseBundle{
		Version: CaseBundleVersionV1Alpha1,
		ID:      spec.Case.ID,
		Source:  DataSourceO11Y,
		Input: CaseInput{
			DecisionAt: spec.Case.DecisionAt,
			Component:  spec.Case.Component,
			ClusterID:  spec.Target.ClusterID,
			Current:    spec.Case.Current,
			Metrics:    metrics,
		},
		Baseline: spec.Case.Baseline,
		Oracle:   spec.Case.Oracle,
		Quality:  quality,
	}
	if err := validateCase(bundle); err != nil {
		return nil, err
	}

	return bundle, nil
}

func validateO11YArchiveIdentity(spec *O11YExportSpec, archive *O11YArchive) error {
	if archive == nil {
		return fmt.Errorf("evaluation: O11Y archive is required")
	}
	if archive.Version != O11YExportVersionV1Alpha1 {
		return fmt.Errorf("evaluation: O11Y archive version %q is unsupported", archive.Version)
	}
	if archive.CaseID != spec.Case.ID {
		return fmt.Errorf("evaluation: O11Y archive case id %q, want %q", archive.CaseID, spec.Case.ID)
	}
	if archive.Component != spec.Case.Component {
		return fmt.Errorf("evaluation: O11Y archive component %q, want %q", archive.Component, spec.Case.Component)
	}
	if archive.Target != spec.Target {
		return fmt.Errorf("evaluation: O11Y archive target does not match case %q", spec.Case.ID)
	}
	if !archive.Window.Start.Equal(spec.Window.Start) || !archive.Window.End.Equal(spec.Window.End) || archive.Window.StepSeconds != spec.Window.StepSeconds {
		return fmt.Errorf("evaluation: O11Y archive window does not match case %q", spec.Case.ID)
	}
	if got, want := len(archive.Queries), queryCount(spec.Sources); got != want {
		return fmt.Errorf("evaluation: O11Y archive queries = %d, want %d", got, want)
	}

	return nil
}

func validateO11YArchivedQuery(source *O11YSourceSpec, query *O11YQuerySpec, window *O11YQueryWindow, decisionAt time.Time, record *O11YArchivedQuery) error {
	if record.Name != query.Name || record.Source != source.Type || record.Type != query.Type || record.PromQL != query.PromQL {
		return fmt.Errorf("evaluation: O11Y archive query %q does not match the metrics profile", record.Name)
	}
	requestURL, err := buildO11YQueryURL(source, query, window, decisionAt)
	if err != nil {
		return fmt.Errorf("evaluation: build archived query %q: %w", query.Name, err)
	}
	if want := o11yRequestDigestWithHeaders(requestURL, source.Headers); record.RequestDigest != want {
		return fmt.Errorf("evaluation: O11Y archive query %q request digest does not match", query.Name)
	}
	if len(record.Response) == 0 {
		return fmt.Errorf("evaluation: O11Y archive query %q response is required", query.Name)
	}

	return nil
}

func liveMetricSeries(series []MetricSeries, decisionAt time.Time, maxAge time.Duration) []MetricSeries {
	live := make([]MetricSeries, 0, len(series))
	for i := range series {
		samples := series[i].Samples
		if decisionAt.Sub(samples[len(samples)-1].Timestamp) <= maxAge {
			live = append(live, series[i])
		}
	}

	return live
}
