@prefix : <https://ontology.inferal.com/modules/conversation/> .
@prefix prov: <http://www.w3.org/ns/prov#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

:ConversationShape
    a sh:NodeShape ;
    rdfs:label "conversation shape"@en ;
    rdfs:comment "Validates supplied conversation timestamps and participation values without imposing record or contribution completeness."@en ;
    sh:targetClass :Conversation ;
    sh:property [ sh:path :closedAt ; sh:maxCount 1 ] ;
    sh:property [ sh:path :hasParticipation ; sh:class :Participation ] .

:ParticipationShape
    a sh:NodeShape ;
    rdfs:label "participation shape"@en ;
    rdfs:comment "Requires a qualified participation to identify one conversation and one participant while accepting either direction of the conversation-participation inverse."@en ;
    sh:targetClass :Participation ;
    sh:property [
        sh:path [ sh:alternativePath ( :participationIn [ sh:inversePath :hasParticipation ] ) ] ;
        sh:class :Conversation ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
    ] ;
    sh:property [ sh:path :hasParticipant ; sh:class prov:Agent ; sh:minCount 1 ; sh:maxCount 1 ] ;
    sh:property [ sh:path :participationRole ; sh:class :ParticipantRole ] ;
    sh:property [ sh:path :beganAt ; sh:maxCount 1 ] ;
    sh:property [ sh:path :endedAt ; sh:maxCount 1 ] .

:ResponsibleParticipationShape
    a sh:NodeShape ;
    rdfs:label "responsible participation shape"@en ;
    rdfs:comment "Requires the responsible participation node to be the conversation's qualified PROV association and to identify the same agent and roles in both vocabularies."@en ;
    sh:targetClass :ResponsibleParticipation ;
    sh:property [ sh:path prov:agent ; sh:class prov:Agent ; sh:minCount 1 ; sh:maxCount 1 ] ;
    sh:property [
        sh:path [ sh:inversePath prov:qualifiedAssociation ] ;
        sh:class :Conversation ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
    ] ;
    sh:sparql [
        sh:message "convo:hasParticipant and prov:agent must identify the same agent on a responsible participation."@en ;
        sh:select """
            PREFIX convo: <https://ontology.inferal.com/modules/conversation/>
            PREFIX prov: <http://www.w3.org/ns/prov#>
            SELECT $this WHERE {
                $this convo:hasParticipant ?participant ; prov:agent ?agent .
                FILTER(?participant != ?agent)
            }
        """ ;
    ] ;
    sh:sparql [
        sh:message "The conversation identified by the participation must be the activity that uses it as prov:qualifiedAssociation."@en ;
        sh:select """
            PREFIX convo: <https://ontology.inferal.com/modules/conversation/>
            PREFIX prov: <http://www.w3.org/ns/prov#>
            SELECT $this WHERE {
                $this (convo:participationIn|^convo:hasParticipation) ?conversation .
                ?other prov:qualifiedAssociation $this .
                FILTER(?conversation != ?other)
            }
        """ ;
    ] ;
    sh:sparql [
        sh:message "Conversation and PROV roles must agree on a responsible participation."@en ;
        sh:select """
            PREFIX convo: <https://ontology.inferal.com/modules/conversation/>
            PREFIX prov: <http://www.w3.org/ns/prov#>
            SELECT $this ?role WHERE {
                { $this convo:participationRole ?role . FILTER NOT EXISTS { $this prov:hadRole ?role . } }
                UNION
                { $this prov:hadRole ?role . FILTER NOT EXISTS { $this convo:participationRole ?role . } }
            }
        """ ;
    ] .

:ContributionShape
    a sh:NodeShape ;
    rdfs:label "contribution shape"@en ;
    rdfs:comment "Validates supplied conversation membership and timestamps without restricting a contribution to one conversation, and prevents immediate self-replies."@en ;
    sh:targetClass :Contribution ;
    sh:property [
        sh:path [ sh:alternativePath ( :contributionOf [ sh:inversePath :hasContribution ] ) ] ;
        sh:class :Conversation ;
    ] ;
    sh:property [ sh:path :inReplyTo ; sh:class :Contribution ] ;
    sh:property [ sh:path :authoredAt ; sh:maxCount 1 ] ;
    sh:sparql [
        sh:message "A contribution cannot reply to itself."@en ;
        sh:select """
            PREFIX convo: <https://ontology.inferal.com/modules/conversation/>
            SELECT $this WHERE { $this convo:inReplyTo $this . }
        """ ;
    ] .

