package cmd

import (
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
	"strconv"
	"strings"
	"time"

	"github.com/tidbcloud/clinic-cli/internal/clinic"
)

const diagnoseBundleSchemaVersion = "diagnose.v2"

func runDiagnoseBundle(ctx *RuntimeContext, opts diagnoseOptions, now time.Time) (DiagnoseBundle, error) {
	clusterID, err := requiredFlagOrEnvWithContext(ctx, "cluster-id", opts.ClusterID, envClinicClusterID)
	if err != nil {
		return DiagnoseBundle{}, err
	}

	start, end, err := resolveDiagnoseTimeScope(ctx, opts, now)
	if err != nil {
		return DiagnoseBundle{}, err
	}

	clusterRef, err := resolveClusterReferenceByIDWithMetadata(ctx, clusterID)
	if err != nil {
		return DiagnoseBundle{}, err
	}
	namespace, err := inferLokiNamespace(clusterRef)
	if err != nil {
		return DiagnoseBundle{}, err
	}
	selector := fmt.Sprintf(`{cluster_env=%q, namespace=%q}`, diagnoseBundleEnv(ctx), namespace)
	seedQuery := selector + " |= " + strconv.Quote(opts.Seed)

	plan := newDiagnoseQueryPlanBuilder()
	seedQueryRecord := plan.newLokiSeedRecord(seedQuery, start.UTC(), end.UTC(), opts.Limit)
	req := diagnoseLokiQueryRangeRequest(clusterID, seedQueryRecord)

	queryStarted := time.Now()
	body, seedQueryErr := executeLokiQueryRangeRawWithRetry(ctx, req)
	resultCount := 0
	if seedQueryErr == nil {
		resultCount, seedQueryErr = countDiagnoseLokiResults(body)
	}
	var seedMatches []DiagnoseLogEntry
	var seedWarnings []DiagnoseWarning
	if seedQueryErr == nil {
		seedMatches, seedWarnings, seedQueryErr = normalizeDiagnoseLogEntries(body, opts.Seed, []string{seedQueryRecord.ID}, time.Local)
	}
	completeDiagnoseQueryRecord(&seedQueryRecord, queryStarted, resultCount, seedQueryErr)

	queryPlan := []DiagnoseQueryRecord{seedQueryRecord}
	warnings := seedWarnings
	if seedQueryErr != nil {
		warnings = append(warnings, diagnoseQueryErrorWarning("seed_exact_match", seedQueryRecord.ID, seedQueryErr))
		seedMatches = []DiagnoseLogEntry{}
	}

	topologyRecord := plan.newClinicAPIRecord("topology", "", start.UTC(), end.UTC(), 0)
	topologyStarted := time.Now()
	topology, topologyReq, topologyCount, topologyErr := fetchDiagnoseTopologySource(ctx, clusterRef)
	topologyRecord.Query = requestPathWithQuery(topologyReq)
	completeDiagnoseQueryRecord(&topologyRecord, topologyStarted, topologyCount, topologyErr)
	queryPlan = append(queryPlan, topologyRecord)
	if topologyErr != nil {
		warnings = append(warnings, diagnoseMissingSourceWarning("topology", topologyRecord.ID, topologyErr))
	} else {
		topology.QueryIDs = []string{topologyRecord.ID}
	}

	eventsRecord := plan.newClinicAPIRecord("events", "", start.UTC(), end.UTC(), diagnoseEventsLimit)
	eventsStarted := time.Now()
	events, eventsReq, eventCount, eventsErr := fetchDiagnoseEventsSource(ctx, clusterRef, start, end, eventsRecord.ID)
	eventsRecord.Query = requestPathWithQuery(eventsReq)
	completeDiagnoseQueryRecord(&eventsRecord, eventsStarted, eventCount, eventsErr)
	queryPlan = append(queryPlan, eventsRecord)
	if eventsErr != nil {
		warnings = append(warnings, diagnoseMissingSourceWarning("events", eventsRecord.ID, eventsErr))
		events = []DiagnoseEvent{}
	}

	expansion := expandDiagnoseRelatedLogs(ctx, clusterID, selector, seedMatches, opts, plan, time.Local)
	queryPlan = append(queryPlan, expansion.QueryPlan...)
	seedMatches = expansion.Seeds
	warnings = append(warnings, expansion.Warnings...)
	scopeRecord := buildDiagnoseBundleScope(ctx, opts, clusterRef, namespace, start, end)
	groups, timelines, summaries, groupingWarnings := buildDiagnoseGroupsTimelinesAndSummaries(
		scopeRecord,
		queryPlan,
		seedMatches,
		expansion.Related,
		events,
		time.Local,
	)
	warnings = append(warnings, groupingWarnings...)

	return DiagnoseBundle{
		SchemaVersion:  diagnoseBundleSchemaVersion,
		GeneratedAtUTC: formatDiagnoseBundleTimeUTC(now),
		Scope:          scopeRecord,
		QueryPlan:      queryPlan,
		SeedMatches:    seedMatches,
		RelatedLogs:    expansion.Related,
		Events:         events,
		Topology:       topology,
		Groups:         groups,
		Timelines:      timelines,
		Summaries:      summaries,
		Warnings:       warnings,
	}, seedQueryErr
}

