package httpapi

import (
	"context"
	"encoding/json"
	"fmt"
	"strings"
	"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/dtoro/oikos/internal/observability"
	"github.com/google/uuid"
	"github.com/jackc/pgx/v5"
)

// ─── Executions ────────────────────────────────────────────────────────

// ListExecutions returns executions newest-first.
//
// The target/action/correlation_id filters are declared in the OpenAPI spec and
// generated into the request struct, but were never bound — so
// `GET /executions?target=<id>` silently returned the first page of the whole
// fleet. Ordering was by target slug, which is neither useful for a history
// view nor unique enough to paginate on: several executions share a target, so
// a slug cursor could skip or repeat rows.
func (s *Server) ListExecutions(ctx context.Context, req gen.ListExecutionsRequestObject) (gen.ListExecutionsResponseObject, error) {
	limit := clampLimit(req.Params.Limit)

	cursorTime, cursorID, err := parseExecutionCursor(req.Params.Cursor)
	if err != nil {
		return nil, err
	}

	rows, err := s.pool.Query(ctx, `
		SELECT e.entity_id, e.classification_id::text, e.signal_entity_id::text,
		       e.target_entity_id, e.action, e.risk_class,
		       e.approval_id::text, e.agent_id::text, e.skill_id::text,
		       e.skill_version, e.status, e.result, e.duration_ms,
		       e.verified, e.correlation_id, e.started_at, e.completed_at, e.created_at,
		       te.slug
		FROM executions e
		JOIN entities te ON te.id = e.target_entity_id
		WHERE ($1::text IS NULL OR e.status = $1)
		  -- target accepts a slug or a uuid: the SPA passes an entity id,
		  -- while a human poking the API reaches for the slug.
		  AND ($2::text IS NULL OR te.slug = $2 OR e.target_entity_id::text = $2)
		  -- the run tool encodes action as "run:{json}", so match the verb too
		  AND ($3::text IS NULL OR e.action = $3 OR split_part(e.action, ':', 1) = $3)
		  AND ($4::text IS NULL OR e.correlation_id = $4)
		  AND ($5::timestamptz IS NULL
		       OR (e.created_at, e.entity_id) < ($5::timestamptz, $6::uuid))
		ORDER BY e.created_at DESC, e.entity_id DESC
		LIMIT $7`,
		req.Params.Status, req.Params.Target, req.Params.Action, req.Params.CorrelationId,
		cursorTime, cursorID, limit+1)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	items := []gen.Execution{}
	for rows.Next() {
		var exec gen.Execution
		var resultBytes []byte
		var targetSlug string
		if err := rows.Scan(&exec.Id, &exec.ClassificationId, &exec.SignalId,
			&exec.Target, &exec.Action, &exec.RiskClass,
			&exec.ApprovalId, &exec.AgentId, &exec.SkillId,
			&exec.SkillVersion, &exec.Status, &resultBytes, &exec.DurationMs,
			&exec.Verified, &exec.CorrelationId, &exec.StartedAt, &exec.CompletedAt,
			&exec.CreatedAt, &targetSlug); err != nil {
			return nil, err
		}
		var result map[string]any
		if len(resultBytes) > 0 && json.Unmarshal(resultBytes, &result) == nil {
			exec.Result = &result
		}
		// Target is stored as UUID, but we surface the slug
		exec.Slug = targetSlug
		items = append(items, exec)
	}
	if rows.Err() != nil {
		return nil, rows.Err()
	}

	var next *string
	if len(items) > limit {
		items = items[:limit]
		last := items[len(items)-1]
		cursor := formatExecutionCursor(last.CreatedAt, last.Id)
		next = &cursor
	}
	if items == nil {
		items = []gen.Execution{}
	}
	return gen.ListExecutions200JSONResponse{Items: items, NextCursor: next}, nil
}

// Executions are ordered by (created_at DESC, entity_id DESC), so the cursor
// has to carry both — created_at alone is not unique, and paginating on a
// non-unique key drops or repeats rows at page boundaries.
func formatExecutionCursor(createdAt time.Time, id uuid.UUID) string {
	return createdAt.UTC().Format(time.RFC3339Nano) + "," + id.String()
}

