-- 22_tickets_schema.sql
-- Promote the ticket layer into its own `tickets` schema and SPLIT by type:
--   migration-21 tracksolid.tickets (one table, service_type discriminator)  -->
--   tickets.inc + tickets.crq          (one table per ticket type)
--   tracksolid.geo_clusters            -->  tickets.geo_clusters (shared gazetteer)
--
-- The read function reporting.fn_tickets_for_map keeps its SAME signature
-- (so dashboard_api GET /webhook/tickets and the FleetOps map are unchanged) but
-- now UNIONs the two per-type tables. The tracksolid.* objects from migration 21
-- are dropped after the data is copied across. Safe to re-apply.
--
-- NOTE: this intentionally introduces a schema beyond `tracksolid`/`reporting`
-- (a deliberate exception to the single-live-schema convention) to give tickets
-- their own namespace + access boundary.

CREATE SCHEMA IF NOT EXISTS tickets;
SET search_path = tickets, public;

-- ── normalize helper (cluster_key) ────────────────────────────────────────────
CREATE OR REPLACE FUNCTION tickets.norm_cluster(p text)
 RETURNS text LANGUAGE sql IMMUTABLE PARALLEL SAFE
AS $fn$ SELECT NULLIF(upper(regexp_replace(trim(COALESCE(p, '')), '\s+', ' ', 'g')), '') $fn$;

-- ── gazetteer: move from tracksolid if present, else create fresh ─────────────
DO $mv$
BEGIN
  IF to_regclass('tracksolid.geo_clusters') IS NOT NULL
     AND to_regclass('tickets.geo_clusters') IS NULL THEN
    ALTER TABLE tracksolid.geo_clusters SET SCHEMA tickets;
  END IF;
END $mv$;

CREATE TABLE IF NOT EXISTS tickets.geo_clusters (
  cluster_key text PRIMARY KEY,
  region      text,
  lat         double precision,
  lng         double precision,
  geom        geometry(Point, 4326),
  source      text,
  verified    boolean     NOT NULL DEFAULT false,
  updated_at  timestamptz NOT NULL DEFAULT now()
);

CREATE OR REPLACE FUNCTION tickets.tg_geo_clusters_geom()
 RETURNS trigger LANGUAGE plpgsql AS $fn$
BEGIN
  IF NEW.lat IS NOT NULL AND NEW.lng IS NOT NULL
     AND NEW.lat BETWEEN -90 AND 90 AND NEW.lng BETWEEN -180 AND 180
     AND NOT (NEW.lat = 0 AND NEW.lng = 0) THEN
    NEW.geom := ST_SetSRID(ST_MakePoint(NEW.lng, NEW.lat), 4326);
  ELSE
    NEW.geom := NULL;
  END IF;
  NEW.updated_at := now();
  RETURN NEW;
END $fn$;
DROP TRIGGER IF EXISTS trg_geo_clusters_geom ON tickets.geo_clusters;
CREATE TRIGGER trg_geo_clusters_geom BEFORE INSERT OR UPDATE ON tickets.geo_clusters
  FOR EACH ROW EXECUTE FUNCTION tickets.tg_geo_clusters_geom();

-- ── per-type ticket tables (identical shape; CHECK-locked to their type) ──────
CREATE TABLE IF NOT EXISTS tickets.inc (
  ticket_id          text PRIMARY KEY,
  source_type        text,
  service_type       text,
  bucket             text,
  raw_status         text,
  normalized_status  text,
  created_at_service timestamptz,
  scheduled_at       timestamptz,
  closed_at          timestamptz,
  last_seen_at       timestamptz,
  first_seen_at      timestamptz,
  week_start         date,
  week_end           date,
  cluster            text,
  region             text,
  location_name      text,
  latitude           double precision,
  longitude          double precision,
  department         text,
  assigned_team      text,
  owner              text,
  sla_status         text,
  mttr               double precision,
  is_auto_created    boolean,
  is_auto_closed     boolean,
  is_alarm           boolean,
  is_actionable      boolean,
  source_s3_bucket   text,
  source_s3_key      text,
  source_snapshot_id bigint,
  created_at         timestamptz,
  updated_at         timestamptz,
  geom               geometry(Point, 4326),
  geo_source         text,
  raw                jsonb,
  ingested_at        timestamptz NOT NULL DEFAULT now(),
  CONSTRAINT inc_service_type_chk CHECK (service_type = 'inc')
);

