package userbilling

import (
	"bytes"
	"context"
	crand "crypto/rand"
	"encoding/hex"
	"encoding/json"
	"errors"
	"io"
	"net/http"
	"os"
	"strconv"
	"strings"
	"time"
)

const (
	otelTraceIDZero      = "00000000000000000000000000000000"
	otelSpanIDZero       = "0000000000000000"
	otelSpanKindInternal = 1
	otelSpanKindServer   = 2
	otelExportTimeout    = 1500 * time.Millisecond
)

type otelTraceContext struct {
	TraceID      string
	ParentSpanID string
	ServerSpanID string
}

func newOtelTraceContext(traceparent string) otelTraceContext {
	traceID, parentSpanID, ok := parseOtelTraceparent(traceparent)
	if !ok {
		traceID = newOtelTraceID()
		parentSpanID = ""
	}
	return otelTraceContext{TraceID: traceID, ParentSpanID: parentSpanID, ServerSpanID: newOtelSpanID()}
}

func (ctx otelTraceContext) traceparent() string {
	if !validOtelHex(ctx.TraceID, 32, otelTraceIDZero) || !validOtelHex(ctx.ServerSpanID, 16, otelSpanIDZero) {
		return ""
	}
	return "00-" + ctx.TraceID + "-" + ctx.ServerSpanID + "-01"
}

func newOtelTraceID() string {
	return randomOtelHex(16, otelTraceIDZero)
}

func newOtelSpanID() string {
	return randomOtelHex(8, otelSpanIDZero)
}

func randomOtelHex(byteCount int, zero string) string {
	buf := make([]byte, byteCount)
	if _, err := crand.Read(buf); err != nil {
		return strings.TrimSuffix(zero, "0") + "1"
	}
	value := hex.EncodeToString(buf)
	if value == zero {
		return strings.TrimSuffix(zero, "0") + "1"
	}
	return value
}

func parseOtelTraceparent(value string) (string, string, bool) {
	parts := strings.Split(strings.ToLower(strings.TrimSpace(value)), "-")
	if len(parts) != 4 {
		return "", "", false
	}
	traceID := parts[1]
	spanID := parts[2]
	if !validOtelHex(traceID, 32, otelTraceIDZero) || !validOtelHex(spanID, 16, otelSpanIDZero) {
		return "", "", false
	}
	return traceID, spanID, true
}

func validOtelHex(value string, length int, zero string) bool {
	if len(value) != length || value == zero {
		return false
	}
	for _, ch := range value {
		if (ch < '0' || ch > '9') && (ch < 'a' || ch > 'f') {
			return false
		}
	}
	return true
}

func (s *Server) emitOtelSpanAsync(name string, trace otelTraceContext, spanID string, parentSpanID string, kind int, startedAt time.Time, attrs map[string]any, httpStatus int, errorCode string) {
	go s.emitOtelSpan(context.Background(), name, trace, spanID, parentSpanID, kind, startedAt, attrs, httpStatus, errorCode)
}

func (s *Server) emitOtelSpan(ctx context.Context, name string, trace otelTraceContext, spanID string, parentSpanID string, kind int, startedAt time.Time, attrs map[string]any, httpStatus int, errorCode string) {
	endpoint := resolveOtelTracesEndpoint()
	if endpoint == "" || !validOtelHex(trace.TraceID, 32, otelTraceIDZero) || !validOtelHex(spanID, 16, otelSpanIDZero) {
		return
	}
	endedAt := time.Now().UTC()
	if startedAt.IsZero() {
		startedAt = endedAt
	}
	attributes := map[string]any{
		"otel.trace_id": trace.TraceID,
	}
	for key, value := range attrs {
		if value != nil {
			attributes[key] = value
		}
	}
	if errorCode != "" {
		attributes["error.code"] = errorCode
	}
	statusCode := 1
	status := map[string]any{"code": statusCode}
	if errorCode != "" || httpStatus >= 500 {
		statusCode = 2
		status = map[string]any{"code": statusCode, "message": first(errorCode, "request_failed")}
	}
	span := map[string]any{
		"traceId":           trace.TraceID,
		"spanId":            spanID,
		"name":              name,
		"kind":              kind,
		"startTimeUnixNano": strconv.FormatInt(startedAt.UTC().UnixNano(), 10),
		"endTimeUnixNano":   strconv.FormatInt(endedAt.UnixNano(), 10),
		"attributes":        otelAttributes(attributes),
		"status":            status,
	}
	if validOtelHex(parentSpanID, 16, otelSpanIDZero) {
		span["parentSpanId"] = parentSpanID
	}
	body := map[string]any{
		"resourceSpans": []any{map[string]any{
			"resource": map[string]any{"attributes": otelAttributes(s.otelResourceAttributes())},
			"scopeSpans": []any{map[string]any{
				"scope": map[string]any{"name": "hwlab.user-billing", "version": "1"},
				"spans": []any{span},
			}},
		}},
	}
	payload, err := json.Marshal(body)
	if err != nil {
		return
	}
	requestCtx, cancel := context.WithTimeout(ctx, otelExportTimeout)
	defer cancel()
	request, err := http.NewRequestWithContext(requestCtx, http.MethodPost, endpoint, bytes.NewReader(payload))
	if err != nil {
		return
	}
	request.Header.Set("Content-Type", "application/json")
	client := http.Client{Timeout: otelExportTimeout}
	response, err := client.Do(request)
	if err != nil {
		return
	}
	_, _ = io.Copy(io.Discard, response.Body)
	_ = response.Body.Close()
}