func parseExecutionCursor(cursor *string) (*time.Time, *uuid.UUID, error) {
	if cursor == nil || *cursor == "" {
		return nil, nil, nil
	}
	rawTime, rawID, ok := strings.Cut(*cursor, ",")
	if !ok {
		return nil, nil, domain.ErrInvalidInput
	}
	t, err := time.Parse(time.RFC3339Nano, rawTime)
	if err != nil {
		return nil, nil, domain.ErrInvalidInput
	}
	id, err := uuid.Parse(rawID)
	if err != nil {
		return nil, nil, domain.ErrInvalidInput
	}
	return &t, &id, nil
}

func (s *Server) GetExecution(ctx context.Context, req gen.GetExecutionRequestObject) (gen.GetExecutionResponseObject, error) {
	id, err := s.resolveEntityID(ctx, req.Id)
	if err != nil {
		return nil, err
	}

	var exec gen.Execution
	var resultBytes []byte
	var targetSlug string
	err = s.pool.QueryRow(ctx, `
		SELECT e.entity_id, e.classification_id::text, e.signal_entity_id::text,
		       e.target_entity_id, e.action, e.risk_class,
		       e.approval_id::text, e.agent_id::text, e.skill_id::text,
		       e.skill_version, e.status, e.result, e.duration_ms,
		       e.verified, e.correlation_id, e.started_at, e.completed_at, e.created_at,
		       te.slug
		FROM executions e
		JOIN entities te ON te.id = e.target_entity_id
		WHERE e.entity_id = $1`, id).
		Scan(&exec.Id, &exec.ClassificationId, &exec.SignalId,
			&exec.Target, &exec.Action, &exec.RiskClass,
			&exec.ApprovalId, &exec.AgentId, &exec.SkillId,
			&exec.SkillVersion, &exec.Status, &resultBytes, &exec.DurationMs,
			&exec.Verified, &exec.CorrelationId, &exec.StartedAt, &exec.CompletedAt,
			&exec.CreatedAt, &targetSlug)
	if err != nil {
		if err == pgx.ErrNoRows {
			return nil, fmt.Errorf("%w: execution %s", domain.ErrNotFound, req.Id)
		}
		return nil, err
	}
	var result map[string]any
	if len(resultBytes) > 0 && json.Unmarshal(resultBytes, &result) == nil {
		exec.Result = &result
	}
	exec.Slug = targetSlug
	return gen.GetExecution200JSONResponse(exec), nil
}

func (s *Server) RequestExecution(ctx context.Context, req gen.RequestExecutionRequestObject) (gen.RequestExecutionResponseObject, error) {
	if req.Body == nil {
		return nil, fmt.Errorf("%w: request body is required", domain.ErrInvalidInput)
	}

	id, err := uuid.NewV7()
	if err != nil {
		return nil, err
	}

	targetID, err := s.resolveEntityID(ctx, req.Body.Target)
	if err != nil {
		return nil, err
	}

	correlationID := uuid.New().String()

	tx, err := s.pool.Begin(ctx)
	if err != nil {
		return nil, err
	}
	defer tx.Rollback(ctx)

	q := sqlcgen.New(tx)

	// Full UUID, not a truncated prefix — an 8-char prefix of a UUIDv7
	// collides for real under back-to-back requests since the leading bytes
	// encode a millisecond timestamp (observed live via the MCP run tool).
	execSlug := "exec:" + id.String()
	if _, err := q.InsertEntity(ctx, sqlcgen.InsertEntityParams{
		ID:         id,
		Slug:       execSlug,
		Type:       "execution",
		Name:       req.Body.Action + " on " + req.Body.Target,
		Attributes: []byte("{}"),
	}); err != nil {
		return nil, err
	}

	if err := q.InsertExecution(ctx, sqlcgen.InsertExecutionParams{
		EntityID:       id,
		TargetEntityID: &targetID,
		Action:         req.Body.Action,
		RiskClass:      "unclassified", // will be classified by classifier
		CorrelationID:  correlationID,
	}); err != nil {
		return nil, err
	}

	// Re-read to get the full record.
	var exec gen.Execution
	var resultBytes []byte
	var targetSlug string
	err = tx.QueryRow(ctx, `
		SELECT e.entity_id, e.classification_id::text, e.signal_entity_id::text,
		       e.target_entity_id, e.action, e.risk_class,
		       e.approval_id::text, e.agent_id::text, e.skill_id::text,
		       e.skill_version, e.status, e.result, e.duration_ms,
		       e.verified, e.correlation_id, e.started_at, e.completed_at, e.created_at,
		       te.slug
		FROM executions e
		JOIN entities te ON te.id = e.target_entity_id
		WHERE e.entity_id = $1`, id).
		Scan(&exec.Id, &exec.ClassificationId, &exec.SignalId,
			&exec.Target, &exec.Action, &exec.RiskClass,
			&exec.ApprovalId, &exec.AgentId, &exec.SkillId,
			&exec.SkillVersion, &exec.Status, &resultBytes, &exec.DurationMs,
			&exec.Verified, &exec.CorrelationId, &exec.StartedAt, &exec.CompletedAt,
			&exec.CreatedAt, &targetSlug)
	if err != nil {
		return nil, err
	}
	exec.Slug = targetSlug

	actorType, actor := actorInfo(ctx)
	if auditErr := observability.Audit(ctx, q, actorType, actor, "create",
		&id, "POST", "/api/v1/executions", "",
		nil,
		map[string]any{"action": req.Body.Action, "target": req.Body.Target}); auditErr != nil {
		return nil, auditErr
	}
	if eventErr := observability.Event(ctx, q, "execution.requested", &id,
		"info", "oikos-api", "",
		map[string]any{"action": req.Body.Action, "target": req.Body.Target}); eventErr != nil {
		return nil, eventErr
	}

	if err := tx.Commit(ctx); err != nil {
		return nil, err
	}

	return gen.RequestExecution201JSONResponse(exec), nil
}

