-- 16_crq_dashboard.sql — fleettickets · CRQ dashboard parity (view + read functions)
-- ─────────────────────────────────────────────────────────────────────────────
-- Brings CRQ to FleetOps-dashboard parity with INC, so the Tickets tab's CRQ
-- sub-tab works "just like INC". Mirrors, against tickets.crq:
--   tickets.crq_open_sla              ← mirror of tickets.inc_open_sla (08)
--   reporting.fn_crq_dashboard        ← mirror of reporting.fn_inc_dashboard (09/12)
--   reporting.fn_crq_search           ← mirror of reporting.fn_inc_search (13)
--   reporting.fn_crq_filter_options   ← mirror of reporting.fn_inc_filter_options (14)
-- consumed by dashboard_api GET /webhook/crq-dashboard | crq-search | crq-filter-options.
--
-- Differences from the INC view: tickets.crq has no `geog` column (mig 05 is INC-only)
-- and its latitude/longitude come from `raw` (empty in the feed), so crq_open_sla omits
-- geog and derives latitude/longitude from `geom`. The 48h SLA rule is reused verbatim
-- for layout parity — installation-lifecycle SLA semantics may be refined later.
--
-- Idempotent (CREATE OR REPLACE / VIEW). Requires migration 15 (tickets.crq + typed cols).
-- ─────────────────────────────────────────────────────────────────────────────

SET search_path = tickets, public;

-- ── crq_open_sla — open CRQ tickets with derived SLA (mirror of inc_open_sla) ─
CREATE OR REPLACE VIEW tickets.crq_open_sla AS
SELECT
  ticket_id,
  normalized_status,
  bucket,
  cluster,
  region,
  location_name,
  assigned_team,
  owner,
  sla_status AS source_sla_status,
  mttr,                                              -- minutes (null until closed)
  COALESCE(created_at_service, first_seen_at)                       AS sla_clock,
  CASE WHEN created_at_service IS NOT NULL THEN 'service' ELSE 'first_seen' END AS sla_clock_source,
  round((EXTRACT(EPOCH FROM now() - COALESCE(created_at_service, first_seen_at)) / 3600)::numeric, 1) AS hours_open,
  CASE
    WHEN COALESCE(created_at_service, first_seen_at) IS NULL              THEN 'unknown'
    WHEN now() - COALESCE(created_at_service, first_seen_at) >= interval '48h' THEN 'breached'
    WHEN now() - COALESCE(created_at_service, first_seen_at) >= interval '36h' THEN 'at_risk'
    ELSE 'ok'
  END AS sla_state,
  created_at_service,
  first_seen_at,
  scheduled_at,
  CASE WHEN geom IS NOT NULL THEN ST_Y(geom) END AS latitude,
  CASE WHEN geom IS NOT NULL THEN ST_X(geom) END AS longitude,
  geo_source,
  geom
FROM tickets.crq
WHERE is_actionable;

COMMENT ON VIEW tickets.crq_open_sla IS
  'Open (is_actionable) CRQ tickets with derived SLA (48h rule; clock = created_at_service '
  'or first_seen_at fallback). Mirror of inc_open_sla; no geog. fleettickets 16.';

-- ── fn_crq_dashboard — mirror of fn_inc_dashboard over tickets.crq ───────────
CREATE OR REPLACE FUNCTION reporting.fn_crq_dashboard(
  p_cluster text DEFAULT NULL,
  p_status  text DEFAULT NULL,
  p_window  text DEFAULT 'today',
  p_from    timestamptz DEFAULT NULL,
  p_to      timestamptz DEFAULT NULL
)
 RETURNS jsonb LANGUAGE plpgsql STABLE AS $function$
DECLARE
  v_now_eat timestamp;
  v_from    timestamptz;
  v_to      timestamptz;
  v_preset  text;
  v_days    numeric;
  v_result  jsonb;
