-- pgmnemo upgrade: 0.1.4 → 0.2.0
-- Delivers three v0.2.0 primitives (RFC §3–§5):
--   § 3 mem_edge DDL — directed typed edges between agent_lesson rows
--   § 4 traverse_causal_chain() — recursive CTE walk of CAUSED_BY graph
--   § 5 traverse_temporal_window() — co-temporal episode discovery
-- RFC reference: spec/v2/pgmnemo/PGMNEMO_V0.2.0_RFC.md
-- SPDX-License-Identifier: Apache-2.0

-- ─────────────────────────────────────────────────────────────────
-- § 3  pgmnemo.mem_edge DDL
-- ─────────────────────────────────────────────────────────────────

CREATE TABLE IF NOT EXISTS pgmnemo.mem_edge (
    id              BIGSERIAL       PRIMARY KEY,
    source_id       BIGINT          NOT NULL
                        REFERENCES pgmnemo.agent_lesson(id) ON DELETE CASCADE,
    target_id       BIGINT          NOT NULL
                        REFERENCES pgmnemo.agent_lesson(id) ON DELETE CASCADE,
    relation_type   TEXT            NOT NULL,
    -- CAUSED_BY | SUPERSEDES | CO_OCCURRED | DERIVED_FROM | user-defined

    -- Bitemporality: dual valid-time + transaction-time temporal model
    valid_from      TIMESTAMPTZ     NOT NULL DEFAULT now(),
    valid_until     TIMESTAMPTZ,               -- NULL = currently valid

    -- Edge strength/confidence [0.0, 1.0]
    weight          REAL            NOT NULL DEFAULT 1.0
                        CHECK (weight BETWEEN 0.0 AND 1.0),

    -- Provenance — required by extension gate discipline
    commit_sha      TEXT,
    metadata        JSONB,

    created_at      TIMESTAMPTZ     NOT NULL DEFAULT now(),
    updated_at      TIMESTAMPTZ     NOT NULL DEFAULT now(),

    CONSTRAINT uq_mem_edge UNIQUE (source_id, target_id, relation_type, valid_from),
    CONSTRAINT ck_no_self_loop CHECK (source_id <> target_id)
);

COMMENT ON TABLE pgmnemo.mem_edge IS
    'Directed typed edges between agent_lesson rows. '
    'Built-in relation_type vocabulary: CAUSED_BY, SUPERSEDES, CO_OCCURRED, DERIVED_FROM. '
    'User-defined types are allowed (no enum constraint). '
    'valid_until IS NULL = currently valid edge.';

-- Forward traversal: source → all outbound edges of a given type
CREATE INDEX IF NOT EXISTS pgmnemo_mem_edge_source_type_idx
    ON pgmnemo.mem_edge (source_id, relation_type)
    WHERE valid_until IS NULL;

-- Reverse traversal: target → all inbound edges
CREATE INDEX IF NOT EXISTS pgmnemo_mem_edge_target_type_idx
    ON pgmnemo.mem_edge (target_id, relation_type)
    WHERE valid_until IS NULL;

-- Temporal range scans (traverse_temporal_window)
CREATE INDEX IF NOT EXISTS pgmnemo_mem_edge_valid_range_idx
    ON pgmnemo.mem_edge (valid_from, valid_until);

CREATE TRIGGER mem_edge_updated_at
    BEFORE UPDATE ON pgmnemo.mem_edge
    FOR EACH ROW EXECUTE FUNCTION pgmnemo._set_updated_at();

-- ─────────────────────────────────────────────────────────────────
-- § 4  traverse_causal_chain()
-- ─────────────────────────────────────────────────────────────────

CREATE OR REPLACE FUNCTION pgmnemo.traverse_causal_chain(
    start_id        BIGINT,
    max_depth       INT     DEFAULT 5,
    relation_types  TEXT[]  DEFAULT ARRAY['CAUSED_BY'],
    only_active     BOOLEAN DEFAULT TRUE
)
RETURNS TABLE (
    lesson_id       BIGINT,
    depth           INT,
    path            BIGINT[],
    path_weight     REAL,
    role            TEXT,
    topic           TEXT,
    lesson_text     TEXT,
    importance      SMALLINT,
    created_at      TIMESTAMPTZ,
    commit_sha      TEXT,
    verified_at     TIMESTAMPTZ
)
LANGUAGE plpgsql
STABLE
PARALLEL SAFE
AS $$
BEGIN
    -- Fail-safe: return zero rows if start_id does not exist
    IF NOT EXISTS (SELECT 1 FROM pgmnemo.agent_lesson WHERE id = start_id) THEN
        RETURN;
    END IF;

    RETURN QUERY
    WITH RECURSIVE chain AS (
        -- Base case: start node
        SELECT
            start_id            AS c_lesson_id,
            0                   AS c_depth,
            ARRAY[start_id]     AS c_path,
            1.0::REAL           AS c_path_weight
        UNION ALL
        -- Recursive step: follow outbound edges
        SELECT
            e.target_id,
            c.c_depth + 1,
            c.c_path || e.target_id,
            c.c_path_weight * e.weight
        FROM chain c
        JOIN pgmnemo.mem_edge e ON e.source_id = c.c_lesson_id
        WHERE e.relation_type = ANY(relation_types)
          AND (NOT only_active OR e.valid_until IS NULL)
          AND c.c_depth < max_depth
          AND NOT (e.target_id = ANY(c.c_path))  -- cycle guard
    )
    SELECT
        c.c_lesson_id,
        c.c_depth,
        c.c_path,
        c.c_path_weight,
        al.role,
        al.topic,
        al.lesson_text,
        al.importance,
        al.created_at,
        al.commit_sha,
        al.verified_at
    FROM chain c
    JOIN pgmnemo.agent_lesson al ON al.id = c.c_lesson_id
    WHERE c.c_lesson_id <> start_id
    ORDER BY c.c_depth, c.c_path_weight DESC;
