-- 03_inc_columns.sql — fleettickets · unpack tickets.inc.raw into typed columns
-- ─────────────────────────────────────────────────────────────────────────────
-- Adds STORED generated columns derived from `raw`, so the INC dataset has real
-- typed, indexable columns while `raw` stays the source of truth (drift-safe).
-- STORED generated columns are computed for ALL existing rows on creation and
-- auto-recomputed on every future insert/update — no loader change needed.
--
-- Timestamps are the source's EAT wall-clock; we convert them to timestamptz via
-- tickets.eat_ts(). text->timestamp is technically DateStyle-dependent (so the
-- raw cast is only STABLE and can't sit in a generated column), but our source is
-- unambiguous ISO 'YYYY-MM-DD HH24:MI:SS', so the result is genuinely invariant —
-- hence eat_ts() is safely declared IMMUTABLE, which lets it back a generated col.
--
-- Idempotent: safe on a fresh DB and re-appliable on the live DB.
-- ─────────────────────────────────────────────────────────────────────────────

SET search_path = tickets, public;

-- EAT (Africa/Nairobi) text -> timestamptz; IMMUTABLE so it can back generated cols.
CREATE OR REPLACE FUNCTION tickets.eat_ts(p text)
 RETURNS timestamptz LANGUAGE sql IMMUTABLE PARALLEL SAFE
AS $fn$ SELECT (NULLIF(p, '')::timestamp) AT TIME ZONE 'Africa/Nairobi' $fn$;

ALTER TABLE tickets.inc
  -- text
  ADD COLUMN IF NOT EXISTS service_type      text GENERATED ALWAYS AS (raw->>'service_type') STORED,
  ADD COLUMN IF NOT EXISTS bucket            text GENERATED ALWAYS AS (raw->>'bucket') STORED,
  ADD COLUMN IF NOT EXISTS raw_status        text GENERATED ALWAYS AS (raw->>'raw_status') STORED,
  ADD COLUMN IF NOT EXISTS normalized_status text GENERATED ALWAYS AS (raw->>'normalized_status') STORED,
  ADD COLUMN IF NOT EXISTS cluster           text GENERATED ALWAYS AS (raw->>'cluster') STORED,
  ADD COLUMN IF NOT EXISTS region            text GENERATED ALWAYS AS (raw->>'region') STORED,
  ADD COLUMN IF NOT EXISTS location_name     text GENERATED ALWAYS AS (raw->>'location_name') STORED,
  ADD COLUMN IF NOT EXISTS assigned_team     text GENERATED ALWAYS AS (raw->>'assigned_team') STORED,
  ADD COLUMN IF NOT EXISTS owner             text GENERATED ALWAYS AS (raw->>'owner') STORED,
  ADD COLUMN IF NOT EXISTS sla_status        text GENERATED ALWAYS AS (raw->>'sla_status') STORED,
  -- numeric / float
  ADD COLUMN IF NOT EXISTS mttr      numeric          GENERATED ALWAYS AS (NULLIF(raw->>'mttr','')::numeric) STORED,
  ADD COLUMN IF NOT EXISTS latitude  double precision GENERATED ALWAYS AS (NULLIF(raw->>'latitude','')::double precision) STORED,
  ADD COLUMN IF NOT EXISTS longitude double precision GENERATED ALWAYS AS (NULLIF(raw->>'longitude','')::double precision) STORED,
  -- boolean
  ADD COLUMN IF NOT EXISTS is_actionable   boolean GENERATED ALWAYS AS (NULLIF(raw->>'is_actionable','')::boolean) STORED,
  ADD COLUMN IF NOT EXISTS is_auto_created boolean GENERATED ALWAYS AS (NULLIF(raw->>'is_auto_created','')::boolean) STORED,
  ADD COLUMN IF NOT EXISTS is_auto_closed  boolean GENERATED ALWAYS AS (NULLIF(raw->>'is_auto_closed','')::boolean) STORED,
  ADD COLUMN IF NOT EXISTS is_alarm        boolean GENERATED ALWAYS AS (NULLIF(raw->>'is_alarm','')::boolean) STORED,
  -- timestamps (EAT wall-clock -> timestamptz). created_at/updated_at are the
  -- EXPORT pipeline's bookkeeping (not ticket lifecycle), hence the source_ prefix.
  ADD COLUMN IF NOT EXISTS created_at_service timestamptz GENERATED ALWAYS AS (tickets.eat_ts(raw->>'created_at_service')) STORED,
  ADD COLUMN IF NOT EXISTS scheduled_at       timestamptz GENERATED ALWAYS AS (tickets.eat_ts(raw->>'scheduled_at')) STORED,
  ADD COLUMN IF NOT EXISTS closed_at          timestamptz GENERATED ALWAYS AS (tickets.eat_ts(raw->>'closed_at')) STORED,
  ADD COLUMN IF NOT EXISTS last_seen_at       timestamptz GENERATED ALWAYS AS (tickets.eat_ts(raw->>'last_seen_at')) STORED,
  ADD COLUMN IF NOT EXISTS first_seen_at      timestamptz GENERATED ALWAYS AS (tickets.eat_ts(raw->>'first_seen_at')) STORED,
  ADD COLUMN IF NOT EXISTS source_created_at  timestamptz GENERATED ALWAYS AS (tickets.eat_ts(raw->>'created_at')) STORED,
  ADD COLUMN IF NOT EXISTS source_updated_at  timestamptz GENERATED ALWAYS AS (tickets.eat_ts(raw->>'updated_at')) STORED;

-- indexes on the new typed columns (serve SLA / team / closure queries)
CREATE INDEX IF NOT EXISTS ix_inc_norm_status_col ON tickets.inc (normalized_status);
CREATE INDEX IF NOT EXISTS ix_inc_cluster_col     ON tickets.inc (cluster);
CREATE INDEX IF NOT EXISTS ix_inc_assigned_team   ON tickets.inc (assigned_team);
CREATE INDEX IF NOT EXISTS ix_inc_closed_at       ON tickets.inc (closed_at);
CREATE INDEX IF NOT EXISTS ix_inc_actionable_col  ON tickets.inc (is_actionable) WHERE is_actionable;

-- drop the throwaway probe function from interactive testing, if present
DROP FUNCTION IF EXISTS tickets._eat_ts_test(text);

-- grants (guarded: roles may not exist on a fresh DB)
DO $grants$
BEGIN
  IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'dashboard_ro') THEN
    GRANT EXECUTE ON FUNCTION tickets.eat_ts(text) TO dashboard_ro;
  END IF;
  IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'grafana_ro') THEN
    GRANT EXECUTE ON FUNCTION tickets.eat_ts(text) TO grafana_ro;
  END IF;
END $grants$;
