package trace

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"strconv"
	"strings"
	"time"
	"unicode/utf8"

	platformid "aigateway.local/core/internal/platform/id"
	"github.com/jackc/pgx/v5"
	"github.com/jackc/pgx/v5/pgxpool"
)

var ErrNotFound = errors.New("trace not found")

type Store struct{ pool *pgxpool.Pool }

func NewStore(pool *pgxpool.Pool) *Store { return &Store{pool: pool} }

type StartInput struct {
	RequestID      string
	APIKeyID       string
	TenantID       *string
	TraceType      string
	TargetID       string
	TargetCode     string
	ConversationID string
	Metadata       map[string]any
}

type Trace struct {
	ID             string          `json:"id"`
	RequestID      string          `json:"request_id"`
	APIKeyID       *string         `json:"api_key_id,omitempty"`
	TenantID       *string         `json:"tenant_id,omitempty"`
	TraceType      string          `json:"trace_type"`
	TargetID       *string         `json:"target_id,omitempty"`
	TargetCode     string          `json:"target_code"`
	ConversationID string          `json:"conversation_id"`
	Status         string          `json:"status"`
	StartedAt      time.Time       `json:"started_at"`
	FinishedAt     *time.Time      `json:"finished_at,omitempty"`
	LatencyMS      *int            `json:"latency_ms,omitempty"`
	RetrievalCount int             `json:"retrieval_count"`
	ModelCallCount int             `json:"model_call_count"`
	ToolCallCount  int             `json:"tool_call_count"`
	Error          string          `json:"error"`
	Metadata       json.RawMessage `json:"metadata"`
	Spans          []Span          `json:"spans,omitempty"`
}

type SpanInput struct {
	TraceID      string
	ParentID     string
	SpanType     string
	Name         string
	Round        int
	ProviderCode string
	Model        string
	Metadata     map[string]any
}

type Span struct {
	ID           string          `json:"id"`
	TraceID      string          `json:"trace_id"`
	ParentID     *string         `json:"parent_id,omitempty"`
	SpanType     string          `json:"span_type"`
	Name         string          `json:"name"`
	Status       string          `json:"status"`
	StartedAt    time.Time       `json:"started_at"`
	FinishedAt   *time.Time      `json:"finished_at,omitempty"`
	LatencyMS    *int            `json:"latency_ms,omitempty"`
	ProviderCode string          `json:"provider_code,omitempty"`
	Model        string          `json:"model,omitempty"`
	InputTokens  int64           `json:"input_tokens"`
	OutputTokens int64           `json:"output_tokens"`
	Round        int             `json:"round"`
	Error        string          `json:"error"`
	Metadata     json.RawMessage `json:"metadata"`
}

type FinishInput struct {
	Status         string
	Error          string
	RetrievalCount int
	ModelCallCount int
	ToolCallCount  int
	Metadata       map[string]any
}

type SpanFinishInput struct {
	Status       string
	Error        string
	InputTokens  int64
	OutputTokens int64
	ProviderCode string
	Model        string
	Metadata     map[string]any
}

type Filter struct {
	From       time.Time
	To         time.Time
	TraceType  string
	TargetCode string
	RequestID  string
	Status     string
	Limit      int
}

// Session is a metadata-only aggregation of traces that share a conversation
// ID. Stateless requests use a request-derived key so they remain visible in
// the session center without pretending to be part of a persistent chat.
type Session struct {
	ID             string    `json:"id"`
	TraceType      string    `json:"trace_type"`
	TargetCode     string    `json:"target_code"`
	TraceCount     int       `json:"trace_count"`
	LatestTraceID  string    `json:"latest_trace_id"`
	LatestStatus   string    `json:"latest_status"`
	StartedAt      time.Time `json:"started_at"`
	UpdatedAt      time.Time `json:"updated_at"`
	RetrievalCount int       `json:"retrieval_count"`
	ModelCallCount int       `json:"model_call_count"`
	ToolCallCount  int       `json:"tool_call_count"`
}

type SessionFilter struct {
	From       time.Time
	To         time.Time
	TraceType  string
	TargetCode string
	SessionID  string
	Limit      int
}

func metadataJSON(value map[string]any) []byte {
	if value == nil {
		return []byte(`{}`)
	}
	raw, err := json.Marshal(value)
	if err != nil {
		return []byte(`{}`)
	}
	return raw
}

func normalizeMetadata(raw []byte) json.RawMessage {
	if len(raw) == 0 || !json.Valid(raw) {
		return json.RawMessage(`{}`)
	}
	return raw
}