func (s *Server) CancelExecution(ctx context.Context, req gen.CancelExecutionRequestObject) (gen.CancelExecutionResponseObject, error) {
	id, err := s.resolveEntityID(ctx, req.Id)
	if err != nil {
		return nil, err
	}

	tx, err := s.pool.Begin(ctx)
	if err != nil {
		return nil, err
	}
	defer tx.Rollback(ctx)

	q := sqlcgen.New(tx)
	if err := q.UpdateExecutionStatus(ctx, sqlcgen.UpdateExecutionStatusParams{
		EntityID: id,
		Status:   "cancelled",
	}); err != nil {
		if err == pgx.ErrNoRows {
			return nil, fmt.Errorf("%w: execution %s", domain.ErrNotFound, req.Id)
		}
		return nil, err
	}

	// Re-read.
	var exec gen.Execution
	var resultBytes []byte
	var targetSlug string
	err = tx.QueryRow(ctx, `
		SELECT e.entity_id, e.classification_id::text, e.signal_entity_id::text,
		       e.target_entity_id, e.action, e.risk_class,
		       e.approval_id::text, e.agent_id::text, e.skill_id::text,
		       e.skill_version, e.status, e.result, e.duration_ms,
		       e.verified, e.correlation_id, e.started_at, e.completed_at, e.created_at,
		       te.slug
		FROM executions e
		JOIN entities te ON te.id = e.target_entity_id
		WHERE e.entity_id = $1`, id).
		Scan(&exec.Id, &exec.ClassificationId, &exec.SignalId,
			&exec.Target, &exec.Action, &exec.RiskClass,
			&exec.ApprovalId, &exec.AgentId, &exec.SkillId,
			&exec.SkillVersion, &exec.Status, &resultBytes, &exec.DurationMs,
			&exec.Verified, &exec.CorrelationId, &exec.StartedAt, &exec.CompletedAt,
			&exec.CreatedAt, &targetSlug)
	if err != nil {
		return nil, err
	}
	exec.Slug = targetSlug

	actorType, actor := actorInfo(ctx)
	if auditErr := observability.Audit(ctx, q, actorType, actor, "cancel",
		&id, "POST", "/api/v1/executions/"+req.Id+"/cancel", "",
		nil,
		map[string]any{"status": "cancelled"}); auditErr != nil {
		return nil, auditErr
	}

	if err := tx.Commit(ctx); err != nil {
		return nil, err
	}

	return gen.CancelExecution200JSONResponse(exec), nil
}
