@prefix sh:       <http://www.w3.org/ns/shacl#> .
@prefix rdf:      <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs:     <http://www.w3.org/2000/01/rdf-schema#> .
@prefix xsd:      <http://www.w3.org/2001/XMLSchema#> .
@prefix dcterms:  <http://purl.org/dc/terms/> .
@prefix dcmitype: <http://purl.org/dc/dcmitype/> .
@prefix skos:     <http://www.w3.org/2004/02/skos/core#> .
@prefix prov:     <http://www.w3.org/ns/prov#> .
@prefix dcat:     <http://www.w3.org/ns/dcat#> .
@prefix crm:      <http://www.cidoc-crm.org/cidoc-crm/> .
@prefix foaf:     <http://xmlns.com/foaf/0.1/> .
@prefix locrel:   <http://id.loc.gov/vocabulary/relators/> .
@prefix schema:   <https://schema.org/> .

@prefix rft:      <https://eugeniavd.github.io/magic_tagger/rdf/ontology#> .

# ============================================================
# 0) Reusable shapes
# ============================================================

rft:AttributionShape a sh:NodeShape ;
  sh:property [
    sh:path rdf:type ;
    sh:minCount 1 ;
    sh:hasValue prov:Attribution ;
    sh:message "Attribution node must have rdf:type prov:Attribution." ;
  ] ;
  sh:property [
    sh:path prov:agent ;
    sh:minCount 1 ;
    sh:nodeKind sh:IRI ;
    sh:node rft:AgentNodeShape ;
    sh:message "Attribution must have prov:agent (IRI) pointing to a valid Agent node." ;
  ] ;
  sh:property [
    sh:path prov:hadRole ;
    sh:minCount 1 ;
    sh:nodeKind sh:IRI ;
    sh:in ( locrel:nrt locrel:col ) ;
    sh:message "Attribution must have prov:hadRole in {locrel:nrt, locrel:col}." ;
  ] .

rft:PlaceShape a sh:NodeShape ;
  sh:property [
    sh:path rdf:type ;
    sh:minCount 1 ;
    sh:hasValue crm:E53_Place ;
    sh:message "Place must have rdf:type crm:E53_Place." ;
  ] ;
  sh:property [
    sh:path rdfs:label ;
    sh:minCount 1 ;
    sh:or ( [ sh:datatype xsd:string ] [ sh:datatype rdf:langString ] ) ;
    sh:message "Place must have rdfs:label (minCount=1) as string or langString." ;
  ] .

# ============================================================
# 1) Tale recordings
# ============================================================