-- crq mirrors inc's columns/defaults; add its own PK + type CHECK.
CREATE TABLE IF NOT EXISTS tickets.crq (LIKE tickets.inc INCLUDING DEFAULTS);
DO $crq$
BEGIN
  IF NOT EXISTS (SELECT 1 FROM pg_constraint
                 WHERE conrelid = 'tickets.crq'::regclass AND contype = 'p') THEN
    ALTER TABLE tickets.crq ADD PRIMARY KEY (ticket_id);
  END IF;
  IF NOT EXISTS (SELECT 1 FROM pg_constraint
                 WHERE conrelid = 'tickets.crq'::regclass AND conname = 'crq_service_type_chk') THEN
    ALTER TABLE tickets.crq ADD CONSTRAINT crq_service_type_chk CHECK (service_type = 'crq');
  END IF;
END $crq$;

-- ── shared geom-resolution trigger (feed coords → cluster gazetteer → none) ───
CREATE OR REPLACE FUNCTION tickets.tg_ticket_geom()
 RETURNS trigger LANGUAGE plpgsql AS $fn$
DECLARE g geometry(Point, 4326);
BEGIN
  IF NEW.latitude IS NOT NULL AND NEW.longitude IS NOT NULL
     AND NEW.latitude  BETWEEN  -90 AND  90
     AND NEW.longitude BETWEEN -180 AND 180
     AND NOT (NEW.latitude = 0 AND NEW.longitude = 0) THEN
    NEW.geom := ST_SetSRID(ST_MakePoint(NEW.longitude, NEW.latitude), 4326);
    NEW.geo_source := 'feed';
  ELSE
    SELECT gc.geom INTO g FROM tickets.geo_clusters gc
     WHERE gc.cluster_key = tickets.norm_cluster(NEW.cluster) AND gc.geom IS NOT NULL LIMIT 1;
    IF g IS NOT NULL THEN NEW.geom := g; NEW.geo_source := 'cluster';
    ELSE NEW.geom := NULL; NEW.geo_source := 'none'; END IF;
  END IF;
  RETURN NEW;
END $fn$;

DROP TRIGGER IF EXISTS trg_inc_geom ON tickets.inc;
CREATE TRIGGER trg_inc_geom BEFORE INSERT OR UPDATE ON tickets.inc
  FOR EACH ROW EXECUTE FUNCTION tickets.tg_ticket_geom();
DROP TRIGGER IF EXISTS trg_crq_geom ON tickets.crq;
CREATE TRIGGER trg_crq_geom BEFORE INSERT OR UPDATE ON tickets.crq
  FOR EACH ROW EXECUTE FUNCTION tickets.tg_ticket_geom();

-- re-resolve geoms across both tables after the gazetteer is (re)seeded.
CREATE OR REPLACE FUNCTION tickets.resolve_ticket_geoms()
 RETURNS integer LANGUAGE plpgsql AS $fn$
DECLARE n integer; m integer;
BEGIN
  UPDATE tickets.inc t SET geom = gc.geom, geo_source = 'cluster'
    FROM tickets.geo_clusters gc
   WHERE gc.cluster_key = tickets.norm_cluster(t.cluster) AND gc.geom IS NOT NULL
     AND t.geo_source IS DISTINCT FROM 'feed' AND t.geom IS DISTINCT FROM gc.geom;
  GET DIAGNOSTICS n = ROW_COUNT;
  UPDATE tickets.crq t SET geom = gc.geom, geo_source = 'cluster'
    FROM tickets.geo_clusters gc
   WHERE gc.cluster_key = tickets.norm_cluster(t.cluster) AND gc.geom IS NOT NULL
     AND t.geo_source IS DISTINCT FROM 'feed' AND t.geom IS DISTINCT FROM gc.geom;
  GET DIAGNOSTICS m = ROW_COUNT;
  RETURN n + m;
END $fn$;

