// Package ontology implements the meta-schema logic: the entity-type
// hierarchy (is-a with abstract types), relationship endpoint validation,
// cardinality enforcement, lifecycle state checks, and transition
// requirement enforcement. Both the seed ingest and the API mutation paths
// validate through this package so the graph can never violate the
// ontology (plan R3-1).
package ontology

import (
	"context"
	"fmt"

	"github.com/dtoro/oikos/internal/core/domain"
	"github.com/jackc/pgx/v5/pgxpool"
	"github.com/google/uuid"
)

// TypeInfo is the subset of an entity type the validator needs.
type TypeInfo struct {
	Parent      string
	IsAbstract  bool
	LifecycleID string
	Layer       string

	// Monitoring is this type's own `monitoring:` declaration, or nil if it
	// declared nothing (in which case the answer comes from an ancestor, or
	// from the layer default). A non-nil pointer to an empty slice means
	// "explicitly unmonitorable" — see TypeTree.Monitoring.
	Monitoring *[]string
}

// RelTypeInfo is the subset of a relationship type the validator needs.
type RelTypeInfo struct {
	SourceType  string
	TargetType  string
	Cardinality string
}

// LifecycleInfo is the subset of a lifecycle the validator needs.
type LifecycleInfo struct {
	States       map[string]bool
	DefaultState string
}

// TypeTree holds the loaded ontology meta-schema for validation.
type TypeTree struct {
	Types      map[string]TypeInfo
	RelTypes   map[string]RelTypeInfo
	Lifecycles map[string]LifecycleInfo
}

// IsA reports whether typ is target or a descendant of it.
func (t *TypeTree) IsA(typ, target string) bool {
	seen := map[string]bool{}
	for cur := typ; cur != ""; cur = t.Types[cur].Parent {
		if cur == target {
			return true
		}
		if seen[cur] {
			return false // cycle guard — ingest rejects cycles, belt and braces
		}
		seen[cur] = true
		if _, ok := t.Types[cur]; !ok {
			return false
		}
	}
	return false
}

// ValidateEntity checks that typ exists, is not abstract, and that state
// (if set) is legal for the type's lifecycle.
func (t *TypeTree) ValidateEntity(typ, state string) error {
	info, ok := t.Types[typ]
	if !ok {
		return fmt.Errorf("%w: entity type %q", domain.ErrNotFound, typ)
	}
	if info.IsAbstract {
		return fmt.Errorf("%w: %q", domain.ErrAbstractType, typ)
	}
	if state == "" {
		return nil
	}
	if info.LifecycleID == "" {
		return fmt.Errorf("%w: type %q has no lifecycle but state %q given",
			domain.ErrInvalidTransition, typ, state)
	}
	lc, ok := t.Lifecycles[info.LifecycleID]
	if !ok {
		return fmt.Errorf("%w: lifecycle %q", domain.ErrNotFound, info.LifecycleID)
	}
	if !lc.States[state] {
		return fmt.Errorf("%w: state %q not in lifecycle %q",
			domain.ErrInvalidTransition, state, info.LifecycleID)
	}
	return nil
}

// ValidateEdge checks that relType exists and that the endpoint entity
// types are the declared source/target types or descendants of them.
func (t *TypeTree) ValidateEdge(relType, sourceEntityType, targetEntityType string) error {
	rt, ok := t.RelTypes[relType]
	if !ok {
		return fmt.Errorf("%w: relationship type %q", domain.ErrNotFound, relType)
	}
	if !t.IsA(sourceEntityType, rt.SourceType) {
		return fmt.Errorf("%w: %s source %q is not a %q",
			domain.ErrInvalidEdge, relType, sourceEntityType, rt.SourceType)
	}
	if !t.IsA(targetEntityType, rt.TargetType) {
		return fmt.Errorf("%w: %s target %q is not a %q",
			domain.ErrInvalidEdge, relType, targetEntityType, rt.TargetType)
	}
	return nil
}

// DefaultState returns the default lifecycle state for a type ("" if none).
func (t *TypeTree) DefaultState(typ string) string {
	info, ok := t.Types[typ]
	if !ok || info.LifecycleID == "" {
		return ""
	}
	return t.Lifecycles[info.LifecycleID].DefaultState
}

// ─── Transition check enforcement ────────────────────────────────────

// Validated transitions in seeds/ontology.yaml carry a "requires:" list
// of named checks. Each check name maps to one of the functions below.
// The Go runtime enforces these before allowing a state transition.

// CheckFn validates a single named transition requirement.
type CheckFn func(ctx context.Context, pool *pgxpool.Pool, entityID uuid.UUID, entityType string, attrs map[string]any) error