func diagnoseLokiQueryRangeRequest(clusterID string, record DiagnoseQueryRecord) clinic.Request {
	req := clinic.Request{
		Method: http.MethodGet,
		Path:   fmt.Sprintf("/data-proxy/loki/%s/api/v1/query_range", clusterID),
		Query:  url.Values{},
	}
	req.Query.Set("query", record.Query)
	req.Query.Set("start", strconv.FormatInt(mustParseDiagnoseBundleUTC(record.StartUTC).UnixNano(), 10))
	req.Query.Set("end", strconv.FormatInt(mustParseDiagnoseBundleUTC(record.EndUTC).UnixNano(), 10))
	req.Query.Set("limit", strconv.Itoa(record.Limit))
	req.Query.Set("direction", record.Direction)
	return req
}

func buildDiagnoseBundleScope(
	ctx *RuntimeContext,
	opts diagnoseOptions,
	clusterRef clusterReference,
	namespace string,
	start time.Time,
	end time.Time,
) DiagnoseBundleScope {
	location := time.Local
	return DiagnoseBundleScope{
		Env:                     diagnoseBundleEnv(ctx),
		SelectedProfile:         diagnoseSelectedProfileName(ctx),
		ProfileActive:           diagnoseSelectedProfileName(ctx) != "",
		OrgID:                   clusterRef.OrgID,
		ClusterID:               clusterRef.ClusterID,
		ClusterName:             clusterRef.ClusterName,
		DeployType:              clusterRef.DeployType(),
		Namespace:               namespace,
		SharedpoolID:            diagnoseSharedpoolID(clusterRef),
		Timezone:                diagnoseLocationName(location),
		StartUTC:                formatDiagnoseBundleTimeUTC(start),
		EndUTC:                  formatDiagnoseBundleTimeUTC(end),
		StartLocal:              formatDiagnoseBundleTimeLocal(start, location),
		EndLocal:                formatDiagnoseBundleTimeLocal(end, location),
		Seed:                    opts.Seed,
		SeedSource:              "argument",
		ClusterSource:           diagnoseClusterSource(ctx, opts),
		TimeSource:              diagnoseTimeSource(ctx, opts),
		TimeInterpretationNotes: "local timezone is used because profiles do not yet store timezone",
	}
}

func countDiagnoseLokiResults(body []byte) (int, error) {
	var response lokiQueryRangeResponse
	if err := json.Unmarshal(body, &response); err != nil {
		return 0, fmt.Errorf("decode loki response: %w", err)
	}
	if response.Data.ResultType != "" && response.Data.ResultType != "streams" {
		return 0, fmt.Errorf("unexpected loki result type: %s", response.Data.ResultType)
	}

	count := 0
	for _, stream := range response.Data.Result {
		for _, value := range stream.Values {
			if len(value) >= 2 {
				count++
			}
		}
	}
	return count, nil
}

func diagnoseBundleEnv(ctx *RuntimeContext) string {
	if ctx == nil {
		return "prod"
	}
	if ctx.SelectedEnv != "" {
		return ctx.SelectedEnv
	}
	if ctx.Config != nil && ctx.Config.ActiveEnv != "" {
		return ctx.Config.ActiveEnv
	}
	return "prod"
}

func diagnoseSelectedProfileName(ctx *RuntimeContext) string {
	if ctx == nil {
		return ""
	}
	return ctx.SelectedProfileName
}

func diagnoseClusterSource(ctx *RuntimeContext, opts diagnoseOptions) string {
	switch {
	case strings.TrimSpace(opts.ClusterID) != "":
		return "explicit_flag"
	case rawEnvValue(envClinicClusterID) != "":
		return "environment_variable"
	case ctx != nil && ctx.SelectedProfile != nil && ctx.SelectedProfile.ClusterID != "":
		return "active_profile"
	default:
		return "unresolved"
	}
}

func diagnoseTimeSource(ctx *RuntimeContext, opts diagnoseOptions) string {
	switch {
	case opts.Since != "":
		return "since_flag"
	case opts.Start != "" || opts.End != "":
		return "start_end_flags"
	case opts.Around != "" || opts.Window != "":
		return "around_window_flags"
	case rawEnvValue(envClinicRangeStart) != "" || rawEnvValue(envClinicRangeEnd) != "":
		return "start_end_flags"
	case ctx != nil && ctx.SelectedProfile != nil && ctx.SelectedProfile.RangeStart != "" && ctx.SelectedProfile.RangeEnd != "":
		return "profile_range"
	default:
		return "default"
	}
}

func diagnoseSharedpoolID(clusterRef clusterReference) string {
	if clusterRef.ParentID == "null" {
		return ""
	}
	return clusterRef.ParentID
}

func diagnoseLocationName(location *time.Location) string {
	if location == nil {
		return "UTC"
	}
	name := location.String()
	if name == "" || name == "Local" {
		return time.Now().In(location).Format("MST")
	}
	return name
}

func formatDiagnoseBundleTimeUTC(value time.Time) string {
	return value.UTC().Format(time.RFC3339)
}

func formatDiagnoseBundleTimeLocal(value time.Time, location *time.Location) string {
	return value.In(location).Format(time.RFC3339)
}

func mustParseDiagnoseBundleUTC(value string) time.Time {
	parsed, err := time.Parse(time.RFC3339, value)
	if err != nil {
		panic(err)
	}
	return parsed.UTC()
}