:ReactionShape
    a sh:NodeShape ;
    rdfs:label "reaction shape"@en ;
    rdfs:comment "Requires each semantic reaction to identify exactly one target and at least one open reaction type."@en ;
    sh:targetClass :Reaction ;
    sh:property [
        sh:path :reactsTo ;
        sh:nodeKind sh:BlankNodeOrIRI ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
    ] ;
    sh:property [
        sh:path :hasReactionType ;
        sh:class :ReactionType ;
        sh:minCount 1 ;
    ] .

:ReactionTypeShape
    a sh:NodeShape ;
    rdfs:label "reaction type shape"@en ;
    rdfs:comment "Requires every reaction type used as a controlled concept to have a preferred human label and validates any declared concept scheme."@en ;
    sh:targetClass :ReactionType ;
    sh:property [ sh:path skos:prefLabel ; sh:nodeKind sh:Literal ; sh:minCount 1 ] ;
    sh:property [ sh:path skos:inScheme ; sh:class skos:ConceptScheme ] .

:ThreadShape
    a sh:NodeShape ;
    rdfs:label "thread shape"@en ;
    rdfs:comment "Requires every structural thread to belong to exactly one conversation while accepting either direction of the inverse relation."@en ;
    sh:targetClass :Thread ;
    sh:property [
        sh:path [ sh:alternativePath ( :threadOfConversation [ sh:inversePath :hasThread ] ) ] ;
        sh:class :Conversation ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
    ] .

:ConversationRecordShape
    a sh:NodeShape ;
    rdfs:label "conversation record shape"@en ;
    rdfs:comment "Requires a record asserted in a validated graph to identify exactly one represented conversation."@en ;
    sh:targetClass :ConversationRecord ;
    sh:property [ sh:path :recordsConversation ; sh:class :Conversation ; sh:minCount 1 ; sh:maxCount 1 ] .

:ConversationStatusShape
    a sh:NodeShape ;
    rdfs:label "conversation status shape"@en ;
    rdfs:comment "Requires each controlled status value to identify exactly one operational, resolution, or retention dimension."@en ;
    sh:targetClass :ConversationStatus ;
    sh:property [ sh:path :hasStatusDimension ; sh:class :StatusDimension ; sh:minCount 1 ; sh:maxCount 1 ] .

:ConversationStatusAssignmentShape
    a sh:NodeShape ;
    rdfs:label "conversation status assignment shape"@en ;
    rdfs:comment "Validates qualified lifecycle-state assignments for either a conversation or a thread."@en ;
    sh:targetClass :ConversationStatusAssignment ;
    sh:property [
        sh:path :statusOf ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:or ( [ sh:class :Conversation ] [ sh:class :Thread ] ) ;
    ] ;
    sh:property [ sh:path :assignedStatus ; sh:class :ConversationStatus ; sh:minCount 1 ; sh:maxCount 1 ] ;
    sh:property [ sh:path :assignedAt ; sh:maxCount 1 ] .

:SameDimensionStatusWarningShape
    a sh:NodeShape ;
    rdfs:label "same-dimension status warning shape"@en ;
    rdfs:comment "Warns when one conversation or thread directly reports two distinct current statuses in the same semantic dimension."@en ;
    sh:targetSubjectsOf :hasStatus ;
    sh:severity sh:Warning ;
    sh:sparql [
        sh:message "Multiple direct statuses occupy the same dimension; use qualified status assignments when this represents disagreement or history."@en ;
        sh:select """
            PREFIX convo: <https://ontology.inferal.com/modules/conversation/>
            SELECT $this ?leftStatus ?rightStatus ?dimension WHERE {
                $this convo:hasStatus ?leftStatus, ?rightStatus .
                ?leftStatus convo:hasStatusDimension ?dimension .
                ?rightStatus convo:hasStatusDimension ?dimension .
                FILTER(?leftStatus != ?rightStatus)
                FILTER(STR(?leftStatus) < STR(?rightStatus))
            }
        """ ;
    ] .

:TombstoneShape
    a sh:NodeShape ;
    rdfs:label "tombstone shape"@en ;
    rdfs:comment "Requires a tombstone to identify exactly one unavailable contribution while permitting the original payload to remain absent."@en ;
    sh:targetClass :Tombstone ;
    sh:property [ sh:path :tombstoneFor ; sh:class :Contribution ; sh:minCount 1 ; sh:maxCount 1 ] .

:AccountShape
    a sh:NodeShape ;
    rdfs:label "account shape"@en ;
    rdfs:comment "Validates any supplied connection from an account to the agent it represents without requiring the represented agent to be known."@en ;
    sh:targetClass :Account ;
    sh:property [ sh:path :representsAgent ; sh:class prov:Agent ] .