END;
$$;

COMMENT ON FUNCTION pgmnemo.traverse_causal_chain(BIGINT, INT, TEXT[], BOOLEAN) IS
    'Walk the directed CAUSED_BY (or caller-specified) edge graph from start_id up to max_depth hops. '
    'Returns all lessons in the chain excluding the start node. '
    'Cycle-safe via accumulated path array. Fail-safe: returns zero rows if start_id missing.';

-- ─────────────────────────────────────────────────────────────────
-- § 5  traverse_temporal_window()
-- ─────────────────────────────────────────────────────────────────

CREATE OR REPLACE FUNCTION pgmnemo.traverse_temporal_window(
    start_id            BIGINT,
    window_interval     INTERVAL    DEFAULT INTERVAL '15 minutes',
    include_unlinked    BOOLEAN     DEFAULT TRUE,
    role_filter         TEXT        DEFAULT NULL,
    project_id_filter   INT         DEFAULT NULL,
    k                   INT         DEFAULT 20
)
RETURNS TABLE (
    lesson_id       BIGINT,
    time_delta_sec  DOUBLE PRECISION,
    linked          BOOLEAN,
    edge_weight     REAL,
    role            TEXT,
    topic           TEXT,
    lesson_text     TEXT,
    importance      SMALLINT,
    created_at      TIMESTAMPTZ,
    commit_sha      TEXT,
    verified_at     TIMESTAMPTZ
)
LANGUAGE plpgsql
STABLE
PARALLEL SAFE
AS $$
DECLARE
    _start_ts           TIMESTAMPTZ;
    _include_unverified BOOLEAN;
BEGIN
    -- Reference anchor: read created_at for start_id
    SELECT al.created_at INTO _start_ts
    FROM pgmnemo.agent_lesson al
    WHERE al.id = start_id;

    -- Fail-safe: start_id not found → return zero rows
    IF NOT FOUND THEN
        RETURN;
    END IF;

    -- Ghost-lesson exclusion: honour pgmnemo.include_unverified GUC
    _include_unverified := COALESCE(
        (current_setting('pgmnemo.include_unverified', true) = 'on'),
        FALSE
    );

    RETURN QUERY
    WITH candidates AS (
        SELECT
            al.id,
            al.role,
            al.topic,
            al.lesson_text,
            al.importance,
            al.created_at,
            al.commit_sha,
            al.verified_at,
            ABS(EXTRACT(EPOCH FROM (al.created_at - _start_ts))) AS delta_sec
        FROM pgmnemo.agent_lesson al
        WHERE al.is_active
          AND al.id <> start_id
          AND al.created_at BETWEEN (_start_ts - window_interval)
                                AND (_start_ts + window_interval)
          AND (_include_unverified OR al.verified_at IS NOT NULL)
          AND (role_filter         IS NULL OR al.role       = role_filter)
          AND (project_id_filter   IS NULL OR al.project_id = project_id_filter)
    ),
    -- Edges in either direction between start_id and each candidate
    edges AS (
        SELECT e.target_id AS other_id, e.weight
        FROM pgmnemo.mem_edge e
        WHERE e.source_id = start_id
          AND e.valid_until IS NULL
        UNION ALL
        SELECT e.source_id AS other_id, e.weight
        FROM pgmnemo.mem_edge e
        WHERE e.target_id = start_id
          AND e.valid_until IS NULL
    )
    SELECT
        c.id                            AS lesson_id,
        c.delta_sec                     AS time_delta_sec,
        (e.weight IS NOT NULL)          AS linked,
        e.weight                        AS edge_weight,
        c.role,
        c.topic,
        c.lesson_text,
        c.importance,
        c.created_at,
        c.commit_sha,
        c.verified_at
    FROM candidates c
    LEFT JOIN edges e ON e.other_id = c.id
    WHERE include_unlinked OR e.weight IS NOT NULL
    ORDER BY c.delta_sec ASC, c.importance DESC
    LIMIT k;
END;
$$;

COMMENT ON FUNCTION pgmnemo.traverse_temporal_window(BIGINT, INTERVAL, BOOLEAN, TEXT, INT, INT) IS
    'Return up to k agent_lesson rows whose created_at falls within ±window_interval of start_id. '
    'linked=TRUE when a mem_edge (any direction) exists between that row and start_id. '
    'include_unlinked=FALSE restricts output to explicitly connected lessons. '
    'Ghost-lesson exclusion controlled by pgmnemo.include_unverified GUC (default off). '
    'Returns zero rows if start_id is missing. Fail-safe: missing GUC treated as off.';