rft:TaleRecordingShape a sh:NodeShape ;
  sh:targetClass rft:TaleRecording ;

  sh:property [
    sh:path rdf:type ;
    sh:minCount 1 ;
    sh:hasValue crm:E33_Linguistic_Object ;
    sh:message "TaleRecording should also be typed as crm:E33_Linguistic_Object." ;
  ] ;

  sh:property [
    sh:path dcterms:identifier ;
    sh:minCount 1 ;
    sh:datatype xsd:string ;
    sh:message "TaleRecording must have dcterms:identifier (minCount=1, xsd:string)." ;
  ] ;

  sh:property [
    sh:path prov:wasDerivedFrom ;
    sh:minCount 1 ;
    sh:maxCount 1 ;
    sh:nodeKind sh:IRI ;
    sh:class rft:TaleContent ;
    sh:message "TaleRecording must have exactly one prov:wasDerivedFrom pointing to rft:TaleContent." ;
  ] ;

  sh:sparql [
    a sh:SPARQLConstraint ;
    sh:message "TaleRecording must have dcterms:isPartOf a volume IRI (…/volume/…)." ;
    sh:select """
      SELECT $this WHERE {
        FILTER NOT EXISTS {
          $this dcterms:isPartOf ?v .
          FILTER(isIRI(?v))
          FILTER(CONTAINS(STR(?v), "/volume/"))
        }
      }
    """ ;
  ] ;

  sh:sparql [
    a sh:SPARQLConstraint ;
    sh:message "TaleRecording must have dcterms:isPartOf a dataset IRI (…/dataset/…)."
    ;
    sh:select """
      SELECT $this WHERE {
        FILTER NOT EXISTS {
          $this dcterms:isPartOf ?ds .
          FILTER(isIRI(?ds))
          FILTER(CONTAINS(STR(?ds), "/dataset/"))
        }
      }
    """ ;
  ] ;

  sh:property [
    sh:path dcterms:accessRights ;
    sh:minCount 1 ;
    sh:nodeKind sh:IRI ;
    sh:in ( rft:rights_open rft:rights_partly_anonymised ) ;
    sh:message "TaleRecording must have dcterms:accessRights pointing to a controlled rights individual." ;
  ] ;

  sh:property [
    sh:path dcterms:created ;
    sh:maxCount 1 ;
    sh:or (
      [ sh:datatype xsd:date ]
      [ sh:datatype xsd:gYearMonth ]
      [ sh:datatype xsd:gYear ]
    ) ;
    sh:message "If present, dcterms:created must be xsd:date, xsd:gYearMonth, or xsd:gYear." ;
  ] ;

  sh:property [
    sh:path dcterms:bibliographicCitation ;
    sh:or ( [ sh:datatype xsd:string ] [ sh:datatype rdf:langString ] ) ;
    sh:message "If present, dcterms:bibliographicCitation must be string or langString." ;
  ] ;

  sh:property [
    sh:path dcterms:description ;
    sh:or ( [ sh:datatype xsd:string ] [ sh:datatype rdf:langString ] ) ;
    sh:message "If present, dcterms:description must be string or langString." ;
  ] ;

  sh:property [
    sh:path dcterms:spatial ;
    sh:nodeKind sh:BlankNodeOrIRI ;
    sh:node rft:PlaceShape ;
    sh:message "If present, dcterms:spatial must point to a valid Place node." ;
  ] ;

  sh:property [
    sh:path prov:qualifiedAttribution ;
    sh:nodeKind sh:IRI ;
    sh:node rft:AttributionShape ;
    sh:message "If present, prov:qualifiedAttribution must point to a valid prov:Attribution node." ;
  ] ;

  sh:sparql [
    a sh:SPARQLConstraint ;
    sh:message "TaleRecording must have at least one narrator attribution (prov:hadRole locrel:nrt)." ;
    sh:select """
      SELECT $this WHERE {
        FILTER NOT EXISTS {
          $this prov:qualifiedAttribution ?att .
          ?att rdf:type prov:Attribution ;
               prov:hadRole locrel:nrt ;
               prov:agent ?n .
        }
      }
    """ ;
  ] ;

  sh:sparql [
    a sh:SPARQLConstraint ;
    sh:message "TaleRecording must have at least one collector attribution (prov:hadRole locrel:col)." ;
    sh:select """
      SELECT $this WHERE {
        FILTER NOT EXISTS {
          $this prov:qualifiedAttribution ?att .
          ?att rdf:type prov:Attribution ;
               prov:hadRole locrel:col ;
               prov:agent ?c .
        }
      }
    """ ;
  ] .

# ============================================================
# 2) Tale contents
# ============================================================

rft:TaleContentShape a sh:NodeShape ;
  sh:targetClass rft:TaleContent ;

  sh:property [
    sh:path rdf:type ;
    sh:minCount 1 ;
    sh:hasValue crm:E28_Conceptual_Object ;
    sh:message "TaleContent should also be typed as crm:E28_Conceptual_Object." ;
  ] ;

  sh:property [
    sh:path dcterms:subject ;
    sh:minCount 1 ;
    sh:nodeKind sh:IRI ;
    sh:class rft:TaleType ;
    sh:message "TaleContent must have at least one dcterms:subject pointing to rft:TaleType." ;
  ] ;

  sh:sparql [
    a sh:SPARQLConstraint ;
    sh:message "TaleContent should be referenced by at least one TaleRecording via prov:wasDerivedFrom." ;
    sh:select """
      SELECT $this WHERE {
        FILTER NOT EXISTS {
          ?rec prov:wasDerivedFrom $this .
        }
      }
    """ ;
  ] .

# ============================================================
# 3) Volumes
# ============================================================

rft:VolumeShape a sh:NodeShape ;
  sh:target [
    a sh:SPARQLTarget ;
    sh:select """
      SELECT ?this WHERE {
        ?this a dcterms:BibliographicResource .
        FILTER(isIRI(?this))
        FILTER(CONTAINS(STR(?this), "/volume/"))
      }
    """ ;
  ] ;

  sh:property [
    sh:path dcterms:identifier ;
    sh:minCount 1 ;
    sh:datatype xsd:string ;
    sh:message "Volume must have dcterms:identifier (minCount=1)." ;
  ] ;

  sh:property [
    sh:path dcterms:isPartOf ;
    sh:minCount 1 ;
    sh:nodeKind sh:IRI ;
    sh:class dcmitype:Collection ;
    sh:message "Volume must have dcterms:isPartOf pointing to a Collection." ;
  ] ;

  sh:property [
    sh:path rdfs:label ;
    sh:minCount 1 ;
    sh:or ( [ sh:datatype xsd:string ] [ sh:datatype rdf:langString ] ) ;
    sh:message "Volume must have rdfs:label (minCount=1)." ;
  ] ;

  sh:property [
    sh:path dcterms:creator ;
    sh:nodeKind sh:IRI ;
    sh:node rft:AgentNodeShape ;
    sh:message "If present, dcterms:creator must point to a valid Agent node." ;
  ] ;

  sh:property [
    sh:path foaf:page ;
    sh:nodeKind sh:IRI ;
    sh:message "If present, foaf:page must be an IRI." ;
  ] .

