-- ============================================================================
-- 0098_time_tracking_timesheets.sql
--
-- Time Tracking — Phase 2 (Timesheets, GPS, Approval/Lock, Audit)
--
-- This migration extends the existing time-tracking schema with the
-- production / payroll features the field app + admin UI need:
--
--   1. New roles on `field_workers`              — `driver`, `supervisor`
--      (kept distinct from `worker` so admin UIs can filter / grant
--      approval rights without expanding the existing role semantics).
--
--   2. GPS verification on every clock-in / clock-out —
--      `time_tracking_logs` gains lat/lng/accuracy columns for both
--      events, plus a boolean `gps_verified` (false when the device
--      denied geolocation, returned an error, or low-accuracy fix).
--
--   3. Manual time entries —
--      `time_tracking_logs.entry_kind` becomes `'clock' | 'manual'`.
--      Manual rows carry `manual_reason` (the why-this-was-added text
--      required by payroll / labor law) and can be edited freely until
--      the parent timesheet is locked.
--
--   4. Edit audit fields on `time_tracking_logs` —
--      `edited_at`, `edited_by` (admin_user id), `edit_reason`. Filled
--      by every edit that lands on a row; never cleared.
--
--   5. NEW: `time_tracking_timesheets` — one row per (worker, pay
--      period). Statuses drive the approval workflow:
--        draft → submitted → approved (locked) → unlocked (admin only,
--                                                    audit note required)
--              → draft (after rejection)
--      When `locked_at IS NOT NULL` the underlying logs become
--      read-only to anyone except `platform_admin` / `time_admin`.
--
--   6. NEW: `time_tracking_audit_log` — append-only audit trail for
--      every mutating action on a timesheet or its entries. Every
--      server-action call that mutates a timesheet or a log row MUST
--      write a row here in the same transaction.
--
--   7. NEW: `time_tracking_supervisor_assignments` — many-to-many
--      (supervisor_worker_id, supervisee_worker_id). Supervisors only
--      see / approve timesheets for their assigned crew.
--
-- The migration is idempotent — every ALTER / CREATE uses IF NOT EXISTS
-- or a DO block, so re-running on a partially-applied DB is safe.
-- ============================================================================

-- ───────────────────────────────────────────────────────────────────────────
-- 1. Role enum extension
-- ───────────────────────────────────────────────────────────────────────────

-- Existing enum (migration 0097) is named `field_workers_role_check`
-- via a CHECK constraint, NOT a Postgres ENUM type. Verify by introspecting
-- pg_constraint before adding new values.
DO $$
DECLARE
  constraint_def TEXT;
BEGIN
  -- Fetch the current CHECK expression
  SELECT pg_get_constraintdef(oid)
    INTO constraint_def
    FROM pg_constraint
   WHERE conname = 'field_workers_role_check';

  IF constraint_def IS NOT NULL THEN
    -- Drop and recreate with the expanded vocabulary. The set is the union
    -- of all six role values the codebase now recognises.
    EXECUTE 'ALTER TABLE field_workers DROP CONSTRAINT field_workers_role_check';
  END IF;
END $$;

ALTER TABLE field_workers
  ADD CONSTRAINT field_workers_role_check
  CHECK (role IN ('worker', 'time_admin', 'irrigator', 'water_admin', 'driver', 'supervisor'));

-- Drop & recreate the Drizzle CHECK by hand if it exists under a different name
-- (drizzle-kit may have generated `field_workers_role_check1`).
DO $$
DECLARE
  cname TEXT;
BEGIN
  FOR cname IN
    SELECT conname
      FROM pg_constraint
     WHERE conrelid = 'field_workers'::regclass
       AND contype = 'c'
       AND pg_get_constraintdef(oid) LIKE '%role%'
       AND conname <> 'field_workers_role_check'
  LOOP
    EXECUTE format('ALTER TABLE field_workers DROP CONSTRAINT %I', cname);
  END LOOP;
END $$;

-- ───────────────────────────────────────────────────────────────────────────
-- 2. GPS columns + entry_kind + manual_reason + edit audit on logs
-- ───────────────────────────────────────────────────────────────────────────

ALTER TABLE time_tracking_logs
  ADD COLUMN IF NOT EXISTS clock_in_lat         DOUBLE PRECISION,
  ADD COLUMN IF NOT EXISTS clock_in_lng         DOUBLE PRECISION,
  ADD COLUMN IF NOT EXISTS clock_in_accuracy_m  DOUBLE PRECISION,
  ADD COLUMN IF NOT EXISTS clock_out_lat        DOUBLE PRECISION,
  ADD COLUMN IF NOT EXISTS clock_out_lng        DOUBLE PRECISION,
  ADD COLUMN IF NOT EXISTS clock_out_accuracy_m DOUBLE PRECISION,
  ADD COLUMN IF NOT EXISTS gps_verified         BOOLEAN NOT NULL DEFAULT FALSE,
  ADD COLUMN IF NOT EXISTS entry_kind           TEXT NOT NULL DEFAULT 'clock',
  ADD COLUMN IF NOT EXISTS manual_reason        TEXT,
  ADD COLUMN IF NOT EXISTS edited_at            TIMESTAMPTZ,
  ADD COLUMN IF NOT EXISTS edited_by            UUID REFERENCES admin_users(id) ON DELETE SET NULL,
  ADD COLUMN IF NOT EXISTS edit_reason          TEXT;