func resolveOtelTracesEndpoint() string {
	if endpoint := first(os.Getenv("HWLAB_OTEL_EXPORTER_OTLP_TRACES_ENDPOINT"), os.Getenv("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT")); endpoint != "" {
		return strings.TrimRight(endpoint, "/")
	}
	if endpoint := first(os.Getenv("HWLAB_OTEL_EXPORTER_OTLP_ENDPOINT"), os.Getenv("OTEL_EXPORTER_OTLP_ENDPOINT")); endpoint != "" {
		return strings.TrimRight(endpoint, "/") + "/v1/traces"
	}
	return ""
}

func (s *Server) otelResourceAttributes() map[string]any {
	return map[string]any{
		"service.name":           first(os.Getenv("OTEL_SERVICE_NAME"), "hwlab-user-billing"),
		"deployment.environment": first(os.Getenv("HWLAB_ENVIRONMENT"), os.Getenv("HWLAB_RUNTIME_LANE"), "unknown"),
		"hwlab.lane":             first(os.Getenv("HWLAB_RUNTIME_LANE"), os.Getenv("HWLAB_GITOPS_PROFILE"), "unknown"),
		"k8s.namespace.name":     first(os.Getenv("POD_NAMESPACE"), os.Getenv("HWLAB_NAMESPACE"), s.config.RuntimeNamespace, "unknown"),
		"git.commit":             first(os.Getenv("HWLAB_COMMIT_ID"), os.Getenv("HWLAB_GITOPS_SOURCE_COMMIT"), os.Getenv("HWLAB_REVISION"), "unknown"),
	}
}

func otelAttributes(values map[string]any) []map[string]any {
	attrs := make([]map[string]any, 0, len(values))
	for key, value := range values {
		if key == "" || value == nil {
			continue
		}
		attrs = append(attrs, map[string]any{"key": key, "value": otelAnyValue(value)})
	}
	return attrs
}

func otelAnyValue(value any) map[string]any {
	switch typed := value.(type) {
	case bool:
		return map[string]any{"boolValue": typed}
	case int:
		return map[string]any{"intValue": strconv.FormatInt(int64(typed), 10)}
	case int64:
		return map[string]any{"intValue": strconv.FormatInt(typed, 10)}
	case float64:
		return map[string]any{"doubleValue": typed}
	case string:
		return map[string]any{"stringValue": typed}
	default:
		return map[string]any{"stringValue": first(toJSONString(typed), "{}")}
	}
}

func toJSONString(value any) string {
	payload, err := json.Marshal(value)
	if err != nil {
		return ""
	}
	return string(payload)
}

type sqlStateError interface {
	SQLState() string
}

func sqlErrorCode(err error) string {
	if err == nil {
		return ""
	}
	var stateErr sqlStateError
	if errors.As(err, &stateErr) {
		if code := strings.ToUpper(strings.TrimSpace(stateErr.SQLState())); validSQLState(code) {
			return code
		}
	}
	const marker = "SQLSTATE "
	message := strings.ToUpper(err.Error())
	index := strings.Index(message, marker)
	if index < 0 || len(message) < index+len(marker)+5 {
		return ""
	}
	code := strings.Trim(message[index+len(marker):index+len(marker)+5], "()[]{} ")
	if validSQLState(code) {
		return code
	}
	return ""
}

func validSQLState(code string) bool {
	if len(code) != 5 {
		return false
	}
	for _, ch := range code {
		if (ch < '0' || ch > '9') && (ch < 'A' || ch > 'Z') {
			return false
		}
	}
	return true
}
