-- allium: 1
-- scheduled-publishing.allium

-- Scope: Scheduled blog post publishing and authenticated author dashboard
-- Includes: Post visibility rules, date-based scheduling, authentication,
--           registration gating, author dashboard
-- Excludes:
--   - Post editing / CMS (posts are markdown files in git)
--   - Role-based access (single role: authenticated user)
--   - Multi-tenancy
--   - Post creation workflow
-- Constraints: Posts are compiled from markdown at build time (NimblePublisher).
--   All filtering is runtime — no database for post content.

------------------------------------------------------------
-- External Entities
------------------------------------------------------------

-- Posts are compiled from markdown at build time, not managed by this spec
external entity Post {
    id: String
    title: String
    author: String
    body: String
    description: String
    date: Date
    tags: Set<String>
    blog: Blog
    published: Boolean
}

external entity Blog {
    blog_id: String
    title: String
    description: String
    base_path: String

    posts: Post with blog = this
}

------------------------------------------------------------
-- Enumerations
------------------------------------------------------------

enum PostVisibility { live | scheduled | draft }

------------------------------------------------------------
-- Entities and Variants
------------------------------------------------------------

entity User {
    email: String
    password_hash: String
    active_sessions: Session with user = this

    -- Derived
    is_authenticated: active_sessions.count > 0
}

entity Session {
    user: User
    token: String
}

------------------------------------------------------------
-- Value Types
------------------------------------------------------------

value PostWithVisibility {
    post: Post
    visibility: PostVisibility

    -- Derived
    days_until_live: if visibility = scheduled: post.date - today else: null
}

------------------------------------------------------------
-- Config
------------------------------------------------------------

config {
    allowed_registration_email: String?
}

------------------------------------------------------------
-- Defaults
------------------------------------------------------------

-- Dev-only seed user
default User demo_user = {
    email: "demo@example.com",
    password_hash: hash("password123")
}

------------------------------------------------------------
-- Rules
------------------------------------------------------------

-- Post visibility

rule DeterminePostVisibility {
    when: post: Post.created
    ensures:
        if not post.published:
            post.visibility = draft
        else if post.date > today:
            post.visibility = scheduled
        else:
            post.visibility = live
}

rule FilterPublicPosts {
    when: PublicPostsRequested(blog)
    ensures: PublicPostsResolved(
        posts: blog.posts where published and date <= today
    )
}

rule FilterPublicPostsByTag {
    when: PublicPostsByTagRequested(blog, tag)
    let matching = blog.posts where published and date <= today
    ensures: PublicPostsResolved(
        posts: matching where tag in tags
    )
}

rule ResolvePostForPublic {
    when: PostRequested(blog, slug)
    let post = blog.posts where id = slug
    requires: exists post
    ensures: PostResolved(post: post)

    -- No date/published filter — direct URL access always works
    guidance:
        -- Direct URL access is intentionally unfiltered so authors
        -- can share preview links with reviewers before publish date
}

rule GenerateFeed {
    when: FeedRequested(blog)
    ensures: FeedResolved(
        posts: blog.posts where published and date <= today
    )
}

-- Authentication

rule UserRegisters {
    when: RegisterRequested(email, password)
    requires: config.allowed_registration_email != null
    requires: email = config.allowed_registration_email
    ensures: User.created(
        email: email,
        password_hash: hash(password)
    )
}

rule RegistrationRejectedWhenDisabled {
    when: RegisterRequested(email, password)
    requires: config.allowed_registration_email = null
    ensures: RegistrationRejected(reason: "registration is invite only.")
}

rule RegistrationRejectedWhenEmailNotAllowed {
    when: RegisterRequested(email, password)
    requires: config.allowed_registration_email != null
    requires: email != config.allowed_registration_email
    ensures: RegistrationRejected(reason: "registration is invite only.")
}

rule UserLogsIn {
    when: LoginRequested(email, password)
    let user = User{email}
    requires: exists user
    requires: verify(password, user.password_hash)
    ensures: Session.created(user: user)
}

rule UserLogsOut {
    when: LogoutRequested(session)
    ensures: not exists session
}

------------------------------------------------------------
-- Actor Declarations
------------------------------------------------------------

actor Author {
    identified_by: User where is_authenticated
}

------------------------------------------------------------
-- Surfaces
------------------------------------------------------------

surface PublicBlogIndex {
    facing visitor: User

    context blog: Blog

    let visible_posts = blog.posts where published and date <= today

    exposes:
        blog.title
        blog.description
        for post in visible_posts:
            post.title
            post.date
            post.author
            post.description
            post.tags

    provides:
        PublicPostsRequested(blog)

    related:
        PublicPostDetail(post) when post in visible_posts
        PublicBlogByTag(blog)
}

surface PublicPostDetail {
    facing visitor: User

    context post: Post

    exposes:
        post.title
        post.date
        post.author
        post.body
        post.description
        post.tags
        post.visibility

    guidance:
        -- Accessible by direct URL regardless of date or published status.
        -- This allows authors to share preview links with reviewers.
        -- When viewer is authenticated and post is not live:
        --   draft → banner: "Draft — not published"
        --   scheduled → banner: "This post is scheduled for {date}"
}

surface PublicBlogByTag {
    facing visitor: User

    context blog: Blog

    let visible_posts = blog.posts where published and date <= today

    exposes:
        for tag in blog.all_tags:
            tag
            for post in visible_posts where tag in post.tags:
                post.title
                post.date

    provides:
        PublicPostsByTagRequested(blog, tag)
}

surface PublicFeed {
    facing visitor: User

    context blog: Blog

    exposes:
        blog.title
        blog.description
        for post in blog.posts where published and date <= today:
            post.title
            post.date
            post.author
            post.description
            post.body

    provides:
        FeedRequested(blog)

    guidance:
        -- Serves RSS and Atom feeds.
        -- Only includes posts that are published and not future-dated.
        -- Feeds reflect the filtered list at request time; no cache-busting needed.
}

surface EditorDashboard {
    facing author: Author

    context blog: Blog

    let drafts = blog.posts where not published
    let scheduled = blog.posts where published and date > today

    exposes:
        blog.title
        for draft in drafts:
            draft.title
            draft.date
            draft.author
            draft.blog
            draft.visibility
        for scheduled_post in scheduled:
            scheduled_post.title
            scheduled_post.date
            scheduled_post.author
            scheduled_post.blog
            scheduled_post.visibility
            scheduled_post.days_until_live

    guidance:
        -- Dashboard at /editor/dashboard.
        -- Drafts and scheduled posts shown in separate tabs.
        -- Unified timeline across all blogs (not grouped by blog).
        -- Drafts sorted by date descending.
        -- Scheduled posts sorted by date ascending (soonest first).
        -- Scheduled posts show "X days until live" countdown.

    related:
        PublicPostDetail(post) when post in drafts or post in scheduled
}

surface Login {
    facing visitor: User

    provides:
        LoginRequested(email, password)

    guidance:
        -- Login page is not linked from public navigation.
        -- Authors access it directly by URL.
}

surface Registration {
    facing visitor: User

    provides:
        RegisterRequested(email, password)

    guidance:
        -- Registration page is not linked from public navigation.
        -- Rejects with "registration is invite only." when email does not
        -- match ALLOWED_REGISTRATION_EMAIL or when the env var is unset.
}

------------------------------------------------------------
-- Open Questions
------------------------------------------------------------

-- No open questions remaining