// TransitionChecks maps named check IDs to their implementation.
var TransitionChecks = map[string]CheckFn{
	"age-key-enrolled-if-needed":  checkAgeKeyEnrolled,
	"mesh-joined-if-needed":       checkMeshJoined,
	"health-check-answering":      checkHealthCheckAnswering,
	"no-inbound-edges":            checkNoInboundEdges,
	"secrets-revoked-and-rekeyed": checkSecretsRevoked,
	"backups-verified":            checkBackupsVerified,
	"ingress-and-dns-removed":     checkIngressDNSRemoved,
	"doc-page-complete":           checkDocPageComplete,
}

func checkAgeKeyEnrolled(ctx context.Context, pool *pgxpool.Pool, entityID uuid.UUID, entityType string, attrs map[string]any) error {
	// Workstations and servers need age keys; compute entities (LXC/VM) don't.
	if entityType == "lxc" || entityType == "vm" || entityType == "docker-container" {
		return nil
	}
	if _, ok := attrs["age_pubkey"]; !ok {
		return fmt.Errorf("%w: age_pubkey not set", domain.ErrInvalidTransition)
	}
	return nil
}

func checkMeshJoined(ctx context.Context, pool *pgxpool.Pool, entityID uuid.UUID, entityType string, attrs map[string]any) error {
	if entityType == "lxc" || entityType == "vm" || entityType == "docker-container" {
		return nil
	}
	if _, ok := attrs["mesh_ip"]; !ok {
		return fmt.Errorf("%w: mesh_ip not set", domain.ErrInvalidTransition)
	}
	return nil
}

func checkHealthCheckAnswering(ctx context.Context, pool *pgxpool.Pool, entityID uuid.UUID, entityType string, attrs map[string]any) error {
	var health string
	err := pool.QueryRow(ctx,
		"SELECT COALESCE(health, 'unknown') FROM entity_status WHERE entity_id = $1", entityID).Scan(&health)
	if err != nil {
		return nil // entity_status row may not exist yet; non-blocking
	}
	if health == "down" {
		return fmt.Errorf("%w: health is down", domain.ErrInvalidTransition)
	}
	return nil
}

func checkNoInboundEdges(ctx context.Context, pool *pgxpool.Pool, entityID uuid.UUID, entityType string, attrs map[string]any) error {
	var count int
	err := pool.QueryRow(ctx,
		`SELECT COUNT(*) FROM relationships
		 WHERE target_id = $1 AND valid_to IS NULL
		   AND type IN ('depends-on', 'hosts', 'provides', 'mounts', 'routes-to', 'stores-on')`,
		entityID).Scan(&count)
	if err != nil {
		return err
	}
	if count > 0 {
		return fmt.Errorf("%w: %d inbound edges still exist", domain.ErrInvalidTransition, count)
	}
	return nil
}

func checkSecretsRevoked(ctx context.Context, pool *pgxpool.Pool, entityID uuid.UUID, entityType string, attrs map[string]any) error {
	// Application-level check: the API caller must have already revoked
	// Infisical identity and removed age key. We verify age_pubkey is
	// still present as a guard — if it's gone, secrets were revoked.
	if _, ok := attrs["age_pubkey"]; ok {
		return fmt.Errorf("%w: age_pubkey still present; revoke secrets first", domain.ErrInvalidTransition)
	}
	return nil
}

func checkBackupsVerified(ctx context.Context, pool *pgxpool.Pool, entityID uuid.UUID, entityType string, attrs map[string]any) error {
	// Check for a recent audit log entry confirming backup verification.
	var count int
	err := pool.QueryRow(ctx,
		`SELECT COUNT(*) FROM audit_log
		 WHERE entity_id = $1 AND action = 'backup-verified'
		   AND timestamp > NOW() - INTERVAL '30 days'`,
		entityID).Scan(&count)
	if err != nil {
		return err
	}
	if count == 0 {
		return fmt.Errorf("%w: backup not verified in last 30 days", domain.ErrInvalidTransition)
	}
	return nil
}

func checkIngressDNSRemoved(ctx context.Context, pool *pgxpool.Pool, entityID uuid.UUID, entityType string, attrs map[string]any) error {
	// Check for remaining ingress or DNS relationships.
	var count int
	err := pool.QueryRow(ctx,
		`SELECT COUNT(*) FROM relationships
		 WHERE source_id = $1 AND valid_to IS NULL
		   AND type IN ('routes-to', 'provides', 'hosts')`,
		entityID).Scan(&count)
	if err != nil {
		return err
	}
	if count > 0 {
		return fmt.Errorf("%w: %d ingress/DNS relationships still exist", domain.ErrInvalidTransition, count)
	}
	return nil
}

func checkDocPageComplete(ctx context.Context, pool *pgxpool.Pool, entityID uuid.UUID, entityType string, attrs map[string]any) error {
	var count int
	err := pool.QueryRow(ctx,
		`SELECT COUNT(*) FROM relationships
		 WHERE (source_id = $1 OR target_id = $1) AND valid_to IS NULL AND type = 'documents'`,
		entityID).Scan(&count)
	if err != nil {
		return err
	}
	if count == 0 {
		return fmt.Errorf("%w: no document edge found", domain.ErrInvalidTransition)
	}
	return nil
}
