-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
-- Migration 06 — Business Analytics Schema Support
-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
-- Adds the schema objects referenced by 01_BusinessAnalytics.md:
--   • tracksolid.devices.assigned_city       (§3.7 Geographic Drift)
--   • tracksolid.dispatch_log                (§4.4, §4.5 Field-Service SLAs)
--   • ops schema                             (external ops integration namespace)
--   • ops.service_log                        (§10 Service-Interval Forecaster)
--   • ops.odometer_readings                  (§3.8 Odometer Divergence)
--   • ops.tickets                            (§2.4 Cost-per-Ticket — skeleton)
--   • ops.vw_service_forecast                (§10 weekly booking view)
--
-- Run after migration 05. Safe to re-run (uses IF NOT EXISTS / DO NOTHING /
-- CREATE OR REPLACE).
-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

BEGIN;

-- ── 1. City cohort column (§3.7) ─────────────────────────────────────────────

ALTER TABLE tracksolid.devices
  ADD COLUMN IF NOT EXISTS assigned_city TEXT;

COMMENT ON COLUMN tracksolid.devices.assigned_city
  IS 'Operating territory code: NBO (Nairobi) | MBA (Mombasa) | KLA (Kampala). '
     'Used for city-cohort analytics and geographic drift detection.';

CREATE INDEX IF NOT EXISTS idx_devices_assigned_city
  ON tracksolid.devices (assigned_city)
  WHERE assigned_city IS NOT NULL;

-- ── 2. Dispatch log (§4.4, §4.5) ──────────────────────────────────────────────
-- One row per ticket dispatch. Populated by n8n / ops integration at
-- assignment; back-filled by nightly job using trips + live_positions.

