# SHACL shapes for the Reiseplan vocabulary (https://reiseplan.app/ns#),
# describing the three resource shapes this app reads/writes in a Solid pod
# (see app/lib/rdf.ts, the single source of truth this file mirrors):
#
#   - rp:Trip        one full itinerary (days -> activities), rp/trips.ttl
#   - rp:TripStatus  set of "done" activity keys, rp/status.ttl
#   - rp:TripLikes   per-activity like sets keyed by liker WebID, rp/likes.ttl
#
# Referenced from the embedded Application Capability document
# (app/lib/applicationCapability.ts) via ac:resourceType / ac:shape.

@prefix sh:     <http://www.w3.org/ns/shacl#> .
@prefix xsd:    <http://www.w3.org/2001/XMLSchema#> .
@prefix rp:     <https://reiseplan.app/ns#> .
@prefix schema: <https://schema.org/> .
@prefix ical:   <http://www.w3.org/2002/12/cal/ical#> .
@prefix geo:    <http://www.w3.org/2003/01/geo/wgs84_pos#> .

# ==================== Trip ====================

rp:TripShape a sh:NodeShape ;
    sh:targetClass rp:Trip ;
    sh:property [
        sh:path rp:slug ; sh:datatype xsd:string ; sh:minCount 1 ; sh:maxCount 1 ;
        sh:message "Stable identifier for the trip, e.g. \"prag\"." ;
    ] ;
    sh:property [
        sh:path rp:kicker ; sh:datatype xsd:string ; sh:minCount 1 ; sh:maxCount 1 ;
    ] ;
    sh:property [
        sh:path schema:name ; sh:datatype xsd:string ; sh:minCount 1 ; sh:maxCount 1 ;
        sh:message "The trip's title." ;
    ] ;
    sh:property [
        sh:path rp:titleAccent ; sh:datatype xsd:string ; sh:maxCount 1 ;
    ] ;
    sh:property [
        sh:path rp:subtitle ; sh:datatype xsd:string ; sh:minCount 1 ; sh:maxCount 1 ;
    ] ;
    sh:property [
        sh:path rp:accent ; sh:datatype xsd:string ; sh:minCount 1 ; sh:maxCount 1 ;
        sh:message "A theme key used by the UI, e.g. \"amber\" or \"emerald\"." ;
    ] ;
    sh:property [
        sh:path rp:footer ; sh:datatype xsd:string ; sh:minCount 1 ; sh:maxCount 1 ;
    ] ;
    sh:property [
        sh:path rp:startDate ; sh:datatype xsd:date ; sh:maxCount 1 ;
        sh:message "Real calendar date of day 0 — day N's date is startDate + N (days are consecutive)." ;
    ] ;
    sh:property [
        sh:path rp:meta ; sh:node rp:MetaEntryShape ; sh:nodeKind sh:BlankNodeOrIRI ;
    ] ;
    sh:property [
        sh:path rp:info ; sh:node rp:InfoEntryShape ; sh:nodeKind sh:BlankNodeOrIRI ;
    ] ;
    sh:property [
        sh:path rp:day ; sh:node rp:DayShape ; sh:nodeKind sh:BlankNodeOrIRI ; sh:minCount 1 ;
    ] .

rp:MetaEntryShape a sh:NodeShape ;
    sh:property [ sh:path rp:label ; sh:datatype xsd:string ; sh:minCount 1 ; sh:maxCount 1 ] ;
    sh:property [ sh:path rp:value ; sh:datatype xsd:string ; sh:minCount 1 ; sh:maxCount 1 ] .

rp:InfoEntryShape a sh:NodeShape ;
    sh:property [ sh:path rp:label ; sh:datatype xsd:string ; sh:minCount 1 ; sh:maxCount 1 ] ;
    sh:property [ sh:path rp:value ; sh:datatype xsd:string ; sh:minCount 1 ; sh:maxCount 1 ] .

rp:DayShape a sh:NodeShape ;
    sh:property [
        sh:path rp:order ; sh:datatype xsd:integer ; sh:minCount 1 ; sh:maxCount 1 ;
        sh:message "0-based position — RDF doesn't preserve list order, so days/activities carry an explicit sequence number." ;
    ] ;
    sh:property [ sh:path rp:label ; sh:datatype xsd:string ; sh:minCount 1 ; sh:maxCount 1 ] ;
    sh:property [ sh:path rp:weekday ; sh:datatype xsd:string ; sh:minCount 1 ; sh:maxCount 1 ] ;
    sh:property [ sh:path schema:name ; sh:datatype xsd:string ; sh:minCount 1 ; sh:maxCount 1 ] ;
    sh:property [ sh:path rp:subtitle ; sh:datatype xsd:string ; sh:minCount 1 ; sh:maxCount 1 ] ;
    sh:property [ sh:path rp:tag ; sh:datatype xsd:string ; sh:minCount 1 ; sh:maxCount 1 ] ;
    sh:property [
        sh:path rp:activity ; sh:node rp:ActivityShape ; sh:nodeKind sh:BlankNodeOrIRI ;
    ] .

