package resources

// exploitation_authorization.go — Autorisation d'Exploitation (AE)
//
// AEs are embedded inside AbstractResource (field ExploitationAuthorizations).
// They are NOT a separate MongoDB collection — each resource document carries
// its own AEs, just like it carries its Instances.
//
// # Visibility filtering
//
// When a resource is returned to a consumer peer the AE list is filtered:
//   - AllowedPeerIDs empty → public AE, visible to all peers.
//   - AllowedPeerIDs non-empty, contains requester → visible to that peer.
//   - AllowedPeerIDs non-empty, doesn't contain requester → stripped from response.
//
// The resource owner always sees all of their own AEs unfiltered.
//
// # Enforcement
//
// oc-schedulerd's validateWorkflowIntegrity calls CheckWorkflowAE (defined in
// its own package to avoid circular imports) before launching any execution.
// Violations emit PEER_BEHAVIOR_EVENT(BehaviorFraud) against the consumer peer
// and cause the execution to be rejected.

import (
	"encoding/json"
	"fmt"
	"slices"
	"time"

	"cloud.o-forge.io/core/oc-lib/tools"
)

// CouplingConstraint defines which resources must or must not co-exist in the
// same workflow when the protected resource is included.
type CouplingConstraint struct {
	// RequiredResourceIDs — ALL of these resource UUIDs must appear in the
	// workflow alongside the protected resource.
	RequiredResourceIDs []string `json:"required_resource_ids,omitempty" bson:"required_resource_ids,omitempty"`
	// ForbiddenResourceIDs — NONE of these resource UUIDs may appear in the
	// workflow alongside the protected resource.
	ForbiddenResourceIDs []string `json:"forbidden_resource_ids,omitempty" bson:"forbidden_resource_ids,omitempty"`
}

// ExploitationAuthorization (AE) is embedded in a resource and restricts how
// the resource may be used by other consumer peers.
//
// It is stored as part of the resource document (bson embedded), not as a
// separate collection.  Create/update it by PATCHing the parent resource.
type ExploitationAuthorization struct {
	// ID is a client-assigned UUID so individual AEs can be referenced.
	ID string `json:"id" bson:"id"`
	// Name is a human-readable label shown in the catalog detail view.
	Name string `json:"name,omitempty" bson:"name,omitempty"`

	// AllowedPeerIDs restricts which consumer peers may use the resource.
	// An empty list means any peer is allowed.
	AllowedPeerIDs []string `json:"allowed_peer_ids,omitempty" bson:"allowed_peer_ids,omitempty"`

	// AllowedWorkflowIDs restricts which workflow IDs may include the resource.
	// An empty list means any workflow is allowed.
	AllowedWorkflowIDs []string `json:"allowed_workflow_ids,omitempty" bson:"allowed_workflow_ids,omitempty"`

	// Coupling describes positive (required) and negative (forbidden) coupling.
	// Nil means no coupling constraint.
	Coupling *CouplingConstraint `json:"coupling,omitempty" bson:"coupling,omitempty"`

	// ValidFrom / ValidUntil define the active window.
	ValidFrom  *time.Time `json:"valid_from,omitempty" bson:"valid_from,omitempty"`
	ValidUntil *time.Time `json:"valid_until,omitempty" bson:"valid_until,omitempty"`

	// IsRevoked allows instant revocation without deleting the AE from the resource.
	IsRevoked bool `json:"is_revoked" bson:"is_revoked"`
}

// IsVisibleTo returns true when this AE should be included in the response to
// peerID. The resource owner (creatorID) always sees all AEs.
func (ae *ExploitationAuthorization) IsVisibleTo(peerID, creatorID string) bool {
	if peerID == creatorID {
		return true // owner sees everything
	}
	return len(ae.AllowedPeerIDs) == 0 || slices.Contains(ae.AllowedPeerIDs, peerID)
}