CREATE TABLE IF NOT EXISTS tracksolid.dispatch_log (
  dispatch_id        BIGSERIAL         PRIMARY KEY,
  ticket_id          TEXT              NOT NULL,
  imei               TEXT              NOT NULL REFERENCES tracksolid.devices(imei),
  driver_name        TEXT,
  job_lat            DOUBLE PRECISION  NOT NULL,
  job_lng            DOUBLE PRECISION  NOT NULL,
  job_geom           geometry(Point, 4326),
  assigned_at        TIMESTAMPTZ       NOT NULL DEFAULT NOW(),
  first_movement_at  TIMESTAMPTZ,
  on_site_at         TIMESTAMPTZ,
  resolved_at        TIMESTAMPTZ,
  cancelled_at       TIMESTAMPTZ,
  distance_km        NUMERIC(8, 2),
  created_at         TIMESTAMPTZ       NOT NULL DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_dispatch_log_ticket
  ON tracksolid.dispatch_log (ticket_id);
CREATE INDEX IF NOT EXISTS idx_dispatch_log_imei_assigned
  ON tracksolid.dispatch_log (imei, assigned_at DESC);
CREATE INDEX IF NOT EXISTS idx_dispatch_log_assigned_at
  ON tracksolid.dispatch_log (assigned_at DESC);
CREATE INDEX IF NOT EXISTS idx_dispatch_log_job_geom
  ON tracksolid.dispatch_log USING GIST (job_geom);

COMMENT ON TABLE  tracksolid.dispatch_log
  IS 'Persistent record of every dispatch decision. Powers SLA metrics: '
     'dispatch latency, depart delay, time-to-site, wrench time.';
COMMENT ON COLUMN tracksolid.dispatch_log.first_movement_at
  IS 'First trip start after assigned_at. Back-filled nightly from trips.';
COMMENT ON COLUMN tracksolid.dispatch_log.on_site_at
  IS 'Time vehicle entered 150 m radius of job_geom. Back-filled nightly.';
COMMENT ON COLUMN tracksolid.dispatch_log.resolved_at
  IS 'Ticket close time from the ops system (ops.tickets.closed_at).';

-- ── 3. ops schema namespace ───────────────────────────────────────────────────
-- Separates Fireside operations domain (tickets, services, odometers) from
-- the tracksolid telematics namespace so ownership / grants can diverge.

CREATE SCHEMA IF NOT EXISTS ops;

COMMENT ON SCHEMA ops
  IS 'Fireside operations domain: tickets, service logs, odometer readings. '
     'Distinct from tracksolid.* which holds telematics data.';

-- ── 4. Service log (§10) ──────────────────────────────────────────────────────

CREATE TABLE IF NOT EXISTS ops.service_log (
  service_id     BIGSERIAL    PRIMARY KEY,
  imei           TEXT         NOT NULL REFERENCES tracksolid.devices(imei),
  service_date   DATE         NOT NULL,
  odometer_km    INTEGER      NOT NULL,
  service_type   TEXT,
  cost_kes       INTEGER,
  notes          TEXT,
  created_at     TIMESTAMPTZ  NOT NULL DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_service_log_imei_date
  ON ops.service_log (imei, service_date DESC);

COMMENT ON TABLE  ops.service_log
  IS 'Workshop service history. Powers §10 Service-Interval Forecaster.';
COMMENT ON COLUMN ops.service_log.service_type
  IS 'scheduled | repair | tyre | bodywork | inspection | other';
COMMENT ON COLUMN ops.service_log.odometer_km
  IS 'Physical odometer reading at service time (integer km).';

-- ── 5. Odometer readings (§3.8) ───────────────────────────────────────────────
-- Periodic physical odometer captures from service events, fuel card receipts,
-- or manual driver entry. Divergence vs tracker-computed distance flags
-- sensor drift or tamper.

CREATE TABLE IF NOT EXISTS ops.odometer_readings (
  reading_id     BIGSERIAL    PRIMARY KEY,
  imei           TEXT         NOT NULL REFERENCES tracksolid.devices(imei),
  reading_date   DATE         NOT NULL,
  reading_km     INTEGER      NOT NULL,
  source         TEXT,
  recorded_by    TEXT,
  created_at     TIMESTAMPTZ  NOT NULL DEFAULT NOW(),
  UNIQUE (imei, reading_date)
);

CREATE INDEX IF NOT EXISTS idx_odometer_readings_imei_date
  ON ops.odometer_readings (imei, reading_date DESC);

COMMENT ON TABLE  ops.odometer_readings
  IS 'Physical odometer captures from service, fuel card, or manual entry. '
     'Powers §3.8 Odometer Divergence audit.';
COMMENT ON COLUMN ops.odometer_readings.source
  IS 'service | fuel_card | driver_manual | workshop_form';

-- ── 6. Tickets skeleton (§2.4) ───────────────────────────────────────────────
-- MINIMAL skeleton so the Cost-per-Ticket query is runnable. In production,
-- this table is expected to be populated by the Fireside ticketing system
-- (Zoho/Freshdesk/job-management export) via n8n or a direct feed. Schema
-- is intentionally narrow — extend with columns specific to your source.

CREATE TABLE IF NOT EXISTS ops.tickets (
  ticket_id        TEXT         PRIMARY KEY,
  assigned_imei    TEXT         REFERENCES tracksolid.devices(imei),
  driver_name      TEXT,
  customer         TEXT,
  job_type         TEXT,
  priority         TEXT,
  status           TEXT         NOT NULL DEFAULT 'open',
  created_at       TIMESTAMPTZ  NOT NULL,
  assigned_at      TIMESTAMPTZ,
  closed_at        TIMESTAMPTZ,
  job_lat          DOUBLE PRECISION,
  job_lng          DOUBLE PRECISION,
  job_geom         geometry(Point, 4326),
  ingested_at      TIMESTAMPTZ  NOT NULL DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_tickets_status_created
  ON ops.tickets (status, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_tickets_assigned_imei
  ON ops.tickets (assigned_imei)
  WHERE assigned_imei IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_tickets_closed_at
  ON ops.tickets (closed_at DESC NULLS LAST);

COMMENT ON TABLE  ops.tickets
  IS 'Skeleton for ticket data sourced from the Fireside ops system. '
     'Replace or extend to match the actual feed (Zoho Desk, Freshdesk, etc).';
COMMENT ON COLUMN ops.tickets.status
  IS 'open | assigned | in_progress | resolved | cancelled';

-- ── 7. Service forecast view (§10) ────────────────────────────────────────────
-- Wraps the §10 forecaster CTE so the weekly booking query in
-- 01_BusinessAnalytics.md references a stable object.

CREATE OR REPLACE VIEW ops.vw_service_forecast AS
WITH last_service AS (
  SELECT DISTINCT ON (imei)
    imei,
    service_date,
    odometer_km
  FROM ops.service_log
  WHERE service_type = 'scheduled'
  ORDER BY imei, service_date DESC
),
current_odometer AS (
  SELECT imei, current_mileage_km
  FROM tracksolid.devices
),
trailing_rate AS (
  SELECT
    imei,
    SUM(distance_km) / 30.0 AS km_per_day_30d
  FROM tracksolid.trips
  WHERE start_time > NOW() - INTERVAL '30 days'
    AND end_time IS NOT NULL
  GROUP BY imei
)
SELECT
  d.imei,
  d.driver_name,
  d.vehicle_number,
  ls.service_date                                       AS last_service_date,
  ls.odometer_km                                        AS last_service_odo,
  co.current_mileage_km                                 AS current_odo,
  (co.current_mileage_km - COALESCE(ls.odometer_km, 0)) AS km_since_service,
  GREATEST(
    0,
    10000 - (co.current_mileage_km - COALESCE(ls.odometer_km, 0))
  )                                                     AS km_to_next_service,
  ROUND(tr.km_per_day_30d, 1)                           AS km_per_day_30d,
  CASE
    WHEN tr.km_per_day_30d > 0 THEN
      CURRENT_DATE + (
        GREATEST(0, 10000 - (co.current_mileage_km - COALESCE(ls.odometer_km, 0)))
        / tr.km_per_day_30d
      )::INT
    ELSE NULL
  END                                                   AS projected_service_date
FROM tracksolid.devices d
LEFT JOIN last_service     ls ON ls.imei = d.imei
LEFT JOIN current_odometer co ON co.imei = d.imei
LEFT JOIN trailing_rate    tr ON tr.imei = d.imei
WHERE d.enabled_flag = 1;

COMMENT ON VIEW ops.vw_service_forecast
  IS 'Projected next-service date per vehicle based on 30-day km rate. '
     'Service interval default 10,000 km — override at query time if needed.';

COMMIT;