# An Activity is typed ical:Vevent (http://www.w3.org/2002/12/cal/ical#) — its
# title/description use ical:summary/ical:description rather than rp: terms,
# and it gets a best-effort ical:dtstart (xsd:dateTime) computed from the
# trip's rp:startDate + day offset + a parsed "HH:MM" out of rp:time, when
# both are available (see app/lib/rdf.ts: computeDtStart). rp:time itself is
# always kept as the free-form display label, since it's often approximate
# ("~14:00", "vor 9 Uhr") and isn't itself a valid xsd:time.
rp:ActivityShape a sh:NodeShape ;
    sh:targetClass ical:Vevent ;
    sh:property [
        sh:path rp:id ; sh:datatype xsd:string ; sh:maxCount 1 ;
        sh:message "Stable UUID, used as the events/ VEVENT export filename." ;
    ] ;
    sh:property [
        sh:path rp:order ; sh:datatype xsd:integer ; sh:minCount 1 ; sh:maxCount 1 ;
    ] ;
    sh:property [
        sh:path rp:time ; sh:datatype xsd:string ; sh:minCount 1 ; sh:maxCount 1 ;
        sh:message "Free-form time label, e.g. \"09:30\" or \"~14:00\" — not xsd:time, arrival times are often approximate." ;
    ] ;
    sh:property [
        sh:path ical:summary ; sh:datatype xsd:string ; sh:minCount 1 ; sh:maxCount 1 ;
        sh:message "The activity's title." ;
    ] ;
    sh:property [ sh:path ical:description ; sh:datatype xsd:string ; sh:maxCount 1 ] ;
    sh:property [
        sh:path ical:dtstart ; sh:datatype xsd:dateTime ; sh:maxCount 1 ;
        sh:message "Best-effort start datetime, only present when the trip has a startDate and rp:time contains a parseable HH:MM." ;
    ] ;
    sh:property [
        sh:path ical:dtend ; sh:datatype xsd:dateTime ; sh:maxCount 1 ;
        sh:message "Always dtstart + 30min (fixed default — there's no real end time in the source data)." ;
    ] ;
    sh:property [ sh:path rp:badge ; sh:datatype xsd:string ; sh:maxCount 1 ] ;
    sh:property [ sh:path rp:highlight ; sh:datatype xsd:boolean ; sh:maxCount 1 ] ;
    sh:property [
        sh:path geo:lat ; sh:datatype xsd:decimal ; sh:maxCount 1 ;
        sh:minInclusive -90 ; sh:maxInclusive 90 ;
        sh:message "Latitude in decimal degrees (WGS84)." ;
    ] ;
    sh:property [
        sh:path geo:long ; sh:datatype xsd:decimal ; sh:maxCount 1 ;
        sh:minInclusive -180 ; sh:maxInclusive 180 ;
        sh:message "Longitude in decimal degrees (WGS84)." ;
    ] ;
    sh:property [
        sh:path rp:mapsUrl ; sh:nodeKind sh:IRI ; sh:maxCount 1 ;
        sh:message "Link to the location on Google Maps or another map service." ;
    ] .

# ==================== TripStatus ====================
# One resource per pod, shared by everyone the trip is shared with (see /share);
# rp:done values are "<tripSlug>:<dayIndex>:<activityIndex>" position keys, not IRIs.

rp:TripStatusShape a sh:NodeShape ;
    sh:targetClass rp:TripStatus ;
    sh:property [
        sh:path rp:done ; sh:datatype xsd:string ;
        sh:pattern "^[^:]+:[0-9]+:[0-9]+$" ;
        sh:message "A done key is \"<tripSlug>:<dayIndex>:<activityIndex>\"." ;
    ] .

# ==================== TripLikes ====================
# One resource per pod; each rp:like blank node records one (key, liker) pair —
# a liker can only ever add/remove their own WebID (enforced by the app, not the shape).

rp:TripLikesShape a sh:NodeShape ;
    sh:targetClass rp:TripLikes ;
    sh:property [
        sh:path rp:like ; sh:node rp:LikeEntryShape ; sh:nodeKind sh:BlankNodeOrIRI ;
    ] .

rp:LikeEntryShape a sh:NodeShape ;
    sh:property [
        sh:path rp:key ; sh:datatype xsd:string ; sh:minCount 1 ; sh:maxCount 1 ;
        sh:pattern "^[^:]+:[0-9]+:[0-9]+$" ;
    ] ;
    sh:property [
        sh:path rp:liker ; sh:nodeKind sh:IRI ; sh:minCount 1 ; sh:maxCount 1 ;
        sh:message "The WebID of the person who liked this activity." ;
    ] .