-- ── indexes (per table) ───────────────────────────────────────────────────────
CREATE INDEX IF NOT EXISTS ix_inc_status     ON tickets.inc (normalized_status);
CREATE INDEX IF NOT EXISTS ix_inc_actionable ON tickets.inc (is_actionable) WHERE is_actionable;
CREATE INDEX IF NOT EXISTS ix_inc_geom       ON tickets.inc USING gist (geom);
CREATE INDEX IF NOT EXISTS ix_inc_cluster    ON tickets.inc (tickets.norm_cluster(cluster));
CREATE INDEX IF NOT EXISTS ix_crq_status     ON tickets.crq (normalized_status);
CREATE INDEX IF NOT EXISTS ix_crq_actionable ON tickets.crq (is_actionable) WHERE is_actionable;
CREATE INDEX IF NOT EXISTS ix_crq_geom       ON tickets.crq USING gist (geom);
CREATE INDEX IF NOT EXISTS ix_crq_cluster    ON tickets.crq (tickets.norm_cluster(cluster));

-- ── backfill from migration-21 tracksolid.tickets, then retire it ─────────────
DO $bf$
BEGIN
  IF to_regclass('tracksolid.tickets') IS NOT NULL THEN
    INSERT INTO tickets.inc (
      ticket_id, source_type, service_type, bucket, raw_status, normalized_status,
      created_at_service, scheduled_at, closed_at, last_seen_at, first_seen_at,
      week_start, week_end, cluster, region, location_name, latitude, longitude,
      department, assigned_team, owner, sla_status, mttr, is_auto_created,
      is_auto_closed, is_alarm, is_actionable, source_s3_bucket, source_s3_key,
      source_snapshot_id, created_at, updated_at, raw)
    SELECT
      ticket_id, source_type, service_type, bucket, raw_status, normalized_status,
      created_at_service, scheduled_at, closed_at, last_seen_at, first_seen_at,
      week_start, week_end, cluster, region, location_name, latitude, longitude,
      department, assigned_team, owner, sla_status, mttr, is_auto_created,
      is_auto_closed, is_alarm, is_actionable, source_s3_bucket, source_s3_key,
      source_snapshot_id, created_at, updated_at, raw
    FROM tracksolid.tickets WHERE service_type = 'inc'
    ON CONFLICT (ticket_id) DO NOTHING;

    INSERT INTO tickets.crq (
      ticket_id, source_type, service_type, bucket, raw_status, normalized_status,
      created_at_service, scheduled_at, closed_at, last_seen_at, first_seen_at,
      week_start, week_end, cluster, region, location_name, latitude, longitude,
      department, assigned_team, owner, sla_status, mttr, is_auto_created,
      is_auto_closed, is_alarm, is_actionable, source_s3_bucket, source_s3_key,
      source_snapshot_id, created_at, updated_at, raw)
    SELECT
      ticket_id, source_type, service_type, bucket, raw_status, normalized_status,
      created_at_service, scheduled_at, closed_at, last_seen_at, first_seen_at,
      week_start, week_end, cluster, region, location_name, latitude, longitude,
      department, assigned_team, owner, sla_status, mttr, is_auto_created,
      is_auto_closed, is_alarm, is_actionable, source_s3_bucket, source_s3_key,
      source_snapshot_id, created_at, updated_at, raw
    FROM tracksolid.tickets WHERE service_type = 'crq'
    ON CONFLICT (ticket_id) DO NOTHING;
  END IF;
END $bf$;

-- retire migration-21 objects (data now lives in tickets.*)
DROP TABLE    IF EXISTS tracksolid.tickets;
DROP FUNCTION IF EXISTS tracksolid.resolve_ticket_geoms();
DROP FUNCTION IF EXISTS tracksolid.tg_tickets_geom();
DROP FUNCTION IF EXISTS tracksolid.tg_geo_clusters_geom();
DROP FUNCTION IF EXISTS tracksolid.norm_cluster(text);

-- ── read function (same signature; now UNIONs the two tables) ─────────────────
CREATE OR REPLACE FUNCTION reporting.fn_tickets_for_map(
  p_service_type text    DEFAULT NULL,
  p_status       text    DEFAULT NULL,
  p_open_only    boolean DEFAULT true
)
 RETURNS jsonb LANGUAGE plpgsql STABLE AS $fn$