# ============================================================
# 4) Collections
# ============================================================

rft:CollectionShape a sh:NodeShape ;
  sh:targetClass dcmitype:Collection ;

  sh:property [
    sh:path dcterms:identifier ;
    sh:minCount 1 ;
    sh:datatype xsd:string ;
    sh:message "Collection must have dcterms:identifier (minCount=1)." ;
  ] ;

  sh:property [
    sh:path rdfs:label ;
    sh:minCount 1 ;
    sh:or ( [ sh:datatype rdf:langString ] [ sh:datatype xsd:string ] ) ;
    sh:message "Collection must have rdfs:label (minCount=1) as langString or string." ;
  ] ;

  sh:property [
    sh:path rdfs:seeAlso ;
    sh:nodeKind sh:IRI ;
    sh:message "If present, rdfs:seeAlso must be an IRI." ;
  ] .

# ============================================================
# 5) Tale type concepts
# ============================================================

rft:ATUConceptShape a sh:NodeShape ;
  sh:targetClass rft:TaleType ;

  sh:property [
    sh:path rdf:type ;
    sh:minCount 1 ;
    sh:hasValue skos:Concept ;
    sh:message "TaleType should also be typed as skos:Concept." ;
  ] ;

  sh:property [
    sh:path skos:prefLabel ;
    sh:minCount 1 ;
    sh:datatype rdf:langString ;
    sh:message "ATU Concept must have skos:prefLabel (minCount=1) with a language tag." ;
  ] ;

  sh:property [
    sh:path skos:notation ;
    sh:minCount 1 ;
    sh:datatype xsd:string ;
    sh:message "ATU Concept must have skos:notation (minCount=1, xsd:string)." ;
  ] ;

  sh:property [
    sh:path skos:inScheme ;
    sh:minCount 1 ;
    sh:hasValue rft:ATU_Scheme ;
    sh:message "ATU Concept must have skos:inScheme rft:ATU_Scheme." ;
  ] ;

  sh:sparql [
    a sh:SPARQLConstraint ;
    sh:message "ATU Concept must have dcterms:source pointing to a bibliographic source IRI (…/biblio/…)." ;
    sh:select """
      SELECT $this WHERE {
        FILTER NOT EXISTS {
          $this dcterms:source ?s .
          FILTER(isIRI(?s))
          FILTER(CONTAINS(STR(?s), "/biblio/"))
        }
      }
    """ ;
  ] ;

  sh:property [
    sh:path skos:definition ;
    sh:datatype rdf:langString ;
    sh:message "If present, skos:definition must be langString." ;
  ] .

# ============================================================
# 6) Agents
# ============================================================

rft:AgentNodeShape a sh:NodeShape ;
  sh:targetClass prov:Agent ;

  sh:property [
    sh:path rdf:type ;
    sh:minCount 1 ;
    sh:hasValue foaf:Person ;
    sh:message "Agent should also be typed as foaf:Person." ;
  ] ;

  sh:property [
    sh:path dcterms:identifier ;
    sh:minCount 1 ;
    sh:datatype xsd:string ;
    sh:message "Agent must have dcterms:identifier (minCount=1, xsd:string)." ;
  ] ;

  sh:property [
    sh:path rdfs:label ;
    sh:minCount 1 ;
    sh:or ( [ sh:datatype xsd:string ] [ sh:datatype rdf:langString ] ) ;
    sh:message "Agent must have rdfs:label (minCount=1) as string or langString." ;
  ] ;

  sh:property [
    sh:path schema:birthDate ;
    sh:or (
      [ sh:datatype xsd:date ]
      [ sh:datatype xsd:gYearMonth ]
      [ sh:datatype xsd:gYear ]
    ) ;
    sh:message "If present, schema:birthDate must be xsd:date, xsd:gYearMonth, or xsd:gYear." ;
  ] ;

  sh:property [
    sh:path rft:ageAtRecording ;
    sh:datatype xsd:integer ;
    sh:message "If present, rft:ageAtRecording must be xsd:integer." ;
  ] ;

  sh:property [
    sh:path rdfs:comment ;
    sh:or ( [ sh:datatype xsd:string ] [ sh:datatype rdf:langString ] ) ;
    sh:message "If present, rdfs:comment must be string or langString." ;
  ] .

# ============================================================
# 7) Bibliographic sources
# ============================================================

