package httpapi

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

	"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"
)

// ─── Checks ────────────────────────────────────────────────────────────

func (s *Server) ListChecks(ctx context.Context, req gen.ListChecksRequestObject) (gen.ListChecksResponseObject, error) {
	limit := clampLimit(req.Params.Limit)
	rows, err := s.pool.Query(ctx, `
		SELECT cd.entity_id, e.slug, cd.kind,
		       COALESCE(te.slug, '') AS target_slug, cd.target_type,
		       cd.config, cd.interval_s, cd.timeout_s, cd.zone, cd.enabled,
		       e.version, cd.last_health, cd.last_run_at
		FROM check_defs cd
		JOIN entities e ON e.id = cd.entity_id
		LEFT JOIN entities te ON te.id = cd.target_id
		WHERE ($1::text IS NULL OR cd.kind = $1)
		  AND ($2::text IS NULL OR te.slug = $2)
		  AND ($3::bool IS NULL OR cd.enabled = $3)
		  AND ($4::text IS NULL OR e.slug > $4)
		ORDER BY e.slug
		LIMIT $5`,
		req.Params.Kind, req.Params.Target, req.Params.Enabled, req.Params.Cursor, limit+1)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	items := []gen.Check{}
	for rows.Next() {
		var c gen.Check
		var targetSlug string
		var configBytes []byte
		// last_health is what turns a check list from configuration into an
		// explanation: an entity's health is the worst of these, so this is
		// the field that says which probe is responsible.
		var lastHealth *string
		if err := rows.Scan(&c.Id, &c.Slug, &c.Kind, &targetSlug, &c.TargetType,
			&configBytes, &c.IntervalS, &c.TimeoutS, &c.Zone, &c.Enabled, &c.Version,
			&lastHealth, &c.LastRunAt); err != nil {
			return nil, err
		}
		if lastHealth != nil {
			h := gen.CheckLastHealth(*lastHealth)
			c.LastHealth = &h
		}
		if targetSlug != "" {
			c.Target = &targetSlug
		}
		var config map[string]any
		if len(configBytes) > 0 && json.Unmarshal(configBytes, &config) == nil && len(config) > 0 {
			c.Config = &config
		}
		items = append(items, c)
	}
	if rows.Err() != nil {
		return nil, rows.Err()
	}

	var next *string
	if len(items) > limit {
		items = items[:limit]
		next = &items[len(items)-1].Slug
	}
	if items == nil {
		items = []gen.Check{}
	}
	return gen.ListChecks200JSONResponse{Items: items, NextCursor: next}, nil
}