:InstantTimestampShape
    a sh:NodeShape ;
    rdfs:label "instant timestamp shape"@en ;
    rdfs:comment "Validates conversation-owned instant-like timestamp values as dateTimeStamp or timezone-bearing dateTime literals."@en ;
    sh:targetSubjectsOf :beganAt, :endedAt, :assignedAt, :authoredAt, :sentAt, :deliveredAt, :receivedAt, :displayedAt, :closedAt ;
    sh:property [ sh:path :beganAt ; sh:node :InstantLiteralValueShape ] ;
    sh:property [ sh:path :endedAt ; sh:node :InstantLiteralValueShape ] ;
    sh:property [ sh:path :assignedAt ; sh:node :InstantLiteralValueShape ] ;
    sh:property [ sh:path :authoredAt ; sh:node :InstantLiteralValueShape ] ;
    sh:property [ sh:path :sentAt ; sh:node :InstantLiteralValueShape ] ;
    sh:property [ sh:path :deliveredAt ; sh:node :InstantLiteralValueShape ] ;
    sh:property [ sh:path :receivedAt ; sh:node :InstantLiteralValueShape ] ;
    sh:property [ sh:path :displayedAt ; sh:node :InstantLiteralValueShape ] ;
    sh:property [ sh:path :closedAt ; sh:node :InstantLiteralValueShape ] .

:InstantLiteralValueShape
    a sh:NodeShape ;
    rdfs:label "instant literal value shape"@en ;
    rdfs:comment "Accepts an xsd:dateTimeStamp or an xsd:dateTime lexical form ending in Z or an explicit numeric timezone offset."@en ;
    sh:message "Instant-like timestamps must be xsd:dateTimeStamp or an xsd:dateTime lexical form with an explicit timezone."@en ;
    sh:or (
        [ sh:datatype xsd:dateTimeStamp ]
        [ sh:datatype xsd:dateTime ; sh:pattern "(Z|[+-][0-9]{2}:[0-9]{2})$" ]
    ) .

:ContributionProvenanceTimeShape
    a sh:NodeShape ;
    rdfs:label "contribution provenance time shape"@en ;
    rdfs:comment "Applies the instant-literal policy to PROV generation and invalidation times on contributions."@en ;
    sh:targetClass :Contribution ;
    sh:property [ sh:path prov:generatedAtTime ; sh:node :InstantLiteralValueShape ] ;
    sh:property [ sh:path prov:invalidatedAtTime ; sh:node :InstantLiteralValueShape ] .

:RecordProvenanceTimeShape
    a sh:NodeShape ;
    rdfs:label "record provenance time shape"@en ;
    rdfs:comment "Applies the instant-literal policy to PROV generation and invalidation times on conversation records."@en ;
    sh:targetClass :ConversationRecord ;
    sh:property [ sh:path prov:generatedAtTime ; sh:node :InstantLiteralValueShape ] ;
    sh:property [ sh:path prov:invalidatedAtTime ; sh:node :InstantLiteralValueShape ] .

:ConversationProvenanceTimeShape
    a sh:NodeShape ;
    rdfs:label "conversation provenance time shape"@en ;
    rdfs:comment "Applies the instant-literal policy to PROV start and end times on conversation activities."@en ;
    sh:targetClass :Conversation ;
    sh:property [ sh:path prov:startedAtTime ; sh:node :InstantLiteralValueShape ] ;
    sh:property [ sh:path prov:endedAtTime ; sh:node :InstantLiteralValueShape ] .

:ContributionActProvenanceTimeShape
    a sh:NodeShape ;
    rdfs:label "contribution act provenance time shape"@en ;
    rdfs:comment "Applies the instant-literal policy to PROV start and end times on contribution-production activities."@en ;
    sh:targetClass :ContributionAct ;
    sh:property [ sh:path prov:startedAtTime ; sh:node :InstantLiteralValueShape ] ;
    sh:property [ sh:path prov:endedAtTime ; sh:node :InstantLiteralValueShape ] .

:TransportProvenanceTimeShape
    a sh:NodeShape ;
    rdfs:label "transport provenance time shape"@en ;
    rdfs:comment "Applies the instant-literal policy to PROV start and end times on transmission, delivery, and moderation activities."@en ;
    sh:targetClass :MessageTransmission, :Delivery, :ModerationAction ;
    sh:property [ sh:path prov:startedAtTime ; sh:node :InstantLiteralValueShape ] ;
    sh:property [ sh:path prov:endedAtTime ; sh:node :InstantLiteralValueShape ] .