-- entry_kind CHECK (idempotent)
DO $$
BEGIN
  IF NOT EXISTS (
    SELECT 1 FROM pg_constraint
     WHERE conname = 'time_tracking_logs_entry_kind_check'
  ) THEN
    ALTER TABLE time_tracking_logs
      ADD CONSTRAINT time_tracking_logs_entry_kind_check
      CHECK (entry_kind IN ('clock', 'manual'));
  END IF;
END $$;

-- Index for fast "all manual entries this period" queries used by payroll
CREATE INDEX IF NOT EXISTS time_tracking_logs_brand_kind_idx
  ON time_tracking_logs (brand_id, entry_kind, clock_in);

-- Index for the approval queue (entries that belong to a timesheet status)
CREATE INDEX IF NOT EXISTS time_tracking_logs_clock_in_idx
  ON time_tracking_logs (clock_in);

-- ───────────────────────────────────────────────────────────────────────────
-- 3. time_tracking_timesheets (the workflow entity)
-- ───────────────────────────────────────────────────────────────────────────

CREATE TABLE IF NOT EXISTS time_tracking_timesheets (
  id                    UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  brand_id              UUID NOT NULL REFERENCES brands(id) ON DELETE CASCADE,
  field_worker_id       UUID NOT NULL REFERENCES field_workers(id) ON DELETE CASCADE,
  period_start          DATE NOT NULL,
  period_end            DATE NOT NULL,

  -- Workflow state
  status                TEXT NOT NULL DEFAULT 'draft',

  -- Submission
  submitted_at          TIMESTAMPTZ,
  submitted_by          UUID REFERENCES admin_users(id) ON DELETE SET NULL,
  submitted_notes       TEXT,

  -- Approval (locks the timesheet)
  reviewed_at           TIMESTAMPTZ,
  reviewed_by           UUID REFERENCES admin_users(id) ON DELETE SET NULL,
  reviewer_comment      TEXT,

  -- Rejection (sends back to draft)
  rejected_at           TIMESTAMPTZ,
  rejected_by           UUID REFERENCES admin_users(id) ON DELETE SET NULL,
  rejection_reason      TEXT,

  -- Lock state (mirrors reviewed_at for fast filtering)
  locked_at             TIMESTAMPTZ,
  locked_by             UUID REFERENCES admin_users(id) ON DELETE SET NULL,

  -- Unlock state (rare, admin-only, MUST carry a note)
  unlocked_at           TIMESTAMPTZ,
  unlocked_by           UUID REFERENCES admin_users(id) ON DELETE SET NULL,
  unlock_audit_note     TEXT NOT NULL DEFAULT '',

  -- Pre-computed totals — recomputed by triggers / RPCs, but cached here
  -- so the timesheet list view doesn't re-aggregate on every render.
  total_minutes         INTEGER NOT NULL DEFAULT 0,
  regular_minutes       INTEGER NOT NULL DEFAULT 0,
  overtime_minutes      INTEGER NOT NULL DEFAULT 0,
  break_minutes         INTEGER NOT NULL DEFAULT 0,
  entry_count           INTEGER NOT NULL DEFAULT 0,

  -- Daily-OT flash cache: JSONB { 'YYYY-MM-DD': minutes }
  daily_totals          JSONB NOT NULL DEFAULT '{}'::jsonb,

  created_at            TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  updated_at            TIMESTAMPTZ NOT NULL DEFAULT NOW(),

  CONSTRAINT time_tracking_timesheets_period_check
    CHECK (period_end >= period_start),
  CONSTRAINT time_tracking_timesheets_status_check
    CHECK (status IN ('draft', 'submitted', 'approved', 'rejected'))
);

CREATE UNIQUE INDEX IF NOT EXISTS time_tracking_timesheets_worker_period_uniq
  ON time_tracking_timesheets (field_worker_id, period_start, period_end);

CREATE INDEX IF NOT EXISTS time_tracking_timesheets_brand_status_idx
  ON time_tracking_timesheets (brand_id, status, period_start DESC);

CREATE INDEX IF NOT EXISTS time_tracking_timesheets_brand_worker_idx
  ON time_tracking_timesheets (brand_id, field_worker_id);

