package app

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

	"github.com/dtoro/oikos/internal/core/domain"
	"github.com/dtoro/oikos/internal/core/ports"
	"github.com/google/uuid"
)

// EntityService owns the entity aggregate's use-cases: create with derived
// checks, update with optimistic versioning and check regeneration,
// lifecycle transitions. Validation against the (cached) ontology happens
// here, in the pure half; transactional check-then-act invariants
// (version, declared transitions, preconditions) run in the repository
// (plan §3.6).
type EntityService struct {
	entities ports.EntityRepository
	onto     ports.OntologyStore
}

// NewEntityService wires the service.
func NewEntityService(entities ports.EntityRepository, onto ports.OntologyStore) *EntityService {
	return &EntityService{entities: entities, onto: onto}
}

// CreateEntityCmd is one entity creation. Actor identifies the calling
// surface for the audit trail ("operator:<label>", "agent:mcp").
type CreateEntityCmd struct {
	Slug       string
	Type       string
	Name       string
	State      string            // "" → lifecycle default
	Attributes map[string]any
	ActorType  string
	Actor      string
	Method     string // audit context, e.g. "POST" / "TOOL"
	Path       string // audit context, e.g. "/api/v1/entities" / "create_entity"
	// Idempotency, when set, replays-protects the create. The adapter owns
	// the request hash and the cached-body renderer (its wire shape); the
	// repository stores the record in the create's transaction.
	Idempotency *ports.Idempotency
}

// Create validates the type (exists, concrete, state declared), derives
// default checks, and commits entity + checks + audit + event in one
// transaction.
func (s *EntityService) Create(ctx context.Context, cmd CreateEntityCmd) (domain.Entity, DeriveResult, error) {
	var res DeriveResult

	tree, err := s.onto.LoadTypeTree(ctx)
	if err != nil {
		return domain.Entity{}, res, err
	}

	slug := cmd.Slug
	if slug == "" {
		slug = cmd.Type + ":" + cmd.Name
	}

	// Caller-supplied states are validated against the lifecycle's declared
	// states — a create bypass of lifecycle guardrails would let an agent
	// create in a terminal state without satisfying the preconditions that
	// SetState enforces for the same transition. Both surfaces (REST, MCP)
	// now share this rule.
	state := cmd.State
	if state == "" {
		state = tree.DefaultState(cmd.Type)
	}
	if err := tree.ValidateEntity(cmd.Type, state); err != nil {
		return domain.Entity{}, res, err
	}

	id, err := uuid.NewV7()
	if err != nil {
		return domain.Entity{}, res, err
	}
	e := domain.Entity{
		ID:         domain.UUID(id.String()),
		Slug:       slug,
		Type:       cmd.Type,
		Name:       cmd.Name,
		State:      state,
		Attributes: cmd.Attributes,
	}

	// Derivation for a create has no graph fallback available — a new entity
	// has no edges yet. A type whose address comes from its host (a service)
	// produces no checks on this pass; the gap is deliberate and visible
	// (coverage sweep), and the next mutation or ingest fills it once the
	// hosting edge exists.
	attrsJSON, _ := json.Marshal(cmd.Attributes)
	defs, dres := Derive(tree, CheckTarget{
		ID: string(e.ID), Slug: slug, Type: cmd.Type, Name: cmd.Name, Attrs: attrsJSON,
	}, nil)
	// Derive is pure and cannot count writes; every derived def is ensured
	// by the repository in the create's transaction (a write failure aborts
	// the whole create), so the ensured count is the derived count.
	dres.Created = len(defs)
	res = dres
	derived := make([]ports.DerivedCheck, len(defs))
	for i, d := range defs {
		derived[i] = ports.DerivedCheck{Kind: d.Kind, Config: d.Config, IntervalS: d.IntervalS}
	}

	input := ports.EntityCreateInput{
		Entity:        e,
		DerivedChecks: derived,
		Audit: []ports.AuditEntry{{
			ActorType: cmd.ActorType, ActorLabel: cmd.Actor, Action: "create",
			EntityID: e.ID, Method: cmd.Method, Path: cmd.Path,
			Details: map[string]any{"type": cmd.Type, "slug": slug},
		}},
		Event: &ports.Event{
			Type: "entity.created", Severity: "info", Source: "oikos-api", EntityID: e.ID,
			Data: map[string]any{"slug": slug, "type": cmd.Type},
		},
	}
	input.Idempotency = cmd.Idempotency

	created, err := s.entities.Create(ctx, input)
	if err != nil {
		return domain.Entity{}, res, err
	}
	return created, res, nil
}

