-- Completed adventure portfolio cards with linked sessions, masters, and moderated reviews.

CREATE TABLE portfolio_games (
    id                 UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    group_id           UUID NOT NULL REFERENCES game_groups(id) ON DELETE CASCADE,
    public_slug        VARCHAR(160),
    title              VARCHAR(255) NOT NULL,
    description        TEXT,
    cover_storage_key  TEXT,
    system             VARCHAR(50),
    format             VARCHAR(20) CHECK (format IN ('Online', 'Offline', 'Hybrid')),
    completed_at       TIMESTAMPTZ NOT NULL DEFAULT now(),
    is_public          BOOLEAN NOT NULL DEFAULT false,
    published_at       TIMESTAMPTZ,
    created_at         TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at         TIMESTAMPTZ NOT NULL DEFAULT now(),
    CHECK (
        NOT is_public
        OR (
            public_slug IS NOT NULL
            AND description IS NOT NULL
            AND cover_storage_key IS NOT NULL
            AND published_at IS NOT NULL
        )
    )
);

CREATE UNIQUE INDEX ux_portfolio_games_public_slug
    ON portfolio_games (lower(public_slug))
    WHERE public_slug IS NOT NULL;

CREATE INDEX ix_portfolio_games_group
    ON portfolio_games (group_id, completed_at DESC);

CREATE INDEX ix_portfolio_games_public
    ON portfolio_games (completed_at DESC)
    WHERE is_public = true;

CREATE TABLE portfolio_game_sessions (
    portfolio_game_id  UUID NOT NULL REFERENCES portfolio_games(id) ON DELETE CASCADE,
    session_id         UUID NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
    PRIMARY KEY (portfolio_game_id, session_id),
    UNIQUE (session_id)
);

CREATE TABLE portfolio_game_masters (
    portfolio_game_id  UUID NOT NULL REFERENCES portfolio_games(id) ON DELETE CASCADE,
    player_id          UUID NOT NULL REFERENCES players(id) ON DELETE CASCADE,
    PRIMARY KEY (portfolio_game_id, player_id)
);

CREATE INDEX ix_portfolio_game_masters_player
    ON portfolio_game_masters (player_id, portfolio_game_id);

CREATE FUNCTION lock_portfolio_publication_mutation()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
    PERFORM pg_advisory_xact_lock(20260530, 108);
    RETURN NULL;
END;
$$;

CREATE TRIGGER trg_portfolio_games_lock_publication_mutation
BEFORE INSERT OR DELETE OR UPDATE OF is_public ON portfolio_games
FOR EACH STATEMENT
EXECUTE FUNCTION lock_portfolio_publication_mutation();

CREATE TRIGGER trg_portfolio_game_sessions_lock_publication_mutation
BEFORE INSERT OR DELETE OR UPDATE ON portfolio_game_sessions
FOR EACH STATEMENT
EXECUTE FUNCTION lock_portfolio_publication_mutation();

CREATE TRIGGER trg_portfolio_game_masters_lock_publication_mutation
BEFORE INSERT OR DELETE OR UPDATE ON portfolio_game_masters
FOR EACH STATEMENT
EXECUTE FUNCTION lock_portfolio_publication_mutation();

CREATE TRIGGER trg_sessions_lock_portfolio_publication_mutation
BEFORE DELETE OR UPDATE OF scheduled_at ON sessions
FOR EACH STATEMENT
EXECUTE FUNCTION lock_portfolio_publication_mutation();

CREATE TRIGGER trg_game_groups_lock_portfolio_publication_mutation_before_delete
BEFORE DELETE ON game_groups
FOR EACH STATEMENT
EXECUTE FUNCTION lock_portfolio_publication_mutation();

CREATE TRIGGER trg_players_lock_portfolio_publication_mutation_before_delete
BEFORE DELETE ON players
FOR EACH STATEMENT
EXECUTE FUNCTION lock_portfolio_publication_mutation();

CREATE FUNCTION validate_public_portfolio_game_required_links()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
DECLARE
    target_portfolio_game_id UUID;
    target_portfolio_game_ids UUID[];
BEGIN
    PERFORM pg_advisory_xact_lock(20260530, 108);

    IF TG_TABLE_NAME = 'portfolio_games' THEN
        target_portfolio_game_ids := ARRAY[NEW.id];
    ELSIF TG_OP = 'DELETE' THEN
        target_portfolio_game_ids := ARRAY[OLD.portfolio_game_id];
    ELSIF TG_OP = 'INSERT' THEN
        target_portfolio_game_ids := ARRAY[NEW.portfolio_game_id];
    ELSE
        target_portfolio_game_ids := ARRAY[OLD.portfolio_game_id, NEW.portfolio_game_id];
    END IF;

    IF current_setting('transaction_isolation') <> 'read committed' THEN
        RAISE EXCEPTION
            'portfolio publication validation requires read committed isolation'
            USING ERRCODE = '0A000';
    END IF;

    SELECT pg.id
    INTO target_portfolio_game_id
    FROM portfolio_games pg
    WHERE pg.id = ANY(target_portfolio_game_ids)
      AND pg.is_public = true
      AND (
          NOT EXISTS (
              SELECT 1
              FROM portfolio_game_sessions pgs
              WHERE pgs.portfolio_game_id = pg.id
          )
          OR EXISTS (
              SELECT 1
              FROM portfolio_game_sessions pgs
              JOIN sessions s ON s.id = pgs.session_id
              WHERE pgs.portfolio_game_id = pg.id
                AND s.scheduled_at >= now()
          )
          OR NOT EXISTS (
              SELECT 1
              FROM portfolio_game_masters pgm
              WHERE pgm.portfolio_game_id = pg.id
          )
      )
    LIMIT 1;

    IF target_portfolio_game_id IS NOT NULL THEN
        RAISE EXCEPTION
            'published portfolio game % must have at least one linked session and at least one linked master',
            target_portfolio_game_id
            USING ERRCODE = '23514';
    END IF;

    RETURN NULL;