-- ───────────────────────────────────────────────────────────────────────────
-- 4. time_tracking_audit_log (append-only)
-- ───────────────────────────────────────────────────────────────────────────

CREATE TABLE IF NOT EXISTS time_tracking_audit_log (
  id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  brand_id        UUID NOT NULL REFERENCES brands(id) ON DELETE CASCADE,
  actor_id        UUID REFERENCES admin_users(id) ON DELETE SET NULL,
  actor_label     TEXT NOT NULL,
  actor_kind      TEXT NOT NULL DEFAULT 'admin',
  action          TEXT NOT NULL,
  entity_type     TEXT NOT NULL,
  entity_id       UUID,
  details         JSONB NOT NULL DEFAULT '{}'::jsonb,
  created_at      TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS time_tracking_audit_log_brand_recent_idx
  ON time_tracking_audit_log (brand_id, created_at DESC);

CREATE INDEX IF NOT EXISTS time_tracking_audit_log_entity_idx
  ON time_tracking_audit_log (entity_type, entity_id);

-- ───────────────────────────────────────────────────────────────────────────
-- 5. Supervisor ↔ supervisee assignments
-- ───────────────────────────────────────────────────────────────────────────

CREATE TABLE IF NOT EXISTS time_tracking_supervisor_assignments (
  id                       UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  brand_id                 UUID NOT NULL REFERENCES brands(id) ON DELETE CASCADE,
  supervisor_field_worker_id UUID NOT NULL REFERENCES field_workers(id) ON DELETE CASCADE,
  supervisee_field_worker_id UUID NOT NULL REFERENCES field_workers(id) ON DELETE CASCADE,
  created_at               TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  CONSTRAINT time_tracking_supervisor_assignments_no_self
    CHECK (supervisor_field_worker_id <> supervisee_field_worker_id)
);

CREATE UNIQUE INDEX IF NOT EXISTS time_tracking_supervisor_assignments_uniq
  ON time_tracking_supervisor_assignments (
    supervisor_field_worker_id, supervisee_field_worker_id
  );

CREATE INDEX IF NOT EXISTS time_tracking_supervisor_assignments_supervisor_idx
  ON time_tracking_supervisor_assignments (supervisor_field_worker_id);

-- ───────────────────────────────────────────────────────────────────────────
-- 6. RLS — minimal, since the platform already relies on app-layer
--    brand scoping via SECURITY DEFINER / Drizzle `withBrand` wrappers.
--    The new tables are added to the same RLS scaffolding as the existing
--    `time_tracking_*` tables when present; otherwise left open at the
--    table level (the app enforces isolation).
-- ───────────────────────────────────────────────────────────────────────────

DO $$
DECLARE
  rls_enabled BOOLEAN;
BEGIN
  -- If RLS is enabled on time_tracking_logs, mirror it on the new tables.
  SELECT relrowsecurity INTO rls_enabled
    FROM pg_class
   WHERE relname = 'time_tracking_logs';

  IF rls_enabled THEN
    EXECUTE 'ALTER TABLE time_tracking_timesheets ENABLE ROW LEVEL SECURITY';
    EXECUTE 'ALTER TABLE time_tracking_audit_log ENABLE ROW LEVEL SECURITY';
    EXECUTE 'ALTER TABLE time_tracking_supervisor_assignments ENABLE ROW LEVEL SECURITY';
  END IF;
END $$;

-- ───────────────────────────────────────────────────────────────────────────
-- 7. Triggers — keep `updated_at` fresh
-- ───────────────────────────────────────────────────────────────────────────

CREATE OR REPLACE FUNCTION time_tracking_set_updated_at()
RETURNS TRIGGER AS $$
BEGIN
  NEW.updated_at = NOW();
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

DROP TRIGGER IF EXISTS time_tracking_timesheets_set_updated_at
  ON time_tracking_timesheets;
CREATE TRIGGER time_tracking_timesheets_set_updated_at
  BEFORE UPDATE ON time_tracking_timesheets
  FOR EACH ROW EXECUTE FUNCTION time_tracking_set_updated_at();

-- ───────────────────────────────────────────────────────────────────────────
-- 8. Helper view — admin "what's pending" dashboard
-- ───────────────────────────────────────────────────────────────────────────

CREATE OR REPLACE VIEW time_tracking_approval_queue AS
SELECT
  t.id,
  t.brand_id,
  t.field_worker_id,
  fw.name AS worker_name,
  t.period_start,
  t.period_end,
  t.status,
  t.total_minutes,
  t.regular_minutes,
  t.overtime_minutes,
  t.entry_count,
  t.submitted_at,
  t.submitted_notes,
  t.reviewed_at,
  t.locked_at
FROM time_tracking_timesheets t
JOIN field_workers fw ON fw.id = t.field_worker_id;

-- ───────────────────────────────────────────────────────────────────────────
-- 9. SECURITY DEFINER RPC — recompute timesheet totals
--    Called by the server after any entry add/edit/delete so the cached
--    totals on `time_tracking_timesheets` stay in sync with the underlying
--    `time_tracking_logs`.
-- ───────────────────────────────────────────────────────────────────────────

CREATE OR REPLACE FUNCTION recompute_timesheet_totals(p_timesheet_id UUID)
RETURNS VOID AS $$
DECLARE
  v_brand_id     UUID;
  v_worker_id    UUID;
  v_period_start DATE;
  v_period_end   DATE;
  v_total        INTEGER;
  v_regular      INTEGER;
  v_ot           INTEGER;
  v_break        INTEGER;
  v_count        INTEGER;
  v_daily        JSONB;
  v_daily_thresh NUMERIC;
  v_settings_brand UUID;
BEGIN
  -- Load the timesheet header
  SELECT brand_id, field_worker_id, period_start, period_end
    INTO v_brand_id, v_worker_id, v_period_start, v_period_end
    FROM time_tracking_timesheets
   WHERE id = p_timesheet_id;

  IF v_brand_id IS NULL THEN
    RETURN;
  END IF;

  -- Pull daily OT threshold from settings (default 8h)
  SELECT COALESCE(daily_overtime_threshold, 8)
    INTO v_daily_thresh
    FROM time_tracking_settings
   WHERE brand_id = v_brand_id;
  v_daily_thresh := COALESCE(v_daily_thresh, 8);

  -- Aggregate underlying logs
  SELECT
    COALESCE(SUM(
      GREATEST(0,
        EXTRACT(EPOCH FROM (COALESCE(clock_out, NOW()) - clock_in)) / 60
        - COALESCE(lunch_break_minutes, 0)
      )
    ), 0)::INTEGER,
    COALESCE(SUM(lunch_break_minutes), 0)::INTEGER,
    COUNT(*)::INTEGER,
    jsonb_object_agg(
      d::text, mins
    ) FILTER (WHERE d IS NOT NULL)
  INTO v_total, v_break, v_count, v_daily
  FROM (
    SELECT
      (clock_in AT TIME ZONE 'America/Denver')::date AS d,
      SUM(
        GREATEST(0,
          EXTRACT(EPOCH FROM (COALESCE(clock_out, NOW()) - clock_in)) / 60
          - COALESCE(lunch_break_minutes, 0)
        )
      ) AS mins
    FROM time_tracking_logs
    WHERE brand_id = v_brand_id
      AND field_worker_id = v_worker_id
      AND clock_in >= v_period_start
      AND clock_in < (v_period_end + INTERVAL '1 day')
    GROUP BY 1
  ) daily_agg
  RIGHT JOIN time_tracking_logs l
    ON l.brand_id = v_brand_id
   AND l.field_worker_id = v_worker_id
   AND l.clock_in >= v_period_start
   AND l.clock_in < (v_period_end + INTERVAL '1 day');

  v_daily := COALESCE(v_daily, '{}'::jsonb);

  -- Daily OT = sum across days of (daily_mins - threshold*60)+
  v_ot := 0;
  IF jsonb_typeof(v_daily) = 'object' THEN
    SELECT COALESCE(SUM(GREATEST(0, (v.value::numeric - v_daily_thresh * 60))), 0)::INTEGER
      INTO v_ot
      FROM jsonb_each(v_daily) v;
  END IF;

  v_regular := GREATEST(0, v_total - v_ot);

  UPDATE time_tracking_timesheets
     SET total_minutes    = v_total,
         regular_minutes  = v_regular,
         overtime_minutes = v_ot,
         break_minutes    = v_break,
         entry_count      = v_count,
         daily_totals     = v_daily
   WHERE id = p_timesheet_id;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

-- ───────────────────────────────────────────────────────────────────────────
-- 10. SECURITY DEFINER RPC — get-or-create a timesheet for a worker × period
-- ───────────────────────────────────────────────────────────────────────────

CREATE OR REPLACE FUNCTION get_or_create_timesheet(
  p_brand_id    UUID,
  p_worker_id   UUID,
  p_period_start DATE,
  p_period_end   DATE
)
RETURNS UUID AS $$
DECLARE
  v_id UUID;
BEGIN
  SELECT id INTO v_id
    FROM time_tracking_timesheets
   WHERE field_worker_id = p_worker_id
     AND period_start    = p_period_start
     AND period_end      = p_period_end;

  IF v_id IS NULL THEN
    INSERT INTO time_tracking_timesheets
      (brand_id, field_worker_id, period_start, period_end)
    VALUES
      (p_brand_id, p_worker_id, p_period_start, p_period_end)
    RETURNING id INTO v_id;

    PERFORM recompute_timesheet_totals(v_id);
  END IF;

  RETURN v_id;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;