DECLARE v_result jsonb;
BEGIN
  p_service_type := lower(NULLIF(p_service_type, ''));
  p_status       := NULLIF(p_status, '');
  WITH filtered AS (
    SELECT ticket_id, service_type, normalized_status, raw_status, cluster, region,
           location_name, department, owner, assigned_team, sla_status, is_actionable,
           geo_source, created_at_service, scheduled_at, geom
    FROM tickets.inc
    WHERE geom IS NOT NULL
      AND (p_service_type IS NULL OR p_service_type = 'inc')
      AND (p_status IS NULL OR normalized_status = p_status)
      AND (NOT p_open_only OR is_actionable IS TRUE)
    UNION ALL
    SELECT ticket_id, service_type, normalized_status, raw_status, cluster, region,
           location_name, department, owner, assigned_team, sla_status, is_actionable,
           geo_source, created_at_service, scheduled_at, geom
    FROM tickets.crq
    WHERE geom IS NOT NULL
      AND (p_service_type IS NULL OR p_service_type = 'crq')
      AND (p_status IS NULL OR normalized_status = p_status)
      AND (NOT p_open_only OR is_actionable IS TRUE)
  )
  SELECT jsonb_build_object(
    'summary', jsonb_build_object(
      'ticket_count', COUNT(*),
      'inc',          COUNT(*) FILTER (WHERE service_type = 'inc'),
      'crq',          COUNT(*) FILTER (WHERE service_type = 'crq'),
      'open',         COUNT(*) FILTER (WHERE is_actionable IS TRUE),
      'by_status',    (SELECT jsonb_object_agg(s, c)
                         FROM (SELECT normalized_status AS s, COUNT(*) AS c
                                 FROM filtered GROUP BY normalized_status) z)
    ),
    'geojson', jsonb_build_object(
      'type', 'FeatureCollection',
      'features', COALESCE(jsonb_agg(
        jsonb_build_object(
          'type', 'Feature',
          'properties', jsonb_build_object(
            'ticket_id',     ticket_id,
            'service_type',  service_type,
            'status',        normalized_status,
            'raw_status',    raw_status,
            'cluster',       cluster,
            'region',        region,
            'location_name', location_name,
            'department',    department,
            'owner',         owner,
            'assigned_team', assigned_team,
            'sla_status',    sla_status,
            'is_actionable', is_actionable,
            'geo_source',    geo_source,
            'created_at',    to_char(created_at_service, 'YYYY-MM-DD HH24:MI:SS'),
            'scheduled_at',  to_char(scheduled_at,       'YYYY-MM-DD HH24:MI:SS')
          ),
          'geometry', ST_AsGeoJSON(geom)::jsonb
        )
      ), '[]'::jsonb)
    )
  ) INTO v_result FROM filtered;
  RETURN v_result;
END $fn$;

COMMENT ON FUNCTION reporting.fn_tickets_for_map(text, text, boolean) IS
  'INC/CRQ tickets (tickets.inc + tickets.crq) as GeoJSON for the FleetOps map. Migration 22.';

-- ── grants ────────────────────────────────────────────────────────────────────
DO $grants$
BEGIN
  IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'tracksolid_owner') THEN
    GRANT USAGE, CREATE ON SCHEMA tickets TO tracksolid_owner;
    GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA tickets TO tracksolid_owner;
    GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA tickets TO tracksolid_owner;
  END IF;
  IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'dashboard_ro') THEN
    GRANT USAGE ON SCHEMA tickets TO dashboard_ro;
    GRANT SELECT ON tickets.inc, tickets.crq, tickets.geo_clusters TO dashboard_ro;
    GRANT EXECUTE ON FUNCTION reporting.fn_tickets_for_map(text, text, boolean) TO dashboard_ro;
  END IF;
  IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'grafana_ro') THEN
    GRANT USAGE ON SCHEMA tickets TO grafana_ro;
    GRANT SELECT ON tickets.inc, tickets.crq, tickets.geo_clusters TO grafana_ro;
    GRANT EXECUTE ON FUNCTION reporting.fn_tickets_for_map(text, text, boolean) TO grafana_ro;
  END IF;
END $grants$;
