package db

import (
	"context"
	"fmt"

	"github.com/dtoro/oikos/internal/ontology"
	"github.com/jackc/pgx/v5"
)

// LoadTypeTree loads the ontology meta-schema from the DB for validation.
// Called within the same transaction as an ingest so it sees just-ingested
// types.
func LoadTypeTree(ctx context.Context, tx pgx.Tx) (*ontology.TypeTree, error) {
	t := &ontology.TypeTree{
		Types:      map[string]ontology.TypeInfo{},
		RelTypes:   map[string]ontology.RelTypeInfo{},
		Lifecycles: map[string]ontology.LifecycleInfo{},
	}

	rows, err := tx.Query(ctx,
		`SELECT name, COALESCE(parent_type,''), is_abstract, COALESCE(lifecycle_id,''),
		        layer, monitoring_spec
		 FROM entity_types`)
	if err != nil {
		return nil, fmt.Errorf("load entity_types: %w", err)
	}
	for rows.Next() {
		var name string
		var info ontology.TypeInfo
		// NULL monitoring_spec means the type declared nothing; '[]' means it
		// declared "explicitly unmonitorable". Scanning through a pointer is
		// what keeps those two apart — see ontology.TypeTree.Monitoring.
		var monitoring *[]string
		if err := rows.Scan(&name, &info.Parent, &info.IsAbstract, &info.LifecycleID,
			&info.Layer, &monitoring); err != nil {
			rows.Close()
			return nil, err
		}
		info.Monitoring = monitoring
		t.Types[name] = info
	}
	rows.Close()
	if rows.Err() != nil {
		return nil, rows.Err()
	}

	rows, err = tx.Query(ctx,
		`SELECT name, source_type, target_type, cardinality FROM relationship_types`)
	if err != nil {
		return nil, fmt.Errorf("load relationship_types: %w", err)
	}
	for rows.Next() {
		var name string
		var info ontology.RelTypeInfo
		if err := rows.Scan(&name, &info.SourceType, &info.TargetType, &info.Cardinality); err != nil {
			rows.Close()
			return nil, err
		}
		t.RelTypes[name] = info
	}
	rows.Close()
	if rows.Err() != nil {
		return nil, rows.Err()
	}

	rows, err = tx.Query(ctx,
		`SELECT id, states, default_state FROM lifecycle_defs`)
	if err != nil {
		return nil, fmt.Errorf("load lifecycle_defs: %w", err)
	}
	for rows.Next() {
		var id, def string
		var states []string
		if err := rows.Scan(&id, &states, &def); err != nil {
			rows.Close()
			return nil, err
		}
		set := make(map[string]bool, len(states))
		for _, s := range states {
			set[s] = true
		}
		t.Lifecycles[id] = ontology.LifecycleInfo{States: set, DefaultState: def}
	}
	rows.Close()
	if rows.Err() != nil {
		return nil, rows.Err()
	}

	return t, nil
}

// ValidateCardinality checks all CURRENT edges against their relationship
// type's declared cardinality. Runs inside the ingest transaction so a
// violating seed rolls back atomically.
func ValidateCardinality(ctx context.Context, tx pgx.Tx) error {
	// one-to-one / many-to-one: a source may have at most one outgoing
	// current edge of the type.
	rows, err := tx.Query(ctx, `
		SELECT r.type, se.slug, count(*)
		FROM relationships r
		JOIN relationship_types rt ON rt.name = r.type
		JOIN entities se ON se.id = r.source_id
		WHERE r.valid_to IS NULL AND rt.cardinality IN ('one-to-one','many-to-one')
		GROUP BY r.type, se.slug HAVING count(*) > 1`)
	if err != nil {
		return err
	}
	violations, err := collectViolations(rows, "source")
	if err != nil {
		return err
	}

	// one-to-one / one-to-many: a target may have at most one incoming
	// current edge of the type.
	rows, err = tx.Query(ctx, `
		SELECT r.type, te.slug, count(*)
		FROM relationships r
		JOIN relationship_types rt ON rt.name = r.type
		JOIN entities te ON te.id = r.target_id
		WHERE r.valid_to IS NULL AND rt.cardinality IN ('one-to-one','one-to-many')
		GROUP BY r.type, te.slug HAVING count(*) > 1`)
	if err != nil {
		return err
	}
	v2, err := collectViolations(rows, "target")
	if err != nil {
		return err
	}
	violations = append(violations, v2...)

	if len(violations) > 0 {
		return fmt.Errorf("cardinality violations: %v", violations)
	}
	return nil
}

func collectViolations(rows pgx.Rows, side string) ([]string, error) {
	defer rows.Close()
	var out []string
	for rows.Next() {
		var relType, slug string
		var n int
		if err := rows.Scan(&relType, &slug, &n); err != nil {
			return nil, err
		}
		out = append(out, fmt.Sprintf("%s %s=%s (%d edges)", relType, side, slug, n))
	}
	return out, rows.Err()
}