BEGIN
  p_cluster := NULLIF(p_cluster, '');
  p_status  := NULLIF(p_status, '');
  v_now_eat := now() AT TIME ZONE 'Africa/Nairobi';

  -- ── resolve the window ──────────────────────────────────────────────────────
  IF p_from IS NOT NULL OR p_to IS NOT NULL THEN
    v_preset := 'custom';
    v_from := COALESCE(p_from, '-infinity'::timestamptz);
    v_to   := COALESCE(p_to,   'infinity'::timestamptz);
  ELSE
    v_preset := lower(COALESCE(NULLIF(p_window, ''), 'today'));
    IF v_preset = 'week' THEN
      v_from := date_trunc('week', v_now_eat) AT TIME ZONE 'Africa/Nairobi';
      v_to   := (date_trunc('week', v_now_eat) + interval '1 week') AT TIME ZONE 'Africa/Nairobi';
    ELSIF v_preset = 'month' THEN
      v_from := date_trunc('month', v_now_eat) AT TIME ZONE 'Africa/Nairobi';
      v_to   := (date_trunc('month', v_now_eat) + interval '1 month') AT TIME ZONE 'Africa/Nairobi';
    ELSE
      v_preset := 'today';
      v_from := date_trunc('day', v_now_eat) AT TIME ZONE 'Africa/Nairobi';
      v_to   := (date_trunc('day', v_now_eat) + interval '1 day') AT TIME ZONE 'Africa/Nairobi';
    END IF;
  END IF;

  IF v_from > '-infinity'::timestamptz AND v_to < 'infinity'::timestamptz THEN
    v_days := GREATEST(EXTRACT(EPOCH FROM (v_to - v_from)) / 86400.0, 1);
  ELSE
    v_days := NULL;  -- open-ended custom window → per-day average not meaningful
  END IF;

  -- ── build payload ───────────────────────────────────────────────────────────
  WITH open_t AS (
    SELECT * FROM tickets.crq_open_sla
    WHERE (p_cluster IS NULL OR cluster = p_cluster)
      AND (p_status  IS NULL OR normalized_status = p_status)
  ),
  closed_t AS (
    SELECT ticket_id, normalized_status, cluster, region, location_name,
           assigned_team, owner, closed_at, mttr, sla_status, geo_source, geom
    FROM tickets.crq
    WHERE NOT COALESCE(is_actionable, false)
      AND closed_at IS NOT NULL
      AND closed_at >= v_from AND closed_at < v_to
      AND (p_cluster IS NULL OR cluster = p_cluster)
      AND (p_status  IS NULL OR normalized_status = p_status)
  )
  SELECT jsonb_build_object(
    'window', jsonb_build_object('from', v_from, 'to', v_to, 'preset', v_preset),

    'open', jsonb_build_object(
      'type', 'FeatureCollection',
      'features', COALESCE((
        SELECT jsonb_agg(jsonb_build_object(
          'type', 'Feature',
          'properties', jsonb_build_object(
            'ticket_id', ticket_id, 'normalized_status', normalized_status,
            'cluster', cluster, 'region', region, 'location_name', location_name,
            'assigned_team', assigned_team, 'owner', initcap(lower(NULLIF(owner, ''))),
            'geo_source', geo_source,
            'sla_state', sla_state, 'hours_open', hours_open),
          'geometry', ST_AsGeoJSON(geom)::jsonb))
        FROM open_t WHERE geom IS NOT NULL), '[]'::jsonb)
    ),

    'closed', jsonb_build_object(
      'type', 'FeatureCollection',
      'features', COALESCE((
        SELECT jsonb_agg(jsonb_build_object(
          'type', 'Feature',
          'properties', jsonb_build_object(
            'ticket_id', ticket_id, 'normalized_status', normalized_status,
            'cluster', cluster, 'region', region, 'location_name', location_name,
            'assigned_team', assigned_team, 'owner', initcap(lower(NULLIF(owner, ''))),
            'geo_source', geo_source,
            'closed_at', closed_at, 'mttr', mttr, 'sla_status', sla_status),
          'geometry', ST_AsGeoJSON(geom)::jsonb))
        FROM closed_t WHERE geom IS NOT NULL), '[]'::jsonb)
    ),

    'metrics', jsonb_build_object(
      'open_now',         (SELECT count(*) FROM open_t),
      'closed_in_window', (SELECT count(*) FROM closed_t),
      'sla', jsonb_build_object(
        'open', (SELECT jsonb_build_object(
            'breached', count(*) FILTER (WHERE sla_state = 'breached'),
            'at_risk',  count(*) FILTER (WHERE sla_state = 'at_risk'),
            'ok',       count(*) FILTER (WHERE sla_state = 'ok'),
            'unknown',  count(*) FILTER (WHERE sla_state = 'unknown')) FROM open_t),
        'closed', (SELECT jsonb_build_object(
            'compliant', count(*) FILTER (WHERE sla_status = 'Compliant'),
            'breached',  count(*) FILTER (WHERE sla_status = 'Breached')) FROM closed_t)
      ),
      'by_status', COALESCE((SELECT jsonb_object_agg(s, c) FROM (
          SELECT COALESCE(normalized_status, '(none)') AS s, count(*) AS c FROM (
            SELECT normalized_status FROM open_t
            UNION ALL SELECT normalized_status FROM closed_t) u GROUP BY 1) z), '{}'::jsonb),
      'by_cluster', COALESCE((SELECT jsonb_object_agg(cl, c) FROM (
          SELECT COALESCE(cluster, '(none)') AS cl, count(*) AS c FROM (
            SELECT cluster FROM open_t
            UNION ALL SELECT cluster FROM closed_t) u GROUP BY 1) z), '{}'::jsonb),
      -- closures by engineer (CASE-NORMALIZED owner) — leaderboard for "who closed".
      'by_owner', COALESCE((SELECT jsonb_agg(jsonb_build_object(
            'owner', o, 'closed', c, 'breached', b, 'avg_mttr_min', a) ORDER BY c DESC, o)
          FROM (
            SELECT COALESCE(initcap(lower(NULLIF(owner, ''))), '(unattributed)') AS o,
                   count(*) AS c,
                   count(*) FILTER (WHERE sla_status = 'Breached') AS b,
                   round(avg(mttr) FILTER (WHERE mttr IS NOT NULL), 1) AS a
            FROM closed_t GROUP BY 1) z), '[]'::jsonb),
      'closure_rate', jsonb_build_object(
        'per_day_avg', CASE WHEN v_days IS NULL THEN NULL
                            ELSE round((SELECT count(*) FROM closed_t)::numeric / v_days, 2) END,
        'series', COALESCE((SELECT jsonb_agg(jsonb_build_object('day', d, 'count', c) ORDER BY d) FROM (
            SELECT (closed_at AT TIME ZONE 'Africa/Nairobi')::date AS d, count(*) AS c
            FROM closed_t GROUP BY 1) z), '[]'::jsonb)
      ),
      'avg_mttr_min', (SELECT round(avg(mttr), 1) FROM closed_t WHERE mttr IS NOT NULL)
    ),

    'freshness', (SELECT jsonb_object_agg(dataset, jsonb_build_object(
        'export_type', export_type, 'exported_at', exported_at,
        'records_ingested', records_ingested, 'ingested_at', ingested_at))
      FROM tickets.import_meta)
  ) INTO v_result;

  RETURN v_result;