func validateStart(input StartInput) error {
	if strings.TrimSpace(input.RequestID) == "" || strings.TrimSpace(input.TargetCode) == "" {
		return errors.New("trace request_id 和 target_code 不能为空")
	}
	if input.TraceType != "application" && input.TraceType != "digital_employee" {
		return errors.New("trace 类型无效")
	}
	if len(input.TargetCode) > 128 || len(input.ConversationID) > 128 {
		return errors.New("trace 目标或会话 ID 过长")
	}
	return nil
}

func validateSpan(input SpanInput) error {
	if strings.TrimSpace(input.TraceID) == "" || strings.TrimSpace(input.Name) == "" {
		return errors.New("trace span 标识不能为空")
	}
	if input.SpanType != "model" && input.SpanType != "tool" && input.SpanType != "retrieval" {
		return errors.New("trace span 类型无效")
	}
	if input.Round < 0 || len(input.Name) > 256 {
		return errors.New("trace span 参数无效")
	}
	return nil
}

func (s *Store) Start(ctx context.Context, input StartInput) (Trace, error) {
	if s == nil || s.pool == nil {
		return Trace{}, errors.New("trace store unavailable")
	}
	input.RequestID = strings.TrimSpace(input.RequestID)
	input.TargetCode = strings.TrimSpace(input.TargetCode)
	input.ConversationID = strings.TrimSpace(input.ConversationID)
	if err := validateStart(input); err != nil {
		return Trace{}, err
	}
	id, err := platformid.NewUUID()
	if err != nil {
		return Trace{}, err
	}
	_, err = s.pool.Exec(ctx, `INSERT INTO gateway.agent_traces(id,request_id,api_key_id,tenant_id,trace_type,target_id,target_code,conversation_id,metadata) VALUES($1,$2,nullif($3,'')::uuid,nullif($4,'')::uuid,$5,nullif($6,'')::uuid,$7,$8,$9)`, id, input.RequestID, input.APIKeyID, valueOrEmpty(input.TenantID), input.TraceType, input.TargetID, input.TargetCode, input.ConversationID, metadataJSON(input.Metadata))
	if err != nil {
		return Trace{}, fmt.Errorf("start trace: %w", err)
	}
	return s.Get(ctx, id)
}

func valueOrEmpty(value *string) string {
	if value == nil {
		return ""
	}
	return *value
}

func (s *Store) StartSpan(ctx context.Context, input SpanInput) (Span, error) {
	if s == nil || s.pool == nil {
		return Span{}, errors.New("trace store unavailable")
	}
	input.Name = strings.TrimSpace(input.Name)
	if err := validateSpan(input); err != nil {
		return Span{}, err
	}
	id, err := platformid.NewUUID()
	if err != nil {
		return Span{}, err
	}
	_, err = s.pool.Exec(ctx, `INSERT INTO gateway.agent_trace_spans(id,trace_id,parent_id,span_type,name,round,provider_code,model,metadata) VALUES($1,$2,nullif($3,'')::uuid,$4,$5,$6,nullif($7,''),nullif($8,''),$9)`, id, input.TraceID, input.ParentID, input.SpanType, input.Name, input.Round, input.ProviderCode, input.Model, metadataJSON(input.Metadata))
	if err != nil {
		return Span{}, fmt.Errorf("start trace span: %w", err)
	}
	return s.GetSpan(ctx, id)
}

func (s *Store) Finish(ctx context.Context, id string, input FinishInput) error {
	if s == nil || s.pool == nil {
		return errors.New("trace store unavailable")
	}
	status := normalizeStatus(input.Status)
	errorText := truncate(input.Error, 4000)
	metadata := metadataJSON(input.Metadata)
	tag, err := s.pool.Exec(ctx, `UPDATE gateway.agent_traces SET status=$2,error=$3,retrieval_count=$4,model_call_count=$5,tool_call_count=$6,metadata=$7,finished_at=clock_timestamp(),latency_ms=(extract(epoch FROM (clock_timestamp()-started_at))*1000)::integer WHERE id=$1 AND status='running'`, id, status, errorText, max(input.RetrievalCount, 0), max(input.ModelCallCount, 0), max(input.ToolCallCount, 0), metadata)
	if err != nil {
		return err
	}
	if tag.RowsAffected() == 0 {
		return ErrNotFound
	}
	return nil
}