func (s *Server) CreateCheck(ctx context.Context, req gen.CreateCheckRequestObject) (gen.CreateCheckResponseObject, 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
	}

	slug := req.Body.Slug
	if slug == "" {
		slug = "check:" + string(req.Body.Kind) + ":" + uuid.New().String()[:8]
	}

	// Resolve target if provided.
	var targetID *uuid.UUID
	if req.Body.Target != nil && *req.Body.Target != "" {
		tid, rerr := s.resolveEntityID(ctx, *req.Body.Target)
		if rerr != nil {
			return nil, rerr
		}
		targetID = &tid
	}

	intervalS := int32(300)
	if req.Body.IntervalS != nil {
		intervalS = int32(*req.Body.IntervalS)
	}
	timeoutS := int32(30)
	if req.Body.TimeoutS != nil {
		timeoutS = int32(*req.Body.TimeoutS)
	}
	enabled := true
	if req.Body.Enabled != nil {
		enabled = *req.Body.Enabled
	}

	configJSON := []byte("{}")
	if req.Body.Config != nil {
		configJSON, _ = json.Marshal(req.Body.Config)
	}

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

	q := sqlcgen.New(tx)

	// Create the entity row (checks are entities).
	entity, err := q.InsertEntity(ctx, sqlcgen.InsertEntityParams{
		ID:         id,
		Slug:       slug,
		Type:       "check",
		Name:       slug,
		Attributes: []byte("{}"),
	})
	if err != nil {
		if strings.Contains(err.Error(), "unique") || strings.Contains(err.Error(), "duplicate") {
			return nil, fmt.Errorf("%w: check %q already exists", domain.ErrAlreadyExists, slug)
		}
		return nil, err
	}

	if err := q.InsertCheckDef(ctx, sqlcgen.InsertCheckDefParams{
		EntityID:   id,
		TargetID:   targetID,
		TargetType: req.Body.TargetType,
		Kind:       string(req.Body.Kind),
		Config:     configJSON,
		IntervalS:  intervalS,
		TimeoutS:   timeoutS,
		Zone:       req.Body.Zone,
		Enabled:    enabled,
	}); err != nil {
		return nil, err
	}

	// Build response Check.
	check := gen.Check{
		Id:         id,
		Slug:       entity.Slug,
		Kind:       gen.CheckKind(req.Body.Kind),
		IntervalS:  int(intervalS),
		TimeoutS:   int(timeoutS),
		Enabled:    enabled,
		TargetType: req.Body.TargetType,
		Zone:       req.Body.Zone,
		Version:    int(entity.Version),
	}
	if req.Body.Config != nil {
		check.Config = req.Body.Config
	}
	if targetID != nil && req.Body.Target != nil {
		check.Target = req.Body.Target
	}

	actorType, actor := actorInfo(ctx)
	if auditErr := observability.Audit(ctx, q, actorType, actor, "create",
		&id, "POST", "/api/v1/checks", "",
		nil,
		map[string]any{"kind": req.Body.Kind, "slug": slug}); auditErr != nil {
		return nil, auditErr
	}

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

	return gen.CreateCheck201JSONResponse(check), nil
}

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

	id, err := s.resolveEntityID(ctx, req.Id)
	if err != nil {
		return nil, err
	}

	// Parse If-Match
	ifMatch := strings.Trim(req.Params.IfMatch, `"`)
	expectedVersion, err := parseIntIfMatch(ifMatch)
	if err != nil {
		return nil, err
	}
	_ = expectedVersion // check_defs don't track version via If-Match today, but we validate the header is present

	if ifMatch == "" {
		return nil, fmt.Errorf("%w: invalid If-Match header", domain.ErrInvalidInput)
	}

	// Get current check def
	current, err := sqlcgen.New(s.pool).GetCheckDef(ctx, id)
	if err != nil {
		if err == pgx.ErrNoRows {
			return nil, fmt.Errorf("%w: check %s", domain.ErrNotFound, req.Id)
		}
		return nil, err
	}

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

	// Apply patch.
	if req.Body.Config != nil {
		current.Config, _ = json.Marshal(req.Body.Config)
	}
	if req.Body.IntervalS != nil {
		current.IntervalS = int32(*req.Body.IntervalS)
	}
	if req.Body.TimeoutS != nil {
		current.TimeoutS = int32(*req.Body.TimeoutS)
	}
	if req.Body.Enabled != nil {
		current.Enabled = *req.Body.Enabled
	}

	if err := sqlcgen.New(tx).UpdateCheckDef(ctx, sqlcgen.UpdateCheckDefParams{
		EntityID:   id,
		Kind:       current.Kind,
		Config:     current.Config,
		IntervalS:  current.IntervalS,
		TimeoutS:   current.TimeoutS,
		TargetID:   current.TargetID,
		TargetType: current.TargetType,
		Zone:       current.Zone,
		Enabled:    current.Enabled,
	}); err != nil {
		return nil, err
	}

	// Re-read to get updated timestamp.
	updated, err := sqlcgen.New(tx).GetCheckDef(ctx, id)
	if err != nil {
		return nil, err
	}

	check := checkDefToGen(updated)

	actorType, actor := actorInfo(ctx)
	if auditErr := observability.Audit(ctx, sqlcgen.New(tx), actorType, actor, "patch",
		&id, "PATCH", "/api/v1/checks/"+req.Id, "",
		nil,
		map[string]any{"enabled": updated.Enabled}); auditErr != nil {
		return nil, auditErr
	}

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

	return gen.PatchCheck200JSONResponse(check), nil
}

func checkDefToGen(cd sqlcgen.CheckDef) gen.Check {
	c := gen.Check{
		Id:         cd.EntityID,
		Kind:       gen.CheckKind(cd.Kind),
		IntervalS:  int(cd.IntervalS),
		TimeoutS:   int(cd.TimeoutS),
		Enabled:    cd.Enabled,
		TargetType: cd.TargetType,
		Zone:       cd.Zone,
	}
	var config map[string]any
	if len(cd.Config) > 0 && json.Unmarshal(cd.Config, &config) == nil && len(config) > 0 {
		c.Config = &config
	}
	// Carried through so toggling a check does not blank its verdict in the
	// UI — the entity window renders last_health to explain which probe is
	// responsible for an entity's health, and a patch response missing it
	// would erase that until the next poll.
	c.LastRunAt = cd.LastRunAt
	if cd.LastHealth != nil {
		h := gen.CheckLastHealth(*cd.LastHealth)
		c.LastHealth = &h
	}
	return c
}

// parseIntIfMatch parses an integer from a raw If-Match header value (with quotes stripped).
func parseIntIfMatch(s string) (int, error) {
	if s == "" {
		return 0, fmt.Errorf("empty version")
	}
	var v int
	for _, c := range s {
		if c < '0' || c > '9' {
			return 0, fmt.Errorf("invalid version: %q", s)
		}
		v = v*10 + int(c-'0')
	}
	return v, nil
}