// UpdateEntityCmd mutates an entity. Attributes are REPLACED when
// AttrsReplace is true and shallow-merged otherwise. SetState semantics
// ride along: a State change is lifecycle-validated.
type UpdateEntityCmd struct {
	SlugOrID     string
	ExpectedVer  int
	Name         string
	State        string
	Attributes   map[string]any
	AttrsReplace bool
	Maintenance  *time.Time
	SetMaint     bool
	// RederiveChecks regenerates default checks from the post-update
	// attributes (a `monitoring` attribute change re-wires probes).
	RederiveChecks bool
	ActorType      string
	Actor          string
	Method         string
	Path           string
}

// Update loads the current entity, validates the target state against the
// ontology, and commits the mutation atomically. The optimistic-version and
// transition-precondition checks run inside the repository's transaction.
// When RederiveChecks is set the returned DeriveResult summarizes the
// derivation the transaction applied (for surface messages).
func (s *EntityService) Update(ctx context.Context, cmd UpdateEntityCmd) (domain.Entity, DeriveResult, error) {
	var dres DeriveResult
	current, err := s.resolve(ctx, cmd.SlugOrID)
	if err != nil {
		return domain.Entity{}, dres, err
	}

	tree, err := s.onto.LoadTypeTree(ctx)
	if err != nil {
		return domain.Entity{}, dres, err
	}
	if cmd.State != "" && cmd.State != current.State {
		if err := tree.ValidateEntity(current.Type, cmd.State); err != nil {
			return domain.Entity{}, dres, err
		}
	}

	e := domain.Entity{
		ID:      current.ID,
		Slug:    current.Slug,
		Type:    current.Type,
		Name:    cmd.Name,
		State:   cmd.State,
		Version: current.Version,
	}
	switch {
	case cmd.Attributes == nil:
		e.Attributes = nil // keep current
	case cmd.AttrsReplace:
		e.Attributes = cmd.Attributes
	default:
		merged := map[string]any{}
		for k, v := range current.Attributes {
			merged[k] = v
		}
		for k, v := range cmd.Attributes {
			merged[k] = v
		}
		e.Attributes = merged
	}
	if cmd.SetMaint {
		e.MaintenanceUntil = cmd.Maintenance
	}

	updated, err := s.entities.Update(ctx, ports.EntityUpdateInput{
		Entity:         e,
		ExpectedVersion: cmd.ExpectedVer,
		RederiveChecks:  cmd.RederiveChecks,
		Audit: []ports.AuditEntry{{
			ActorType: cmd.ActorType, ActorLabel: cmd.Actor, Action: "patch",
			EntityID: current.ID, Method: cmd.Method, Path: cmd.Path,
			Details: map[string]any{"version": current.Version},
		}},
		Event: &ports.Event{
			Type: "entity.updated", Severity: "info", Source: "oikos-api", EntityID: current.ID,
			Data: map[string]any{"slug": current.Slug, "type": current.Type},
		},
	})
	if err != nil {
		return domain.Entity{}, dres, err
	}
	if cmd.RederiveChecks {
		attrsJSON, _ := json.Marshal(updated.Attributes)
		defs, d := Derive(tree, CheckTarget{
			ID: string(updated.ID), Slug: updated.Slug, Type: updated.Type,
			Name: updated.Name, Attrs: attrsJSON,
		}, nil)
		d.Created = len(defs)
		dres = d
	}
	return updated, dres, nil
}

// SetState transitions an entity's lifecycle. Declared-transition and
// precondition enforcement happens in the repository's transaction; a
// stale FromState is refused there (check-then-act).
func (s *EntityService) SetState(ctx context.Context, slug, fromState, toState, actorType, actor, method, path string) (domain.Entity, error) {
	current, err := s.entities.BySlug(ctx, slug)
	if err != nil {
		return domain.Entity{}, err
	}
	after, err := s.entities.SetState(ctx, ports.EntityTransitionInput{
		Slug: slug,
		From: fromState,
		To:   toState,
		Audit: []ports.AuditEntry{{
			ActorType: actorType, ActorLabel: actor, Action: "state",
			EntityID: current.ID, Method: method, Path: path,
			Details: map[string]any{"from": fromState, "to": toState},
		}},
		Event: &ports.Event{
			Type: "entity.state.changed", Severity: "info", Source: "oikos-api", EntityID: current.ID,
			Data: map[string]any{"slug": slug, "from": fromState, "to": toState},
		},
	})
	if err != nil {
		return domain.Entity{}, err
	}
	return after, nil
}

func (s *EntityService) resolve(ctx context.Context, slugOrID string) (domain.Entity, error) {
	if _, err := uuid.Parse(slugOrID); err == nil {
		return s.entities.Get(ctx, domain.UUID(slugOrID))
	}
	return s.entities.BySlug(ctx, slugOrID)
}