func (s *Store) FinishSpan(ctx context.Context, id string, input SpanFinishInput) error {
	if s == nil || s.pool == nil {
		return errors.New("trace store unavailable")
	}
	status := normalizeStatus(input.Status)
	tag, err := s.pool.Exec(ctx, `UPDATE gateway.agent_trace_spans SET status=$2,error=$3,input_tokens=$4,output_tokens=$5,provider_code=coalesce(nullif($6,''),provider_code),model=coalesce(nullif($7,''),model),metadata=$8,finished_at=clock_timestamp(),latency_ms=(extract(epoch FROM (clock_timestamp()-started_at))*1000)::integer WHERE id=$1 AND status='running'`, id, status, truncate(input.Error, 4000), max(input.InputTokens, 0), max(input.OutputTokens, 0), input.ProviderCode, input.Model, metadataJSON(input.Metadata))
	if err != nil {
		return err
	}
	if tag.RowsAffected() == 0 {
		return ErrNotFound
	}
	return nil
}

func normalizeStatus(value string) string {
	if value == "success" {
		return "success"
	}
	return "error"
}

func truncate(value string, limit int) string {
	value = strings.TrimSpace(value)
	if len(value) <= limit {
		return value
	}
	cut := value[:limit]
	// 按字节截断可能切半多字节 rune,产生无效 UTF-8 使 trace 落库失败;
	// 回退到最近一个完整 rune 的边界。
	for len(cut) > 0 && !utf8.RuneStart(cut[len(cut)-1]) {
		cut = cut[:len(cut)-1]
	}
	return cut
}

const traceSelect = `SELECT id::text,request_id,api_key_id::text,tenant_id::text,trace_type,target_id::text,target_code,conversation_id,status,started_at,finished_at,latency_ms,retrieval_count,model_call_count,tool_call_count,error,metadata FROM gateway.agent_traces`

func scanTrace(row pgx.Row) (Trace, error) {
	var item Trace
	err := row.Scan(&item.ID, &item.RequestID, &item.APIKeyID, &item.TenantID, &item.TraceType, &item.TargetID, &item.TargetCode, &item.ConversationID, &item.Status, &item.StartedAt, &item.FinishedAt, &item.LatencyMS, &item.RetrievalCount, &item.ModelCallCount, &item.ToolCallCount, &item.Error, &item.Metadata)
	if errors.Is(err, pgx.ErrNoRows) {
		return Trace{}, ErrNotFound
	}
	item.Metadata = normalizeMetadata(item.Metadata)
	return item, err
}

func scanSpan(row pgx.Row) (Span, error) {
	var item Span
	err := row.Scan(&item.ID, &item.TraceID, &item.ParentID, &item.SpanType, &item.Name, &item.Status, &item.StartedAt, &item.FinishedAt, &item.LatencyMS, &item.ProviderCode, &item.Model, &item.InputTokens, &item.OutputTokens, &item.Round, &item.Error, &item.Metadata)
	if errors.Is(err, pgx.ErrNoRows) {
		return Span{}, ErrNotFound
	}
	item.Metadata = normalizeMetadata(item.Metadata)
	return item, err
}

func (s *Store) Get(ctx context.Context, id string) (Trace, error) {
	if s == nil || s.pool == nil {
		return Trace{}, errors.New("trace store unavailable")
	}
	item, err := scanTrace(s.pool.QueryRow(ctx, traceSelect+` WHERE id=$1`, id))
	if err != nil {
		return Trace{}, err
	}
	spans, err := s.listSpans(ctx, id)
	if err != nil {
		return Trace{}, err
	}
	item.Spans = spans
	return item, nil
}

func (s *Store) GetSpan(ctx context.Context, id string) (Span, error) {
	if s == nil || s.pool == nil {
		return Span{}, errors.New("trace store unavailable")
	}
	return scanSpan(s.pool.QueryRow(ctx, `SELECT id::text,trace_id::text,parent_id::text,span_type,name,status,started_at,finished_at,latency_ms,coalesce(provider_code,''),coalesce(model,''),input_tokens,output_tokens,round,error,metadata FROM gateway.agent_trace_spans WHERE id=$1`, id))
}

