-- Skald v0.1 schema. Database is the source of truth; the writer is
-- generic tooling that knows nothing hardcoded about any specific
-- story. Every story is rows.
--
-- pgvector for embedding-based callback search across past prose;
-- pg_trgm for fuzzy character-name lookups.

CREATE EXTENSION IF NOT EXISTS vector;
CREATE EXTENSION IF NOT EXISTS pg_trgm;

-- One row per story (or per sequel). parent_story_id chains a
-- series; root_story_id is the head of the chain (denormalized for
-- cheap series scans).
CREATE TABLE stories (
    id                 UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    title              TEXT NOT NULL,
    status             TEXT NOT NULL DEFAULT 'seed'
                       CHECK (status IN (
                           'seed', 'draft', 'generating', 'cleaning',
                           'auditing', 'complete', 'failed'
                       )),
    prompt             TEXT,
    model              TEXT,
    parent_story_id    UUID REFERENCES stories(id) ON DELETE SET NULL,
    root_story_id      UUID REFERENCES stories(id) ON DELETE SET NULL,
    series_name        TEXT,
    word_count_target  INTEGER,
    word_count_actual  INTEGER NOT NULL DEFAULT 0,
    summary            TEXT,
    created_at         TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at         TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE INDEX idx_stories_parent ON stories(parent_story_id);
CREATE INDEX idx_stories_root   ON stories(root_story_id);
CREATE INDEX idx_stories_status ON stories(status);
CREATE INDEX idx_stories_series ON stories(series_name) WHERE series_name IS NOT NULL;

-- Characters: real (historical) or fictional. The bible blob is
-- decomposed enough to be searchable but the original prose blob
-- stays in key_facts for full fidelity.
CREATE TABLE characters (
    id                  UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    story_id            UUID NOT NULL REFERENCES stories(id) ON DELETE CASCADE,
    name                TEXT NOT NULL,
    kind                TEXT NOT NULL CHECK (kind IN ('real', 'fictional')),
    role                TEXT,
    voice_traits        TEXT,
    key_facts           TEXT NOT NULL,
    aliases             TEXT[] NOT NULL DEFAULT '{}',
    first_seen_chapter  INTEGER,
    state_at_latest     TEXT,
    created_at          TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE INDEX idx_characters_story        ON characters(story_id);
CREATE INDEX idx_characters_name_trgm    ON characters USING gin (name gin_trgm_ops);
CREATE INDEX idx_characters_story_kind   ON characters(story_id, kind);

-- Canon facts: everything that's bible-shaped but not a character.
-- Setting details, mystery threads, themes, rules, historical
-- anchors, fictional liberties, suggested hooks for sequels.
CREATE TABLE canon_facts (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    story_id        UUID NOT NULL REFERENCES stories(id) ON DELETE CASCADE,
    category        TEXT NOT NULL CHECK (category IN (
                        'setting', 'event', 'rule', 'theme',
                        'mystery', 'liberty', 'hook', 'historical_anchor'
                    )),
    title           TEXT NOT NULL,
    body            TEXT NOT NULL,
    weight          INTEGER NOT NULL DEFAULT 1,
    source_chapter  INTEGER,
    resolved        BOOLEAN NOT NULL DEFAULT false,
    created_at      TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE INDEX idx_canon_facts_story_category ON canon_facts(story_id, category);

-- Chapters: full prose body, stored in DB (markdown). One row per
-- chapter; UNIQUE(story_id, n) prevents duplicate insertion.
CREATE TABLE chapters (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    story_id        UUID NOT NULL REFERENCES stories(id) ON DELETE CASCADE,
    n               INTEGER NOT NULL,
    title           TEXT,
    body_md         TEXT NOT NULL,
    word_count      INTEGER NOT NULL DEFAULT 0,
    generated_at    TIMESTAMPTZ NOT NULL DEFAULT now(),
    UNIQUE (story_id, n)
);

CREATE INDEX idx_chapters_story ON chapters(story_id);

-- Per-chapter short summary. The writer pulls these instead of full
-- chapter prose when assembling context for a sequel — much cheaper
-- on tokens. Generated by a separate LLM pass after the chapter is
-- finished.
CREATE TABLE chapter_summaries (
    chapter_id      UUID PRIMARY KEY REFERENCES chapters(id) ON DELETE CASCADE,
    body            TEXT NOT NULL,
    generated_at    TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- Passages: paragraph-level prose with embedding vectors for
-- similarity search. Embeddings nullable so v0.1 import doesn't
-- require an embedding pass — we fill them in lazily when we
-- actually need semantic recall.
CREATE TABLE passages (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    chapter_id      UUID NOT NULL REFERENCES chapters(id) ON DELETE CASCADE,
    paragraph_n     INTEGER NOT NULL,
    body            TEXT NOT NULL,
    embedding       vector(1536),
    embedded_at     TIMESTAMPTZ,
    UNIQUE (chapter_id, paragraph_n)
);

CREATE INDEX idx_passages_chapter ON passages(chapter_id);
-- ivfflat index on `embedding` is deferred until we have data —
-- ivfflat requires training rows to build, and an empty-table
-- index degrades query plans. Add after first ~1k passages.

-- Every LLM call we make is logged. Useful for cost tracking,
-- forensics, "why is this chapter weird?" investigations.
CREATE TABLE generation_runs (
    id                      UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    story_id                UUID NOT NULL REFERENCES stories(id) ON DELETE CASCADE,
    kind                    TEXT NOT NULL CHECK (kind IN (
                                'gen', 'cleanup', 'audit',
                                'summary', 'embed'
                            )),
    clawdforge_session_id   TEXT,
    tokens_in               INTEGER,
    tokens_out              INTEGER,
    cost_estimate_cents     INTEGER,
    started_at              TIMESTAMPTZ NOT NULL DEFAULT now(),
    ended_at                TIMESTAMPTZ,
    status                  TEXT NOT NULL DEFAULT 'running'
                            CHECK (status IN ('running', 'succeeded', 'failed')),
    error                   TEXT
);

CREATE INDEX idx_generation_runs_story ON generation_runs(story_id);
CREATE INDEX idx_generation_runs_kind  ON generation_runs(kind);

-- Canon audit findings. Third-Opus reads parent + sequel + bible
-- and flags any continuity drift, character voice shift, retconned
-- facts, timeline contradictions.
CREATE TABLE audit_findings (
    id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    story_id    UUID NOT NULL REFERENCES stories(id) ON DELETE CASCADE,
    run_id      UUID REFERENCES generation_runs(id) ON DELETE SET NULL,
    severity    TEXT NOT NULL CHECK (severity IN ('info', 'warn', 'crit')),
    area        TEXT NOT NULL CHECK (area IN (
                    'character', 'continuity', 'tone',
                    'fact', 'timeline', 'other'
                )),
    body        TEXT NOT NULL,
    resolved    BOOLEAN NOT NULL DEFAULT false,
    created_at  TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE INDEX idx_audit_findings_story ON audit_findings(story_id);

-- Arbitrary user-applied labels. Genre, mood, status filters, etc.
CREATE TABLE tags (
    id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    story_id    UUID NOT NULL REFERENCES stories(id) ON DELETE CASCADE,
    name        TEXT NOT NULL,
    UNIQUE (story_id, name)
);

CREATE INDEX idx_tags_story ON tags(story_id);

-- Auto-touch stories.updated_at whenever anything changes on the
-- story row itself. Cascade-only — not triggered by child writes.
CREATE OR REPLACE FUNCTION touch_updated_at()
RETURNS TRIGGER AS $$
BEGIN
    NEW.updated_at = now();
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER stories_updated_at
    BEFORE UPDATE ON stories
    FOR EACH ROW
    EXECUTE FUNCTION touch_updated_at();