END;
$$;

CREATE FUNCTION unpublish_public_portfolio_games_for_future_session()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
DECLARE
    final_scheduled_at TIMESTAMPTZ;
BEGIN
    SELECT s.scheduled_at
    INTO final_scheduled_at
    FROM sessions s
    WHERE s.id = NEW.id;

    IF final_scheduled_at >= now() THEN
        IF current_setting('transaction_isolation') <> 'read committed' THEN
            RAISE EXCEPTION
                'portfolio future reschedule requires read committed isolation'
                USING ERRCODE = '0A000';
        END IF;

        PERFORM pg.id
        FROM portfolio_games pg
        WHERE EXISTS (
              SELECT 1
              FROM portfolio_game_sessions pgs
              JOIN sessions s ON s.id = pgs.session_id
              WHERE pgs.portfolio_game_id = pg.id
                AND s.scheduled_at >= now()
          )
        ORDER BY pg.id
        FOR UPDATE OF pg;

        PERFORM pg_advisory_xact_lock(20260530, 108);

        UPDATE portfolio_games pg
        SET is_public = false,
            updated_at = now()
        WHERE pg.is_public = true
          AND EXISTS (
              SELECT 1
              FROM portfolio_game_sessions pgs
              JOIN sessions s ON s.id = pgs.session_id
              WHERE pgs.portfolio_game_id = pg.id
                AND s.scheduled_at >= now()
          );
    END IF;

    RETURN NULL;
END;
$$;

CREATE CONSTRAINT TRIGGER trg_sessions_unpublish_public_portfolio_games_for_future_reschedule
AFTER UPDATE OF scheduled_at ON sessions
DEFERRABLE INITIALLY DEFERRED
FOR EACH ROW
EXECUTE FUNCTION unpublish_public_portfolio_games_for_future_session();

CREATE CONSTRAINT TRIGGER trg_portfolio_games_validate_required_links
AFTER INSERT OR UPDATE OF is_public ON portfolio_games
DEFERRABLE INITIALLY DEFERRED
FOR EACH ROW
EXECUTE FUNCTION validate_public_portfolio_game_required_links();

CREATE CONSTRAINT TRIGGER trg_portfolio_game_sessions_validate_required_links
AFTER INSERT OR DELETE OR UPDATE OF portfolio_game_id, session_id ON portfolio_game_sessions
DEFERRABLE INITIALLY DEFERRED
FOR EACH ROW
EXECUTE FUNCTION validate_public_portfolio_game_required_links();

CREATE CONSTRAINT TRIGGER trg_portfolio_game_masters_validate_required_links
AFTER DELETE OR UPDATE OF portfolio_game_id ON portfolio_game_masters
DEFERRABLE INITIALLY DEFERRED
FOR EACH ROW
EXECUTE FUNCTION validate_public_portfolio_game_required_links();

CREATE TABLE portfolio_game_reviews (
    id                      UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    portfolio_game_id       UUID NOT NULL REFERENCES portfolio_games(id) ON DELETE CASCADE,
    author_player_id        UUID NOT NULL REFERENCES players(id) ON DELETE CASCADE,
    author_display_name     VARCHAR(255) NOT NULL,
    body                    TEXT NOT NULL,
    publication_consent_at  TIMESTAMPTZ NOT NULL,
    moderation_status       VARCHAR(20) NOT NULL DEFAULT 'Pending'
                            CHECK (moderation_status IN ('Pending', 'Approved', 'Rejected', 'Hidden')),
    moderated_by_player_id  UUID REFERENCES players(id) ON DELETE SET NULL,
    moderated_at            TIMESTAMPTZ,
    created_at              TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at              TIMESTAMPTZ NOT NULL DEFAULT now(),
    UNIQUE (portfolio_game_id, author_player_id)
);

CREATE INDEX ix_portfolio_game_reviews_author
    ON portfolio_game_reviews (author_player_id);

CREATE INDEX ix_portfolio_game_reviews_moderator
    ON portfolio_game_reviews (moderated_by_player_id)
    WHERE moderated_by_player_id IS NOT NULL;

CREATE INDEX ix_portfolio_game_reviews_public
    ON portfolio_game_reviews (portfolio_game_id, created_at DESC)
    WHERE moderation_status = 'Approved' AND publication_consent_at IS NOT NULL;

CREATE INDEX ix_portfolio_game_reviews_pending
    ON portfolio_game_reviews (portfolio_game_id, created_at DESC)
    WHERE moderation_status = 'Pending';
