@prefix p: <https://ontology.inferal.com/profiles/relay-advertising/microsoftads/> .
@prefix ms: <https://relay.inferal.com/ontology/microsoftads/> .
@prefix kr: <https://relay.inferal.com/ontology/microsoftads/property/Microsoftads.CampaignManagementGetKeywordsByAdGroupIdRequest/> .
@prefix ks: <https://relay.inferal.com/ontology/microsoftads/property/Microsoftads.CampaignManagementGetKeywordsByAdGroupIdResponse/> .
@prefix kp: <https://relay.inferal.com/ontology/microsoftads/property/Microsoftads.CampaignManagementKeyword/> .
@prefix bp: <https://relay.inferal.com/ontology/microsoftads/property/Microsoftads.CampaignManagementBid/> .
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

p:KeywordRetrievalShape a sh:NodeShape ; sh:targetClass p:KeywordRetrieval ;
    rdfs:label "native keyword retrieval shape" ;
    rdfs:comment "Requires one correlated request, response, qualified parent retrieval and timezone-bearing capture instant. Empty responses and absent configuration fields remain valid; invalid context or conflicting scalar values are rejected." ;
    sh:nodeKind sh:IRI ;
    sh:property [ sh:path p:adGroupRetrieval ; sh:minCount 1 ; sh:maxCount 1 ; sh:class p:AdGroupRetrieval ; sh:nodeKind sh:IRI ] ;
    sh:property [ sh:path p:request ; sh:minCount 1 ; sh:maxCount 1 ; sh:class ms:CampaignManagementGetKeywordsByAdGroupIdRequest ; sh:nodeKind sh:IRI ; sh:node p:KeywordRequestShape ] ;
    sh:property [ sh:path p:response ; sh:minCount 1 ; sh:maxCount 1 ; sh:class ms:CampaignManagementGetKeywordsByAdGroupIdResponse ; sh:nodeKind sh:IRI ; sh:node p:KeywordResponseShape ] ;
    sh:property [ sh:path p:observedAt ; sh:minCount 1 ; sh:maxCount 1 ; sh:datatype xsd:dateTime ; sh:pattern "^[1-9][0-9]{3}-[0-9]{2}-[0-9]{2}T([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9](\\.[0-9]{1,6})?(Z|[+-](0[0-9]|1[0-3]):[0-5][0-9]|[+-]14:00)$" ] ;
    sh:sparql [ sh:message "Keyword and parent retrievals must have the same capture instant." ; sh:select """
      PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
      PREFIX p: <https://ontology.inferal.com/profiles/relay-advertising/microsoftads/>
      SELECT $this WHERE { $this p:observedAt ?seen ; p:adGroupRetrieval/p:observedAt ?parentSeen . FILTER((isLiteral(?seen) &&
      DATATYPE(?seen) = xsd:dateTime &&
      REGEX(STR(?seen), "^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]([.][0-9]+)?(Z|[+-]((0[0-9]|1[0-3]):[0-5][0-9]|14:00))$") &&
      YEAR(xsd:dateTime(REPLACE(STR(?seen), "[.][0-9]+(Z|[+-][0-9]{2}:[0-9]{2})$", "$1"))) >= 1 &&
      isLiteral(?parentSeen) &&
      DATATYPE(?parentSeen) = xsd:dateTime &&
      REGEX(STR(?parentSeen), "^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]([.][0-9]+)?(Z|[+-]((0[0-9]|1[0-3]):[0-5][0-9]|14:00))$") &&
      YEAR(xsd:dateTime(REPLACE(STR(?parentSeen), "[.][0-9]+(Z|[+-][0-9]{2}:[0-9]{2})$", "$1"))) >= 1 &&
      (xsd:dateTime(REPLACE(STR(?seen), "[.][0-9]+(Z|[+-][0-9]{2}:[0-9]{2})$", "$1")) != xsd:dateTime(REPLACE(STR(?parentSeen), "[.][0-9]+(Z|[+-][0-9]{2}:[0-9]{2})$", "$1")) ||
      IF(REGEX(STR(?seen), "[.][0-9]+(Z|[+-][0-9]{2}:[0-9]{2})$"), xsd:decimal(CONCAT("0.", REPLACE(STR(?seen), "^.*[.]([0-9]+)(Z|[+-][0-9]{2}:[0-9]{2})$", "$1"))), "0"^^xsd:decimal) != IF(REGEX(STR(?parentSeen), "[.][0-9]+(Z|[+-][0-9]{2}:[0-9]{2})$"), xsd:decimal(CONCAT("0.", REPLACE(STR(?parentSeen), "^.*[.]([0-9]+)(Z|[+-][0-9]{2}:[0-9]{2})$", "$1"))), "0"^^xsd:decimal)))) }
    """ ] .

p:KeywordRequestShape a sh:NodeShape ;
    rdfs:label "keyword retrieval request shape" ; rdfs:comment "Requires one exact positive native ad-group identifier." ;
    sh:property [ sh:path kr:adGroupId ; sh:minCount 1 ; sh:maxCount 1 ; sh:datatype xsd:long ; sh:minInclusive 1 ] .
p:KeywordResponseShape a sh:NodeShape ;
    rdfs:label "keyword retrieval response shape" ; rdfs:comment "Validates each explicitly returned keyword; an empty array does not invent a missing keyword." ;
    sh:property [ sh:path ks:keywords ; sh:class ms:CampaignManagementKeyword ; sh:node p:ReturnedKeywordShape ] .
p:ReturnedKeywordShape a sh:NodeShape ; sh:nodeKind sh:IRI ;
    rdfs:label "returned keyword configuration shape" ; rdfs:comment "Requires exact keyword identity and validates only reported configuration fields; missing bid amount or statuses are allowed. Status tokens remain source evidence rather than a serving inference." ;
    sh:property [ sh:path kp:id ; sh:minCount 1 ; sh:maxCount 1 ; sh:datatype xsd:long ; sh:minInclusive 1 ] ;
    sh:property [ sh:path kp:text ; sh:maxCount 1 ; sh:datatype xsd:string ] ;
    sh:property [ sh:path kp:matchType ; sh:maxCount 1 ; sh:datatype xsd:string ] ;
    sh:property [ sh:path kp:status ; sh:maxCount 1 ; sh:datatype xsd:string ] ;
    sh:property [ sh:path kp:editorialStatus ; sh:maxCount 1 ; sh:datatype xsd:string ] ;
    sh:property [ sh:path kp:bid ; sh:maxCount 1 ; sh:class ms:CampaignManagementBid ; sh:node p:ExplicitKeywordBidShape ] .
p:ExplicitKeywordBidShape a sh:NodeShape ;
    rdfs:label "explicit keyword bid configuration shape" ; rdfs:comment "Allows a bid whose Amount is null or absent, otherwise requires one finite nonnegative native double; it does not resolve inheritance or determine effective bids." ;
    sh:property [ sh:path bp:amount ; sh:maxCount 1 ; sh:datatype xsd:double ; sh:minInclusive 0 ; sh:maxInclusive "1.7976931348623157E308"^^xsd:double ; sh:pattern "^[+-]?([0-9]+(\\.[0-9]*)?|\\.[0-9]+)([eE][+-]?[0-9]+)?$" ] .