END $function$;

-- ── fn_crq_search — mirror of fn_inc_search over tickets.crq ──────────────────
CREATE OR REPLACE FUNCTION reporting.fn_crq_search(
  p_ticket_id text DEFAULT NULL,
  p_owner     text DEFAULT NULL,
  p_cluster   text DEFAULT NULL,
  p_status    text DEFAULT NULL,
  p_state     text DEFAULT 'closed',
  p_from      timestamptz DEFAULT NULL,
  p_to        timestamptz DEFAULT NULL,
  p_limit     integer DEFAULT 500
)
 RETURNS jsonb LANGUAGE plpgsql STABLE AS $function$
DECLARE
  v_state text := lower(COALESCE(NULLIF(p_state, ''), 'closed'));
  v_limit integer := LEAST(GREATEST(COALESCE(p_limit, 500), 1), 5000);
  v_result jsonb;
BEGIN
  p_ticket_id := NULLIF(trim(p_ticket_id), '');
  p_owner     := NULLIF(trim(p_owner), '');
  p_cluster   := NULLIF(p_cluster, '');
  p_status    := NULLIF(p_status, '');

  WITH hits AS (
    SELECT ticket_id, normalized_status, cluster, region, location_name,
           initcap(lower(NULLIF(owner, ''))) AS owner, assigned_team,
           sla_status, mttr, closed_at, created_at_service, is_actionable,
           CASE WHEN geom IS NOT NULL THEN ST_Y(geom) END AS lat,
           CASE WHEN geom IS NOT NULL THEN ST_X(geom) END AS lng
    FROM tickets.crq
    WHERE (p_ticket_id IS NULL OR ticket_id ILIKE '%' || p_ticket_id || '%')
      AND (p_owner     IS NULL OR lower(owner) LIKE '%' || lower(p_owner) || '%')
      AND (p_cluster   IS NULL OR cluster = p_cluster)
      AND (p_status    IS NULL OR normalized_status = p_status)
      AND CASE v_state
            WHEN 'open' THEN COALESCE(is_actionable, false)
            WHEN 'all'  THEN COALESCE(is_actionable, false)
                             OR (closed_at IS NOT NULL
                                 AND (p_from IS NULL OR closed_at >= p_from)
                                 AND (p_to   IS NULL OR closed_at <  p_to))
            ELSE             NOT COALESCE(is_actionable, false)   -- 'closed'
                             AND closed_at IS NOT NULL
                             AND (p_from IS NULL OR closed_at >= p_from)
                             AND (p_to   IS NULL OR closed_at <  p_to)
          END
  ),
  total AS (SELECT count(*) AS n FROM hits),
  page AS (
    SELECT * FROM hits
    ORDER BY closed_at DESC NULLS LAST, created_at_service DESC NULLS LAST
    LIMIT v_limit
  )
  SELECT jsonb_build_object(
    'count',     (SELECT n FROM total),
    'truncated', (SELECT n FROM total) > v_limit,
    'limit',     v_limit,
    'state',     v_state,
    'rows', COALESCE((SELECT jsonb_agg(to_jsonb(page)
                             ORDER BY page.closed_at DESC NULLS LAST,
                                      page.created_at_service DESC NULLS LAST)
                      FROM page), '[]'::jsonb)
  ) INTO v_result;

  RETURN v_result;
