-- gsc-shell-api — initial schema.
--
-- Lives in `gsc_core` so it's reachable from every app that needs chrome data.
-- Owned by the `gsc_shell` role (created out of band).
--
-- Idempotent: every CREATE uses IF NOT EXISTS where possible.

CREATE SCHEMA IF NOT EXISTS shell;

-- Apps registry.
-- An "app" is a frontend that wants to render chrome from this service.
CREATE TABLE IF NOT EXISTS shell.apps (
  app_key       TEXT PRIMARY KEY CHECK (app_key ~ '^[a-z][a-z0-9-]*$'),
  display_name  TEXT NOT NULL,
  base_url      TEXT NOT NULL,
  is_active     BOOLEAN NOT NULL DEFAULT true,
  created_at    TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at    TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- Menu zones. Mirrors what the existing AdminShell expects.
DO $$
BEGIN
  IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'menu_zone' AND typnamespace = 'shell'::regnamespace) THEN
    CREATE TYPE shell.menu_zone AS ENUM ('topbar', 'sidebar', 'footer', 'user-menu');
  END IF;
END $$;

-- Menu items. One row per nav entry per app per zone.
-- Hierarchical via parent_id (sidebar submenus, etc.).
CREATE TABLE IF NOT EXISTS shell.menu_items (
  id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  app_key         TEXT NOT NULL REFERENCES shell.apps(app_key) ON DELETE CASCADE,
  parent_id       UUID REFERENCES shell.menu_items(id) ON DELETE CASCADE,
  zone            shell.menu_zone NOT NULL,
  key             TEXT NOT NULL CHECK (key ~ '^[a-z][a-z0-9_-]*$'),
  translation_key TEXT NOT NULL,                  -- e.g. 'menu.dashboard'
  href            TEXT NOT NULL,                  -- relative within app, or absolute if is_external
  icon            TEXT,                           -- Phosphor class, e.g. 'ph-house'
  sort_order      INT NOT NULL DEFAULT 0,
  is_external     BOOLEAN NOT NULL DEFAULT false,
  is_active       BOOLEAN NOT NULL DEFAULT true,
  metadata        JSONB NOT NULL DEFAULT '{}',
  created_at      TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at      TIMESTAMPTZ NOT NULL DEFAULT now(),
  UNIQUE (app_key, zone, key)
);

CREATE INDEX IF NOT EXISTS idx_menu_items_app_zone
  ON shell.menu_items (app_key, zone, sort_order)
  WHERE is_active;

CREATE INDEX IF NOT EXISTS idx_menu_items_parent
  ON shell.menu_items (parent_id)
  WHERE parent_id IS NOT NULL;

-- Role grants. Empty grants = visible to anyone authenticated. OR semantics
-- across rows: user sees the item if they hold ANY listed role.
-- Roles are Keycloak realm roles; we store their names as plain text since
-- Keycloak is the source of truth.
CREATE TABLE IF NOT EXISTS shell.menu_role_grants (
  menu_item_id  UUID NOT NULL REFERENCES shell.menu_items(id) ON DELETE CASCADE,
  role          TEXT NOT NULL,
  PRIMARY KEY (menu_item_id, role)
);

-- Per-app branding. Logo, product name, footer text. Optional for now.
CREATE TABLE IF NOT EXISTS shell.branding (
  app_key       TEXT PRIMARY KEY REFERENCES shell.apps(app_key) ON DELETE CASCADE,
  logo_url      TEXT NOT NULL,
  product_name  TEXT NOT NULL,
  footer_html   TEXT,
  brand_color   TEXT,                             -- optional CSS color override
  updated_at    TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- updated_at maintenance.
CREATE OR REPLACE FUNCTION shell.touch_updated_at() RETURNS trigger AS $$
BEGIN
  NEW.updated_at := now();
  RETURN NEW;
END $$ LANGUAGE plpgsql;

DO $$
BEGIN
  IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'apps_touch_updated_at') THEN
    CREATE TRIGGER apps_touch_updated_at
      BEFORE UPDATE ON shell.apps
      FOR EACH ROW EXECUTE FUNCTION shell.touch_updated_at();
  END IF;
  IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'menu_items_touch_updated_at') THEN
    CREATE TRIGGER menu_items_touch_updated_at
      BEFORE UPDATE ON shell.menu_items
      FOR EACH ROW EXECUTE FUNCTION shell.touch_updated_at();
  END IF;
  IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'branding_touch_updated_at') THEN
    CREATE TRIGGER branding_touch_updated_at
      BEFORE UPDATE ON shell.branding
      FOR EACH ROW EXECUTE FUNCTION shell.touch_updated_at();
  END IF;
END $$;
