package httpapi

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

	"github.com/dtoro/oikos/internal/adapters/postgres/sqlcgen"
	"github.com/dtoro/oikos/internal/core/domain"
	"github.com/dtoro/oikos/internal/httpapi/gen"
	"github.com/google/uuid"
	"github.com/jackc/pgx/v5"
)

const (
	defaultLimit = 50
	maxLimit     = 200
	// graphNodeCap bounds the whole-graph view. The cognition transactional
	// types (execution, task) are audit records, not topology, and previously
	// crowded out every host/lxc/service; the default whole-graph view below
	// excludes them so the cap is spent on the actual fleet graph. Operators
	// still reach executions/tasks via list_entities.
	graphNodeCap = 2000
)

// actorInfo returns the caller's (type, label) from the request context,
// falling back to operator/unknown when unset.
func actorInfo(ctx context.Context) (string, string) {
	if a := GetActor(ctx); a != nil {
		typ := a.Type
		if typ == "" {
			typ = "operator"
		}
		label := a.Label
		if label == "" {
			label = a.ID
		}
		return typ, label
	}
	return "operator", "unknown"
}

func clampLimit(l *int) int {
	if l == nil {
		return defaultLimit
	}
	if *l < 1 {
		return 1
	}
	if *l > maxLimit {
		return maxLimit
	}
	return *l
}

// resolveEntityID resolves a UUID-or-slug path/query value to the entity UUID.
func (s *Server) resolveEntityID(ctx context.Context, idOrSlug string) (uuid.UUID, error) {
	if id, err := uuid.Parse(idOrSlug); err == nil {
		if _, ok := s.entityCache.GetSlug(id.String()); ok {
			return id, nil
		}
		entity, err := sqlcgen.New(s.pool).GetEntityByID(ctx, id)
		if err != nil {
			return uuid.Nil, fmt.Errorf("%w: %s", domain.ErrNotFound, idOrSlug)
		}
		s.entityCache.Set(entity.Slug, entity.ID.String(), "")
		return entity.ID, nil
	}
	if cachedID, ok := s.entityCache.GetID(idOrSlug); ok {
		id, parseErr := uuid.Parse(cachedID)
		if parseErr == nil {
			return id, nil
		}
	}
	entity, err := sqlcgen.New(s.pool).GetEntityBySlug(ctx, idOrSlug)
	if err != nil {
		return uuid.Nil, fmt.Errorf("%w: %s", domain.ErrNotFound, idOrSlug)
	}
	s.entityCache.Set(entity.Slug, entity.ID.String(), "")
	return entity.ID, nil
}

// entityCols requires the entities table to be aliased as `e`, with
// entity_status left-joined and aliased as `st` (see withEntityStatus).
const entityCols = `e.id, e.slug, e.type, e.name, e.state, e.attributes,
                    e.maintenance_until, e.version, e.created_at, e.updated_at,
                    st.health, st.last_check_at`

func scanEntity(row pgx.Row) (gen.Entity, error) {
	var e gen.Entity
	var state *string
	var attrsJSON []byte
	var maint *time.Time
	var health *string
	var lastCheckAt *time.Time
	err := row.Scan(&e.Id, &e.Slug, &e.Type, &e.Name, &state, &attrsJSON,
		&maint, &e.Version, &e.CreatedAt, &e.UpdatedAt, &health, &lastCheckAt)
	if err != nil {
		return e, err
	}
	e.State = state
	e.MaintenanceUntil = maint
	if health != nil {
		h := gen.EntityHealth(*health)
		e.Health = &h
	}
	e.LastCheckAt = lastCheckAt
	var attrs map[string]any
	if len(attrsJSON) > 0 && json.Unmarshal(attrsJSON, &attrs) == nil && len(attrs) > 0 {
		e.Attributes = &attrs
	}
	return e, nil
}