END $function$;

-- ── fn_crq_filter_options — mirror of fn_inc_filter_options over tickets.crq ──
CREATE OR REPLACE FUNCTION reporting.fn_crq_filter_options()
 RETURNS jsonb LANGUAGE sql STABLE AS $function$
  SELECT jsonb_build_object(
    'owners', (SELECT COALESCE(jsonb_agg(o ORDER BY o), '[]'::jsonb)
               FROM (SELECT DISTINCT initcap(lower(NULLIF(owner, ''))) AS o
                     FROM tickets.crq WHERE NULLIF(owner, '') IS NOT NULL) s),
    'clusters', (SELECT COALESCE(jsonb_agg(c ORDER BY c), '[]'::jsonb)
                 FROM (SELECT DISTINCT cluster AS c
                       FROM tickets.crq WHERE NULLIF(cluster, '') IS NOT NULL) s),
    'open_ticket_ids', (SELECT COALESCE(jsonb_agg(ticket_id ORDER BY ticket_id), '[]'::jsonb)
                        FROM tickets.crq WHERE COALESCE(is_actionable, false))
  );
$function$;

-- ── 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 SELECT ON tickets.crq_open_sla TO dashboard_ro;
    GRANT EXECUTE ON FUNCTION reporting.fn_crq_dashboard(text, text, text, timestamptz, timestamptz) TO dashboard_ro;
    GRANT EXECUTE ON FUNCTION reporting.fn_crq_search(text, text, text, text, text, timestamptz, timestamptz, integer) TO dashboard_ro;
    GRANT EXECUTE ON FUNCTION reporting.fn_crq_filter_options() TO dashboard_ro;
  END IF;
  IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'grafana_ro') THEN
    GRANT SELECT ON tickets.crq_open_sla TO grafana_ro;
    GRANT EXECUTE ON FUNCTION reporting.fn_crq_dashboard(text, text, text, timestamptz, timestamptz) TO grafana_ro;
    GRANT EXECUTE ON FUNCTION reporting.fn_crq_search(text, text, text, text, text, timestamptz, timestamptz, integer) TO grafana_ro;
    GRANT EXECUTE ON FUNCTION reporting.fn_crq_filter_options() TO grafana_ro;
  END IF;
END $grants$;
