-- ============================================================================
-- 0097_field_workers.sql
--
-- Cycle 10 — unify `water_irrigators` and `time_tracking_workers` into a
-- single `field_workers` table. One row per person, one `pin_hash`, one
-- role vocabulary. Permanent fix for the silent-desync class of bugs
-- (a worker changing their water PIN while their time PIN stays the same,
-- or vice versa) and unlocks future domains (harvest reports, equipment
-- logs) without yet another `*_workers` table.
--
-- Why one table:
--   - Today two tables store the same person twice with separate PIN
--     hashes and disjoint role vocabularies (`irrigator`/`water_admin` vs
--     `worker`/`time_admin`). The Tuxedo worker PWA at `/water` already
--     shows ONE PIN entry screen but under the hood has to look up the
--     right row in the right table depending on which tab the worker
--     hits (Cycle 4 attempted unification via a best-effort cross-call
--     in `MobileWaterApp.handlePinSubmit`).
--   - Phase 2 (separate cycle) collapses the two session cookies and
--     replaces the dual verify actions with a single `verifyFieldWorkerPin`.
--     This cycle ships the schema + action layer; UI behavior is preserved.
--
-- Schema:
--   id                       UUID PK (new — old worker IDs are not reused)
--   brand_id                 UUID NOT NULL → brands(id) ON DELETE CASCADE
--   name                     TEXT NOT NULL
--   pin_hash                 TEXT NOT NULL — scrypt self-describing format
--                                       from `@/lib/water-log-pin`
--   role                     TEXT NOT NULL DEFAULT 'worker'
--                            — union of all four old role values
--   language_preference      TEXT NOT NULL DEFAULT 'en'
--   phone                    TEXT NULL (water-only; null for time-only workers)
--   notes                    TEXT NULL (water-only)
--   active                   BOOLEAN NOT NULL DEFAULT true
--   last_used_at             TIMESTAMPTZ NULL
--   created_at               TIMESTAMPTZ NOT NULL DEFAULT now()
--   updated_at               TIMESTAMPTZ NOT NULL DEFAULT now()
--
-- Migration behavior (idempotent — safe to re-run):
--   1.  Create field_workers + brand-scoped RLS.
--   2.  Add nullable `field_worker_id` columns on the four downstream
--       tables that currently FK into the old worker tables.
--   3.  Backfill: collapse water + time rows that share a (brand_id,
--       lower(trim(name))) key. PIN conflicts → water's wins (water is
--       the older surface, preserves existing auth). Names unique to
--       one side → copy verbatim.
--   4.  Verify every downstream FK row got a field_worker_id (must be 0
--       NULL before we make the column NOT NULL).
--   5.  Drop the old worker tables; CASCADE drops the OLD FK columns
--       (irrigator_id, worker_id) entirely. We do not preserve them —
--       the new `field_worker_id` columns are populated and become the
--       canonical reference.
--   6.  Reissue the partial unique index
--       `time_tracking_logs_one_open_per_worker_idx` against
--       `field_worker_id` (DB-level invariant: at most one open
--       clock-in per worker; preserved).
--
-- DESTRUCTIVE: drops `water_irrigators` and `time_tracking_workers`
-- entirely. The pin hashes are preserved verbatim into field_workers,
-- so no worker has to re-learn their PIN after deploy. Old IDs are not
-- preserved — every downstream FK is re-pointed to the new IDs. This
-- matters for any external system that referenced the old UUIDs; none
-- of our surfaces do.
--
-- Customer impact (Tuxedo has 10 water + 10 time workers today, no name
-- overlap):
--   - After deploy: 20 `field_workers` rows, every entry/log row
--     re-pointed at the right new UUID. Workers do NOT need to re-enter
--     a PIN. The water PIN entry screen continues to work; the time
--     PIN entry screen continues to work. The cross-call best-effort
--     in `MobileWaterApp` continues to work.
--   - The role select in `/admin/water-log/users/[id]` will be tightened
--     to drop `time_admin` (water-only workers shouldn't get time powers).
--     The role select in `/admin/time-tracking` will be tightened to drop
--     the silently-rejected `supervisor`/`admin` values that the explore
--     audit caught.
-- ============================================================================

-- ── 1. Create field_workers ────────────────────────────────────────────────

CREATE TABLE IF NOT EXISTS field_workers (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  brand_id UUID NOT NULL REFERENCES brands(id) ON DELETE CASCADE,
  name TEXT NOT NULL,
  pin_hash TEXT NOT NULL,
  role TEXT NOT NULL DEFAULT 'worker'
    CHECK (role IN ('worker', 'time_admin', 'irrigator', 'water_admin')),
  language_preference TEXT NOT NULL DEFAULT 'en',
  phone TEXT,
  notes TEXT,
  active BOOLEAN NOT NULL DEFAULT true,
  last_used_at TIMESTAMPTZ,
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS field_workers_brand_idx
  ON field_workers(brand_id);

CREATE INDEX IF NOT EXISTS field_workers_brand_active_idx
  ON field_workers(brand_id, active)
  WHERE active = true;

COMMENT ON TABLE field_workers IS
  'Cycle 10 — unified worker table replacing water_irrigators + time_tracking_workers. '
  'One row per person, one pin_hash. Role vocabulary: worker, time_admin, irrigator, water_admin. '
  'See db/migrations/0097_field_workers.sql for the unification rationale.';

DROP TRIGGER IF EXISTS field_workers_set_updated_at ON field_workers;
CREATE TRIGGER field_workers_set_updated_at
  BEFORE UPDATE ON field_workers
  FOR EACH ROW EXECUTE FUNCTION set_updated_at();

-- Brand-scoped RLS (mirrors 0096's smartsheet_workspace pattern).
ALTER TABLE field_workers ENABLE ROW LEVEL SECURITY;

DROP POLICY IF EXISTS tenant_isolation ON field_workers;
CREATE POLICY tenant_isolation ON field_workers FOR ALL
  USING (brand_id = current_brand_id() OR is_platform_admin())
  WITH CHECK (brand_id = current_brand_id() OR is_platform_admin());

-- ── 2. Add nullable field_worker_id columns on the four downstream tables ─

ALTER TABLE water_log_entries
  ADD COLUMN IF NOT EXISTS field_worker_id UUID
    REFERENCES field_workers(id) ON DELETE CASCADE;

ALTER TABLE water_sessions
  ADD COLUMN IF NOT EXISTS field_worker_id UUID
    REFERENCES field_workers(id) ON DELETE CASCADE;

ALTER TABLE time_tracking_logs
  ADD COLUMN IF NOT EXISTS field_worker_id UUID
    REFERENCES field_workers(id) ON DELETE CASCADE;

ALTER TABLE time_tracking_notification_log
  ADD COLUMN IF NOT EXISTS field_worker_id UUID
    REFERENCES field_workers(id) ON DELETE SET NULL;

-- ── 3. Backfill field_workers from the two old tables ─────────────────────
--
-- Insertion order is intentional: WATER first so its rows win on the
-- (brand_id, lower(trim(name))) key (we use ON CONFLICT DO NOTHING).
-- Time-tracking rows are inserted second; if the (brand_id, name) key
-- already has a water row, the time row is dropped (water's PIN wins).

INSERT INTO field_workers (
  id, brand_id, name, pin_hash, role, language_preference,
  phone, notes, active, last_used_at, created_at
)
SELECT
  wi.id,
  wi.brand_id,
  wi.name,
  wi.pin_hash,
  wi.role,
  wi.language_preference,
  wi.phone,
  wi.notes,
  wi.active,
  wi.last_used_at,
  wi.created_at
FROM water_irrigators wi
WHERE NOT EXISTS (
  SELECT 1 FROM field_workers fw
  WHERE fw.brand_id = wi.brand_id
    AND lower(trim(fw.name)) = lower(trim(wi.name))
);

INSERT INTO field_workers (
  id, brand_id, name, pin_hash, role, language_preference,
  phone, notes, active, last_used_at, created_at
)
SELECT
  tw.id,
  tw.brand_id,
  tw.name,
  tw.pin AS pin_hash,
  tw.role,
  tw.lang AS language_preference,
  NULL AS phone,
  NULL AS notes,
  tw.active,
  tw.last_used_at,
  tw.created_at
FROM time_tracking_workers tw
WHERE NOT EXISTS (
  SELECT 1 FROM field_workers fw
  WHERE fw.brand_id = tw.brand_id
    AND lower(trim(fw.name)) = lower(trim(tw.name))
);

-- If a (brand, name) pair was in BOTH tables, the time-tracking row was
-- skipped above. Re-insert only the conflicting rows using water's pin_hash
-- is unnecessary (we already have water's row); this block is a no-op
-- safety check.

-- ── 4. Backfill field_worker_id on downstream tables ──────────────────────
--
-- Join key: (brand_id, lower(trim(name))). The water side uses
-- water_log_entries.irrigator_id → water_irrigators; the time side uses
-- time_tracking_logs.worker_id → time_tracking_workers.

UPDATE water_log_entries wle
SET field_worker_id = fw.id
FROM water_irrigators wi, field_workers fw
WHERE wle.irrigator_id = wi.id
  AND fw.brand_id = wi.brand_id
  AND lower(trim(fw.name)) = lower(trim(wi.name))
  AND wle.field_worker_id IS NULL;

UPDATE water_sessions ws
SET field_worker_id = fw.id
FROM water_irrigators wi, field_workers fw
WHERE ws.irrigator_id = wi.id
  AND fw.brand_id = wi.brand_id
  AND lower(trim(fw.name)) = lower(trim(wi.name))
  AND ws.field_worker_id IS NULL;

UPDATE time_tracking_logs ttl
SET field_worker_id = fw.id
FROM time_tracking_workers tw, field_workers fw
WHERE ttl.worker_id = tw.id
  AND fw.brand_id = tw.brand_id
  AND lower(trim(fw.name)) = lower(trim(tw.name))
  AND ttl.field_worker_id IS NULL;

UPDATE time_tracking_notification_log tnl
SET field_worker_id = fw.id
FROM time_tracking_workers tw, field_workers fw
WHERE tnl.worker_id = tw.id
  AND fw.brand_id = tw.brand_id
  AND lower(trim(fw.name)) = lower(trim(tw.name))
  AND tnl.field_worker_id IS NULL;

-- ── 5. Verification: every downstream row must have a field_worker_id ────
--
-- If any of these SELECTs returns a non-zero count, the migration will
-- leave NULL FKs. The CHECK ensures the migration FAILS LOUDLY in that
-- case rather than silently dropping data on the cascade in step 7.

DO $$
DECLARE
  water_entries_null INTEGER;
  water_sessions_null INTEGER;
  time_logs_null INTEGER;
  time_notif_null INTEGER;
BEGIN
  SELECT count(*) INTO water_entries_null FROM water_log_entries WHERE field_worker_id IS NULL;
  SELECT count(*) INTO water_sessions_null FROM water_sessions WHERE field_worker_id IS NULL;
  SELECT count(*) INTO time_logs_null FROM time_tracking_logs WHERE field_worker_id IS NULL;
  SELECT count(*) INTO time_notif_null FROM time_tracking_notification_log WHERE field_worker_id IS NULL;

  IF water_entries_null > 0 THEN
    RAISE EXCEPTION 'water_log_entries has % rows with NULL field_worker_id after backfill', water_entries_null;
  END IF;
  IF water_sessions_null > 0 THEN
    RAISE EXCEPTION 'water_sessions has % rows with NULL field_worker_id after backfill', water_sessions_null;
  END IF;
  IF time_logs_null > 0 THEN
    RAISE EXCEPTION 'time_tracking_logs has % rows with NULL field_worker_id after backfill', time_logs_null;
  END IF;
  IF time_notif_null > 0 THEN
    RAISE EXCEPTION 'time_tracking_notification_log has % rows with NULL field_worker_id after backfill', time_notif_null;
  END IF;
END $$;

-- ── 6. Make field_worker_id NOT NULL (we've verified backfill is complete)

ALTER TABLE water_log_entries
  ALTER COLUMN field_worker_id SET NOT NULL;

ALTER TABLE water_sessions
  ALTER COLUMN field_worker_id SET NOT NULL;

ALTER TABLE time_tracking_logs
  ALTER COLUMN field_worker_id SET NOT NULL;

-- time_tracking_notification_log.worker_id was originally nullable
-- (ON DELETE SET NULL on the old FK); preserve that contract.

-- ── 7. Drop the old worker tables ────────────────────────────────────────
--
-- CASCADE removes FK constraints and dependent indexes/triggers, but does
-- NOT drop the foreign-key columns from unrelated tables. Drop those
-- explicitly so downstream tables carry only the new `field_worker_id`.

DROP TABLE IF EXISTS water_irrigators CASCADE;
DROP TABLE IF EXISTS time_tracking_workers CASCADE;

ALTER TABLE water_log_entries DROP COLUMN IF EXISTS irrigator_id;
ALTER TABLE water_sessions DROP COLUMN IF EXISTS irrigator_id;
ALTER TABLE time_tracking_logs DROP COLUMN IF EXISTS worker_id;
-- time_tracking_notification_log.worker_id was originally nullable
-- (ON DELETE SET NULL); we keep the column shape and just drop the old FK
-- constraint. The new `field_worker_id` column was added above and
-- backfilled. We drop the old column only if the new one is populated.
DO $$
BEGIN
  IF NOT EXISTS (
    SELECT 1 FROM time_tracking_notification_log WHERE field_worker_id IS NULL
  ) THEN
    ALTER TABLE time_tracking_notification_log DROP COLUMN worker_id;
  END IF;
END $$;

-- ── 8. Reissue the partial unique index on the new column ─────────────────
--
-- DB-level invariant preserved: at most one open clock-in per worker.

DROP INDEX IF EXISTS time_tracking_logs_one_open_per_worker_idx;

CREATE UNIQUE INDEX time_tracking_logs_one_open_per_worker_idx
  ON time_tracking_logs (field_worker_id)
  WHERE clock_out IS NULL;

-- ── 9. Summary of row counts after migration (for the audit log) ─────────
--
-- Run this manually after the migration completes if you want to confirm:
--   SELECT (SELECT count(*) FROM field_workers) AS field_workers,
--          (SELECT count(*) FROM water_log_entries WHERE field_worker_id IS NULL) AS wle_null,
--          (SELECT count(*) FROM time_tracking_logs WHERE field_worker_id IS NULL) AS ttl_null;