func (s *Store) listSpans(ctx context.Context, traceID string) ([]Span, error) {
	if s == nil || s.pool == nil {
		return nil, errors.New("trace store unavailable")
	}
	rows, err := s.pool.Query(ctx, `SELECT id::text,trace_id::text,parent_id::text,span_type,name,status,started_at,finished_at,latency_ms,coalesce(provider_code,''),coalesce(model,''),input_tokens,output_tokens,round,error,metadata FROM gateway.agent_trace_spans WHERE trace_id=$1 ORDER BY started_at,id`, traceID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	items := []Span{}
	for rows.Next() {
		item, scanErr := scanSpan(rows)
		if scanErr != nil {
			return nil, scanErr
		}
		items = append(items, item)
	}
	return items, rows.Err()
}

func (s *Store) List(ctx context.Context, filter Filter) ([]Trace, error) {
	if s == nil || s.pool == nil {
		return nil, errors.New("trace store unavailable")
	}
	if filter.Limit < 1 || filter.Limit > 200 {
		filter.Limit = 50
	}
	where := []string{"started_at >= $1", "started_at < $2"}
	args := []any{filter.From, filter.To}
	add := func(condition string, value any) {
		args = append(args, value)
		where = append(where, fmt.Sprintf(condition, len(args)))
	}
	if filter.TraceType != "" {
		add("trace_type = $%d", filter.TraceType)
	}
	if filter.TargetCode != "" {
		add("target_code = $%d", filter.TargetCode)
	}
	if filter.RequestID != "" {
		add("request_id = $%d", filter.RequestID)
	}
	if filter.Status != "" {
		add("status = $%d", filter.Status)
	}
	args = append(args, filter.Limit)
	query := traceSelect + ` WHERE ` + strings.Join(where, " AND ") + ` ORDER BY started_at DESC,id DESC LIMIT $` + strconv.Itoa(len(args))
	rows, err := s.pool.Query(ctx, query, args...)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	items := []Trace{}
	for rows.Next() {
		item, scanErr := scanTrace(rows)
		if scanErr != nil {
			return nil, scanErr
		}
		items = append(items, item)
	}
	return items, rows.Err()
}

const sessionGroupSelect = `
WITH grouped AS (
	SELECT trace_type,
		target_code,
		coalesce(nullif(conversation_id, ''), 'request:' || request_id) AS session_key,
		count(*)::int AS trace_count,
		(array_agg(id::text ORDER BY started_at DESC, id DESC))[1] AS latest_trace_id,
		(array_agg(status ORDER BY started_at DESC, id DESC))[1] AS latest_status,
		min(started_at) AS started_at,
		max(coalesce(finished_at, started_at)) AS updated_at,
		sum(retrieval_count)::int AS retrieval_count,
		sum(model_call_count)::int AS model_call_count,
		sum(tool_call_count)::int AS tool_call_count
	FROM gateway.agent_traces`

func scanSession(row pgx.Row) (Session, error) {
	var item Session
	err := row.Scan(&item.ID, &item.TraceType, &item.TargetCode, &item.TraceCount, &item.LatestTraceID, &item.LatestStatus, &item.StartedAt, &item.UpdatedAt, &item.RetrievalCount, &item.ModelCallCount, &item.ToolCallCount)
	return item, err
}

func (s *Store) ListSessions(ctx context.Context, filter SessionFilter) ([]Session, error) {
	if s == nil || s.pool == nil {
		return nil, errors.New("trace store unavailable")
	}
	if filter.Limit < 1 || filter.Limit > 200 {
		filter.Limit = 50
	}
	innerWhere := []string{"started_at >= $1", "started_at < $2"}
	args := []any{filter.From, filter.To}
	addInner := func(condition string, value any) {
		args = append(args, value)
		innerWhere = append(innerWhere, fmt.Sprintf(condition, len(args)))
	}
	if filter.TraceType != "" {
		addInner("trace_type = $%d", filter.TraceType)
	}
	if filter.TargetCode != "" {
		addInner("target_code = $%d", filter.TargetCode)
	}
	outerWhere := []string{}
	if filter.SessionID != "" {
		args = append(args, filter.SessionID)
		outerWhere = append(outerWhere, fmt.Sprintf("trace_type || ':' || target_code || ':' || session_key = $%d", len(args)))
	}
	args = append(args, filter.Limit)
	limitArg := strconv.Itoa(len(args))
	query := sessionGroupSelect + ` WHERE ` + strings.Join(innerWhere, " AND ") + ` GROUP BY trace_type,target_code,session_key) SELECT trace_type || ':' || target_code || ':' || session_key AS id,trace_type,target_code,trace_count,latest_trace_id,latest_status,started_at,updated_at,retrieval_count,model_call_count,tool_call_count FROM grouped`
	if len(outerWhere) > 0 {
		query += ` WHERE ` + strings.Join(outerWhere, " AND ")
	}
	query += ` ORDER BY updated_at DESC,id DESC LIMIT $` + limitArg
	rows, err := s.pool.Query(ctx, query, args...)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	items := []Session{}
	for rows.Next() {
		item, scanErr := scanSession(rows)
		if scanErr != nil {
			return nil, scanErr
		}
		items = append(items, item)
	}
	return items, rows.Err()
}
