package persistence

import (
	"context"
	"database/sql"
	"log"
	"strings"

	apperrors "github.com/nexuscrm/kernel/pkg/errors"
	"github.com/nexuscrm/shared/pkg/constants"
)

// GetRoleAncestorsAtSecurityEpoch reads only the requested parent chain and
// checks the org security epoch in the same SQL-statement snapshot. It does not
// serve historical epochs or retain process-local role state. UNION DISTINCT
// bounds cycles to their distinct identities; validation below rejects them.
func (r *PermissionRepository) GetRoleAncestorsAtSecurityEpoch(ctx context.Context, orgID, roleID string, securityEpoch uint64) ([]string, error) {
	if orgID == "" || roleID == "" || securityEpoch == 0 {
		return nil, roleHierarchyReadFailure(ctx, "invalid_authority", nil)
	}
	rows, err := r.getExecutor(ctx).QueryContext(ctx, `WITH RECURSIVE role_path AS (
  SELECT r.Id, r.ParentRoleId, r.IsDeleted
  FROM UserRole r JOIN _System_RuntimeSecurityState s
    ON s.OrgId = ? AND s.SecurityEpoch = ?
  WHERE r.Id = ?
  UNION DISTINCT
  SELECT parent.Id, parent.ParentRoleId, parent.IsDeleted
  FROM UserRole parent JOIN role_path child ON parent.Id = child.ParentRoleId
  WHERE child.IsDeleted = 0
 ) SELECT Id, ParentRoleId, IsDeleted FROM role_path`, orgID, securityEpoch, roleID)
	if err != nil {
		return nil, roleHierarchyReadFailure(ctx, "query_failed", err)
	}
	defer rows.Close()
	type roleLink struct {
		id      string
		parent  sql.NullString
		deleted bool
	}
	links := map[string]roleLink{}
	for rows.Next() {
		var link roleLink
		if err := rows.Scan(&link.id, &link.parent, &link.deleted); err != nil {
			return nil, roleHierarchyReadFailure(ctx, "scan_failed", err)
		}
		links[strings.ToLower(link.id)] = link
	}
	if err := rows.Err(); err != nil {
		return nil, roleHierarchyReadFailure(ctx, "rows_failed", err)
	}
	ancestors := []string{}
	visited := map[string]bool{}
	current := strings.ToLower(roleID)
	for {
		link, exists := links[current]
		if !exists {
			return nil, roleHierarchyReadFailure(ctx, "role_or_epoch_unavailable", nil)
		}
		if link.deleted {
			return nil, roleHierarchyReadFailure(ctx, "deleted_role", nil)
		}
		if visited[current] {
			return nil, roleHierarchyReadFailure(ctx, "cycle", nil)
		}
		visited[current] = true
		// Imported root roles can store an empty reference instead of SQL NULL.
		if !link.parent.Valid || link.parent.String == "" {
			return ancestors, nil
		}
		parent, exists := links[strings.ToLower(link.parent.String)]
		if !exists {
			return nil, roleHierarchyReadFailure(ctx, "missing_parent", nil)
		}
		ancestors = append(ancestors, parent.id)
		current = strings.ToLower(parent.id)
	}
}

// Reasons are fixed at the owning read boundary. Do not log the SQL cause or
// role/org identities: the admitted request ID is enough for correlation.
func roleHierarchyReadFailure(ctx context.Context, reason string, cause error) error {
	requestID, _ := ctx.Value(constants.ContextKeyRequestID).(string)
	log.Printf("runtime role hierarchy read failed: request_id=%q reason=%s", requestID, reason)
	return apperrors.NewInternalError("Role hierarchy read failed: "+reason, cause)
}