// CheckAE evaluates this AE against the execution context and returns any
// violations found.  workflowResourceIDs is the set of all resource UUIDs in
// the workflow; resourceID is the UUID of the resource this AE belongs to.
func (ae *ExploitationAuthorization) CheckAE(
	resourceID, workflowID, consumerPeerID string,
	workflowResourceIDs map[string]struct{},
	now time.Time,
) []AEViolation {
	var vs []AEViolation
	add := func(t AEViolationType, msg string) {
		vs = append(vs, AEViolation{AEID: ae.ID, ResourceID: resourceID, Type: t, Message: msg})
	}

	if ae.IsRevoked {
		add(AEViolationRevoked, fmt.Sprintf("AE %s for resource %s is revoked", ae.ID, resourceID))
		return vs
	}
	if ae.ValidUntil != nil && now.After(*ae.ValidUntil) {
		add(AEViolationExpired, fmt.Sprintf("AE %s for resource %s expired at %s",
			ae.ID, resourceID, ae.ValidUntil.Format(time.RFC3339)))
		return vs
	}
	if ae.ValidFrom != nil && now.Before(*ae.ValidFrom) {
		add(AEViolationNotYetValid, fmt.Sprintf("AE %s for resource %s not valid until %s",
			ae.ID, resourceID, ae.ValidFrom.Format(time.RFC3339)))
		return vs
	}
	if consumerPeerID != "" && len(ae.AllowedPeerIDs) > 0 {
		if !slices.Contains(ae.AllowedPeerIDs, consumerPeerID) {
			add(AEViolationPeerNotAllowed, fmt.Sprintf(
				"peer %s not allowed to use resource %s (AE %s)", consumerPeerID, resourceID, ae.ID))
		}
	}
	if workflowID != "" && len(ae.AllowedWorkflowIDs) > 0 {
		if !slices.Contains(ae.AllowedWorkflowIDs, workflowID) {
			add(AEViolationWorkflowNotAllow, fmt.Sprintf(
				"workflow %s not in allowed-workflow list for resource %s (AE %s)", workflowID, resourceID, ae.ID))
		}
	}
	if ae.Coupling != nil {
		for _, req := range ae.Coupling.RequiredResourceIDs {
			if _, ok := workflowResourceIDs[req]; !ok {
				add(AEViolationCouplingRequired, fmt.Sprintf(
					"resource %s requires %s to be present (AE %s)", resourceID, req, ae.ID))
			}
		}
		for _, forb := range ae.Coupling.ForbiddenResourceIDs {
			if _, ok := workflowResourceIDs[forb]; ok {
				add(AEViolationCouplingForbid, fmt.Sprintf(
					"resource %s forbids co-use with %s (AE %s)", resourceID, forb, ae.ID))
			}
		}
	}
	return vs
}

// ── Violation types ───────────────────────────────────────────────────────────

type AEViolationType string

const (
	AEViolationRevoked          AEViolationType = "ae_revoked"
	AEViolationExpired          AEViolationType = "ae_expired"
	AEViolationNotYetValid      AEViolationType = "ae_not_yet_valid"
	AEViolationPeerNotAllowed   AEViolationType = "ae_peer_not_allowed"
	AEViolationWorkflowNotAllow AEViolationType = "ae_workflow_not_allowed"
	AEViolationCouplingRequired AEViolationType = "ae_coupling_required"
	AEViolationCouplingForbid   AEViolationType = "ae_coupling_forbidden"
)

// AEViolation describes a single constraint that was not satisfied.
type AEViolation struct {
	AEID       string
	ResourceID string
	Type       AEViolationType
	Message    string
}

// ── NATS emit helper (uses tools only — no oclib circular import) ─────────────

// EmitAEBehaviorReport emits a PEER_BEHAVIOR_EVENT(BehaviorFraud) for each
// unique AE violation. Call this before rejecting the execution.
func EmitAEBehaviorReport(consumerPeerID string, violations []AEViolation) {
	if consumerPeerID == "" || len(violations) == 0 {
		return
	}
	seen := map[string]struct{}{}
	for _, v := range violations {
		key := v.AEID + ":" + v.ResourceID
		if _, dup := seen[key]; dup {
			continue
		}
		seen[key] = struct{}{}
		report := tools.PeerBehaviorReport{
			ReporterApp:  "oc-scheduler",
			TargetPeerID: consumerPeerID,
			Severity:     tools.BehaviorFraud,
			Reason:       fmt.Sprintf("AE violation (%s): %s", v.Type, v.Message),
			Evidence:     v.AEID,
			At:           time.Now().UTC(),
		}
		if b, err := json.Marshal(report); err == nil {
			tools.NewNATSCaller().SetNATSPub(tools.PEER_BEHAVIOR_EVENT, tools.NATSResponse{
				FromApp: "oc-scheduler",
				Method:  int(tools.PEER_BEHAVIOR_EVENT),
				Payload: b,
			})
		}
	}
}

// OriginType qualifies where a resource instance comes from.
type OriginType int

const (
	// OriginPeer: instance offered by a known network peer (default).
	OriginPeer OriginType = iota
	// OriginPublic: instance from a public registry (Docker Hub, HuggingFace, etc.).
	// No peer confirmation is needed; access is unrestricted.
	OriginPublic
	// OriginSelf: self-hosted instance with no third-party peer.
	OriginSelf
)

// OriginMeta carries provenance information for a resource instance.
type OriginMeta struct {
	Type     OriginType `json:"origin_type" bson:"origin_type"`
	Ref      string     `json:"origin_ref,omitempty" bson:"origin_ref,omitempty"` // e.g. "docker.io/pytorch/pytorch:2.1"
	Verified bool       `json:"origin_verified" bson:"origin_verified"`           // manually vetted by an OC admin
}

// IsPeerless MUST NOT be used for authorization decisions.
// Use ResourceInstance.IsPeerless() instead, which derives the property
// from structural invariants rather than this self-declared field.
//
// This method is kept only for display/logging purposes.
func (o OriginMeta) DeclaredPeerless() bool {
	return o.Type != OriginPeer
}
