package cmd

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

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

const diagnoseEventPayloadLimitBytes = 20000
const diagnoseEventsLimit = 200

func fetchDiagnoseTopologySource(ctx *RuntimeContext, clusterRef clusterReference) (DiagnoseTopology, clinic.Request, int, error) {
	req, err := diagnoseTopologyRequest(clusterRef)
	if err != nil {
		return DiagnoseTopology{}, clinic.Request{}, 0, err
	}

	switch clusterRef.DeployType() {
	case "dedicated":
		var detail clusterDetailTopology
		if err := doClinicRequest(ctx, req, &detail); err != nil {
			return DiagnoseTopology{}, req, 0, err
		}
		payload, err := diagnoseDedicatedTopologyPayload(clusterRef, detail)
		if err != nil {
			return DiagnoseTopology{}, req, 0, err
		}
		return DiagnoseTopology{Payload: payload}, req, 1, nil
	case "premium", "byoc":
		var payload any
		if err := doClinicRequest(ctx, req, &payload); err != nil {
			return DiagnoseTopology{}, req, 0, err
		}
		return DiagnoseTopology{Payload: payload}, req, 1, nil
	default:
		deployType := clusterRef.DeployType()
		if deployType == "" {
			return DiagnoseTopology{}, req, 0, fmt.Errorf("cluster %s has no deploy type", clusterRef.ClusterID)
		}
		return DiagnoseTopology{}, req, 0, fmt.Errorf("cluster type %q has no topology", deployType)
	}
}

func diagnoseTopologyRequest(clusterRef clusterReference) (clinic.Request, error) {
	switch deployType := clusterRef.DeployType(); deployType {
	case "dedicated":
		return clinic.Request{
			Method: http.MethodGet,
			Path:   fmt.Sprintf("/clinic/api/v1/orgs/%s/clusters/%s", clusterRef.OrgID, clusterRef.ClusterID),
		}, nil
	case "premium", "byoc":
		if clusterRef.ParentID == "" || clusterRef.ParentID == "null" {
			return clinic.Request{}, fmt.Errorf("%s cluster has no parentID: %s", deployType, clusterRef.ClusterID)
		}
		return clinic.Request{
			Method: http.MethodGet,
			Path:   fmt.Sprintf("/clinic/api/v1/orgs/%s/clusters/%s/resource-pool/components", clusterRef.OrgID, clusterRef.ParentID),
		}, nil
	default:
		return clinic.Request{}, nil
	}
}

func diagnoseDedicatedTopologyPayload(clusterRef clusterReference, detail clusterDetailTopology) (any, error) {
	var raw json.RawMessage
	switch {
	case hasJSONPayload(detail.Components):
		raw = detail.Components
	case hasJSONPayload(detail.Topology):
		raw = detail.Topology
	default:
		return nil, fmt.Errorf("cluster %s has no components topology", clusterRef.ClusterID)
	}

	var payload any
	if err := json.Unmarshal(raw, &payload); err != nil {
		return nil, err
	}
	return payload, nil
}

func fetchDiagnoseEventsSource(ctx *RuntimeContext, clusterRef clusterReference, start, end time.Time, queryID string) ([]DiagnoseEvent, clinic.Request, int, error) {
	params := url.Values{}
	params.Set("begin_ts", strconv.FormatInt(start.UTC().Unix(), 10))
	params.Set("end_ts", strconv.FormatInt(end.UTC().Unix(), 10))
	params.Set("limit", strconv.Itoa(diagnoseEventsLimit))

	req := clinic.Request{
		Method: http.MethodGet,
		Path:   fmt.Sprintf("/clinic/api/v1/activityhub/applications/%s/targets/%s/activities", clusterRef.OrgID, clusterRef.ClusterID),
		Query:  params,
	}

	var response diagnoseEventsResponse
	if err := doClinicRequest(ctx, req, &response); err != nil {
		return nil, req, 0, err
	}

	events := make([]DiagnoseEvent, 0, len(response.Items))
	for _, item := range response.Items {
		events = append(events, normalizeDiagnoseEvent(item, queryID))
	}
	return events, req, len(events), nil
}

type diagnoseEventsResponse struct {
	Items []map[string]any `json:"items"`
}

func normalizeDiagnoseEvent(item map[string]any, queryID string) DiagnoseEvent {
	raw, truncated := marshalDiagnoseEventPayload(item)
	return DiagnoseEvent{
		ID:               firstStringField(item, "id", "activityID", "activityId"),
		Name:             firstStringField(item, "name", "activityName", "title"),
		Type:             firstStringField(item, "type", "activityType"),
		Severity:         firstIntField(item, "severity"),
		Timestamp:        firstStringField(item, "createdAt", "createTime", "timestamp", "time"),
		Message:          firstStringField(item, "message", "description", "detail"),
		RawPayload:       raw,
		PayloadTruncated: truncated,
		QueryIDs:         []string{queryID},
	}
}

func marshalDiagnoseEventPayload(item map[string]any) (string, bool) {
	raw, err := json.Marshal(item)
	if err != nil {
		return "", false
	}
	if len(raw) <= diagnoseEventPayloadLimitBytes {
		return string(raw), false
	}
	return string(raw[:diagnoseEventPayloadLimitBytes]), true
}

func firstStringField(values map[string]any, keys ...string) string {
	for _, key := range keys {
		value, ok := values[key]
		if !ok {
			continue
		}
		switch typed := value.(type) {
		case string:
			return typed
		case json.Number:
			return typed.String()
		case float64:
			return strconv.FormatFloat(typed, 'f', -1, 64)
		}
	}
	return ""
}

func firstIntField(values map[string]any, keys ...string) int {
	for _, key := range keys {
		value, ok := values[key]
		if !ok {
			continue
		}
		switch typed := value.(type) {
		case int:
			return typed
		case int64:
			return int(typed)
		case float64:
			return int(typed)
		case json.Number:
			parsed, err := typed.Int64()
			if err == nil {
				return int(parsed)
			}
		}
	}
	return 0
}

func diagnoseMissingSourceWarning(source, queryID string, err error) DiagnoseWarning {
	return DiagnoseWarning{
		Code:    "missing_source",
		Source:  source,
		QueryID: queryID,
		Message: err.Error(),
	}
}

func diagnoseQueryErrorWarning(source, queryID string, err error) DiagnoseWarning {
	return DiagnoseWarning{
		Code:    "query_error",
		Source:  source,
		QueryID: queryID,
		Message: err.Error(),
	}
}
