package store

import (
	"context"
	"encoding/json"
	"fmt"
	"time"
)

// ecosystemTraceRetention is how many trace rows are kept. Traces are
// diagnostics with a short useful life, and they arrive at machine rate, so
// the table is bounded rather than append-only. The facts table is the audit
// trail; this one is not.
const ecosystemTraceRetention = 5000

// EcosystemTrace is one hop of a cross-service call: which service, which
// operation, how it ended, how long it took, and the ids that stitch the hops
// of one turn together. Fields carries the hop-specific detail (entity id,
// capability, failure class) as a JSON object.
type EcosystemTrace struct {
	ID            int64          `json:"id"`
	Ts            time.Time      `json:"ts"`
	Service       string         `json:"service"`
	Operation     string         `json:"operation"`
	Status        string         `json:"status"`
	DurationMs    int64          `json:"duration_ms"`
	CorrelationID string         `json:"correlation_id"`
	CausationID   string         `json:"causation_id"`
	HTTPStatus    int            `json:"http_status"`
	Fields        map[string]any `json:"fields"`
}

// WriteEcosystemTrace appends one trace row and keeps the table bounded.
func (s *Store) WriteEcosystemTrace(ctx context.Context, tr EcosystemTrace) (int64, error) {
	fields := "{}"
	if len(tr.Fields) > 0 {
		b, err := json.Marshal(tr.Fields)
		if err != nil {
			return 0, fmt.Errorf("marshal trace fields: %w", err)
		}
		fields = string(b)
	}
	res, err := s.db.ExecContext(ctx, `
		INSERT INTO ecosystem_traces
		       (ts, service, operation, status, duration_ms, correlation_id, causation_id, http_status, fields)
		VALUES (?,?,?,?,?,?,?,?,?)`,
		tr.Ts.UnixMilli(), tr.Service, tr.Operation, tr.Status, tr.DurationMs,
		tr.CorrelationID, tr.CausationID, tr.HTTPStatus, fields)
	if err != nil {
		return 0, fmt.Errorf("write ecosystem trace: %w", err)
	}
	id, err := res.LastInsertId()
	if err != nil {
		return 0, fmt.Errorf("last insert id: %w", err)
	}
	// Prune rarely: the cost of the delete is not worth paying on every hop,
	// and the bound is a ceiling, not an exact size.
	if id%256 == 0 {
		if err := s.PruneEcosystemTraces(ctx, ecosystemTraceRetention); err != nil {
			return id, err
		}
	}
	return id, nil
}

// PruneEcosystemTraces drops all but the newest keep rows.
func (s *Store) PruneEcosystemTraces(ctx context.Context, keep int) error {
	if keep <= 0 {
		return nil
	}
	_, err := s.db.ExecContext(ctx, `
		DELETE FROM ecosystem_traces
		 WHERE id <= (SELECT MAX(id) FROM ecosystem_traces) - ?`, keep)
	if err != nil {
		return fmt.Errorf("prune ecosystem traces: %w", err)
	}
	return nil
}

// RecentEcosystemTraces returns the newest n traces, newest first.
func (s *Store) RecentEcosystemTraces(ctx context.Context, n int) ([]EcosystemTrace, error) {
	rows, err := s.db.QueryContext(ctx, `
		SELECT id, ts, service, operation, status, duration_ms, correlation_id, causation_id, http_status, fields
		  FROM ecosystem_traces
		 ORDER BY id DESC
		 LIMIT ?`, n)
	if err != nil {
		return nil, fmt.Errorf("recent ecosystem traces: %w", err)
	}
	defer rows.Close()
	var out []EcosystemTrace
	for rows.Next() {
		var tr EcosystemTrace
		var tsMilli int64
		var fields string
		if err := rows.Scan(&tr.ID, &tsMilli, &tr.Service, &tr.Operation, &tr.Status,
			&tr.DurationMs, &tr.CorrelationID, &tr.CausationID, &tr.HTTPStatus, &fields); err != nil {
			return nil, err
		}
		tr.Ts = time.UnixMilli(tsMilli).UTC()
		if fields != "" {
			_ = json.Unmarshal([]byte(fields), &tr.Fields)
		}
		out = append(out, tr)
	}
	return out, rows.Err()
}