rft:BiblioSourceShape a sh:NodeShape ;
  sh:target [
    a sh:SPARQLTarget ;
    sh:select """
      SELECT ?this WHERE {
        ?this a dcterms:BibliographicResource .
        FILTER(isIRI(?this))
        FILTER(CONTAINS(STR(?this), "/biblio/"))
      }
    """ ;
  ] ;

  sh:property [
    sh:path rdfs:label ;
    sh:minCount 1 ;
    sh:datatype rdf:langString ;
    sh:message "Bibliographic source must have rdfs:label (minCount=1, langString)." ;
  ] ;

  sh:property [
    sh:path dcterms:bibliographicCitation ;
    sh:or ( [ sh:datatype xsd:string ] [ sh:datatype rdf:langString ] ) ;
    sh:message "If present, dcterms:bibliographicCitation must be string or langString." ;
  ] ;

  sh:property [
    sh:path dcterms:identifier ;
    sh:datatype xsd:string ;
    sh:message "If present, dcterms:identifier must be xsd:string." ;
  ] ;

  sh:property [
    sh:path rdfs:seeAlso ;
    sh:nodeKind sh:IRI ;
    sh:message "If present, rdfs:seeAlso must be an IRI." ;
  ] ;

  sh:property [
    sh:path dcterms:isPartOf ;
    sh:nodeKind sh:IRI ;
    sh:message "If present, dcterms:isPartOf must be an IRI." ;
  ] ;

  sh:property [
    sh:path dcterms:hasPart ;
    sh:nodeKind sh:IRI ;
    sh:message "If present, dcterms:hasPart must be an IRI." ;
  ] .

# ============================================================
# 8) Dataset / distributions
# ============================================================

rft:DatasetShape a sh:NodeShape ;
  sh:targetClass dcat:Dataset ;

  sh:property [
    sh:path dcterms:title ;
    sh:minCount 1 ;
    sh:datatype rdf:langString ;
    sh:message "Dataset must have dcterms:title (minCount=1, langString)." ;
  ] ;

  sh:property [
    sh:path dcterms:description ;
    sh:minCount 1 ;
    sh:datatype rdf:langString ;
    sh:message "Dataset must have dcterms:description (minCount=1, langString)." ;
  ] ;

  sh:property [
    sh:path dcterms:creator ;
    sh:minCount 1 ;
    sh:nodeKind sh:IRI ;
    sh:message "Dataset must have dcterms:creator (minCount=1, IRI)." ;
  ] ;

  sh:property [
    sh:path dcterms:publisher ;
    sh:minCount 1 ;
    sh:nodeKind sh:IRI ;
    sh:message "Dataset must have dcterms:publisher (minCount=1, IRI)." ;
  ] ;

  sh:property [
    sh:path dcterms:license ;
    sh:minCount 1 ;
    sh:nodeKind sh:IRI ;
    sh:message "Dataset must have dcterms:license (minCount=1, IRI)." ;
  ] ;

  sh:property [
    sh:path dcterms:issued ;
    sh:datatype xsd:gYear ;
    sh:message "If present, dcterms:issued must be xsd:gYear." ;
  ] ;

  sh:property [
    sh:path dcterms:source ;
    sh:nodeKind sh:IRI ;
    sh:message "If present, dcterms:source must be an IRI." ;
  ] ;

  sh:property [
    sh:path dcterms:accessRights ;
    sh:nodeKind sh:IRI ;
    sh:message "If present, dcterms:accessRights must be an IRI." ;
  ] ;

  sh:property [
    sh:path dcat:distribution ;
    sh:minCount 1 ;
    sh:nodeKind sh:IRI ;
    sh:node rft:DistributionShape ;
    sh:message "Dataset must have at least one valid dcat:distribution." ;
  ] .

rft:DistributionShape a sh:NodeShape ;
  sh:targetClass dcat:Distribution ;

  sh:property [
    sh:path dcterms:title ;
    sh:minCount 1 ;
    sh:datatype rdf:langString ;
    sh:message "Distribution must have dcterms:title (minCount=1, langString)." ;
  ] ;

  sh:property [
    sh:path dcterms:format ;
    sh:minCount 1 ;
    sh:datatype xsd:string ;
    sh:message "Distribution must have dcterms:format (minCount=1, xsd:string)." ;
  ] ;

  sh:property [
    sh:path dcat:accessURL ;
    sh:minCount 1 ;
    sh:nodeKind sh:IRI ;
    sh:message "Distribution must have dcat:accessURL (minCount=1, IRI)." ;
  ] ;

  sh:property [
    sh:path dcat:downloadURL ;
    sh:nodeKind sh:IRI ;
    sh:message "If present, dcat:downloadURL must be an IRI." ;
  ] .