-- ============================================================
-- Migration 039: Security & Performance fixes from Supabase linter reports
-- ============================================================

-- ============================================================
-- 1. CRITICAL: Enable RLS on organizations table (policies exist but RLS was off)
-- ============================================================
ALTER TABLE public.organizations ENABLE ROW LEVEL SECURITY;


-- ============================================================
-- 2. Fix mutable search_path on all public functions
--    Adds SET search_path = '' to make them immutable to search_path attacks
-- ============================================================

-- is_platform_admin
CREATE OR REPLACE FUNCTION public.is_platform_admin()
RETURNS boolean
LANGUAGE sql
STABLE
SECURITY DEFINER
SET search_path = ''
AS $$
  SELECT EXISTS (
    SELECT 1 FROM public.profiles
    WHERE id = (select auth.uid()) AND is_platform_admin = true
  );
$$;

-- is_org_member (used heavily in RLS)
CREATE OR REPLACE FUNCTION public.is_org_member(org_id uuid)
RETURNS boolean
LANGUAGE sql
STABLE
SECURITY DEFINER
SET search_path = ''
AS $$
  SELECT EXISTS (
    SELECT 1 FROM public.org_members
    WHERE org_members.org_id = is_org_member.org_id
      AND user_id = (select auth.uid())
  );
$$;

-- handle_new_user
CREATE OR REPLACE FUNCTION public.handle_new_user()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = ''
AS $$
BEGIN
  INSERT INTO public.profiles (id, email, full_name, avatar_url)
  VALUES (
    NEW.id,
    NEW.email,
    COALESCE(NEW.raw_user_meta_data->>'full_name', NEW.raw_user_meta_data->>'name'),
    NEW.raw_user_meta_data->>'avatar_url'
  );
  RETURN NEW;
END;
$$;

-- handle_new_org
CREATE OR REPLACE FUNCTION public.handle_new_org()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = ''
AS $$
BEGIN
  -- Add creator as owner
  INSERT INTO public.org_members (org_id, user_id, role)
  VALUES (NEW.id, NEW.created_by, 'owner');
  -- Create default roles
  PERFORM public.create_default_org_roles(NEW.id);
  RETURN NEW;
END;
$$;

-- create_default_org_roles
CREATE OR REPLACE FUNCTION public.create_default_org_roles(p_org_id uuid)
RETURNS void
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = ''
AS $$
BEGIN
  INSERT INTO public.org_roles (org_id, name, color, permissions, is_system, is_default)
  VALUES
    (p_org_id, 'Admin', '#ef4444', ARRAY['manage_members','manage_roles','manage_settings','manage_content','view_content','comment'], true, false),
    (p_org_id, 'Editor', '#3b82f6', ARRAY['manage_content','view_content','comment'], true, true),
    (p_org_id, 'Commenter', '#8b5cf6', ARRAY['view_content','comment'], true, false),
    (p_org_id, 'Viewer', '#6b7280', ARRAY['view_content'], true, false)
  ON CONFLICT DO NOTHING;
END;
$$;

-- compute_document_path
CREATE OR REPLACE FUNCTION public.compute_document_path(doc_id uuid)
RETURNS text
LANGUAGE plpgsql
STABLE
SET search_path = ''
AS $$
DECLARE
  result text := '';
  current_id uuid := doc_id;
  current_name text;
  parent uuid;
BEGIN
  LOOP
    SELECT d.name, d.parent_id INTO current_name, parent
    FROM public.documents d WHERE d.id = current_id;
    IF NOT FOUND THEN EXIT; END IF;
    IF result = '' THEN result := current_name;
    ELSE result := current_name || '/' || result;
    END IF;
    IF parent IS NULL THEN EXIT; END IF;
    current_id := parent;
  END LOOP;
  RETURN '/' || result;
END;
$$;

-- update_document_path
CREATE OR REPLACE FUNCTION public.update_document_path()
RETURNS trigger
LANGUAGE plpgsql
SET search_path = ''
AS $$
BEGIN
  NEW.path := public.compute_document_path(NEW.id);
  RETURN NEW;
END;
$$;

-- get_next_document_position
CREATE OR REPLACE FUNCTION public.get_next_document_position(p_org_id uuid, p_parent_id uuid)
RETURNS integer
LANGUAGE plpgsql
SET search_path = ''
AS $$
DECLARE
  max_pos integer;
BEGIN
  IF p_parent_id IS NULL THEN
    SELECT COALESCE(MAX(position), -1) + 1 INTO max_pos
    FROM public.documents WHERE org_id = p_org_id AND parent_id IS NULL;
  ELSE
    SELECT COALESCE(MAX(position), -1) + 1 INTO max_pos
    FROM public.documents WHERE org_id = p_org_id AND parent_id = p_parent_id;
  END IF;
  RETURN max_pos;
END;
$$;

-- update_matrix_credentials_updated_at
CREATE OR REPLACE FUNCTION public.update_matrix_credentials_updated_at()
RETURNS trigger
LANGUAGE plpgsql
SET search_path = ''
AS $$
BEGIN
  NEW.updated_at = now();
  RETURN NEW;
END;
$$;

-- handle_new_event
CREATE OR REPLACE FUNCTION public.handle_new_event()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = ''
AS $$
BEGIN
  PERFORM public.seed_event_task_columns(NEW.id);
  RETURN NEW;
END;
$$;

-- seed_event_task_columns
CREATE OR REPLACE FUNCTION public.seed_event_task_columns(p_event_id uuid)
RETURNS void
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = ''
AS $$
BEGIN
  INSERT INTO public.event_task_columns (event_id, name, position, color)
  VALUES
    (p_event_id, 'To Do', 0, '#6b7280'),
    (p_event_id, 'In Progress', 1, '#3b82f6'),
    (p_event_id, 'Done', 2, '#22c55e')
  ON CONFLICT DO NOTHING;
END;
$$;

-- create_department_dashboard
CREATE OR REPLACE FUNCTION public.create_department_dashboard()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = ''
AS $$
DECLARE
  dash_id uuid;
  checklist_id uuid;
  note_id uuid;
BEGIN
  INSERT INTO public.department_dashboards (department_id, created_by)
  VALUES (NEW.id, NEW.created_by)
  RETURNING id INTO dash_id;

  INSERT INTO public.dashboard_panels (dashboard_id, module_type, position, config)
  VALUES
    (dash_id, 'kanban', 0, '{}'),
    (dash_id, 'files', 1, '{}'),
    (dash_id, 'checklist', 2, '{}'),
    (dash_id, 'notes', 3, '{}');

  INSERT INTO public.department_checklists (department_id, title, created_by)
  VALUES (NEW.id, 'Getting Started', NEW.created_by)
  RETURNING id INTO checklist_id;

  INSERT INTO public.department_checklist_items (checklist_id, title, position)
  VALUES
    (checklist_id, 'Set up department goals', 0),
    (checklist_id, 'Assign team members', 1),
    (checklist_id, 'Create initial tasks', 2);

  INSERT INTO public.department_notes (department_id, title, content, created_by)
  VALUES (NEW.id, 'Welcome', 'Welcome to your department! Use this space for notes and documentation.', NEW.created_by);

  RETURN NEW;
END;
$$;


-- ============================================================
-- 3. Fix overly permissive RLS policy: org_members "Allow member inserts"
--    Replace WITH CHECK (true) with proper check
-- ============================================================
DROP POLICY IF EXISTS "Allow member inserts" ON public.org_members;
CREATE POLICY "Allow member inserts" ON public.org_members
  FOR INSERT
  WITH CHECK (
    -- Only allow if user is an admin/owner of the org, or it's the system (via trigger)
    EXISTS (
      SELECT 1 FROM public.org_members om
      WHERE om.org_id = org_members.org_id
        AND om.user_id = (select auth.uid())
        AND om.role IN ('owner', 'admin')
    )
    OR
    -- Allow the handle_new_org trigger (creator becomes owner)
    org_members.user_id = (select auth.uid())
  );


-- ============================================================
-- 4. Add missing RLS policies for tables with RLS enabled but no policies
-- ============================================================

-- card_assignees: inherit access from the card's board's org
CREATE POLICY "Card assignees inherit card access" ON public.card_assignees
  FOR ALL
  USING (
    EXISTS (
      SELECT 1 FROM public.kanban_cards kc
      JOIN public.kanban_columns kcol ON kcol.id = kc.column_id
      JOIN public.kanban_boards kb ON kb.id = kcol.board_id
      JOIN public.org_members om ON om.org_id = kb.org_id
      WHERE kc.id = card_assignees.card_id
        AND om.user_id = (select auth.uid())
    )
  );

-- event_attendees: org members can access
CREATE POLICY "Org members can manage event attendees" ON public.event_attendees
  FOR ALL
  USING (
    EXISTS (
      SELECT 1 FROM public.events e
      JOIN public.org_members om ON om.org_id = e.org_id
      WHERE e.id = event_attendees.event_id
        AND om.user_id = (select auth.uid())
    )
  );


-- ============================================================
-- 5. Add indexes on unindexed foreign keys (created_by, user_id, role_id, org_id)
-- ============================================================

CREATE INDEX IF NOT EXISTS idx_activity_log_user_id ON public.activity_log (user_id);
CREATE INDEX IF NOT EXISTS idx_budget_items_created_by ON public.budget_items (created_by);
CREATE INDEX IF NOT EXISTS idx_calendar_events_created_by ON public.calendar_events (created_by);
CREATE INDEX IF NOT EXISTS idx_card_assignees_user_id ON public.card_assignees (user_id);
CREATE INDEX IF NOT EXISTS idx_department_checklists_created_by ON public.department_checklists (created_by);
CREATE INDEX IF NOT EXISTS idx_department_contacts_created_by ON public.department_contacts (created_by);
CREATE INDEX IF NOT EXISTS idx_department_dashboards_created_by ON public.department_dashboards (created_by);
CREATE INDEX IF NOT EXISTS idx_department_notes_created_by ON public.department_notes (created_by);
CREATE INDEX IF NOT EXISTS idx_document_locks_user_id ON public.document_locks (user_id);
CREATE INDEX IF NOT EXISTS idx_documents_created_by ON public.documents (created_by);
CREATE INDEX IF NOT EXISTS idx_event_attendees_user_id ON public.event_attendees (user_id);
CREATE INDEX IF NOT EXISTS idx_event_members_role_id ON public.event_members (role_id);
CREATE INDEX IF NOT EXISTS idx_event_tasks_created_by ON public.event_tasks (created_by);
CREATE INDEX IF NOT EXISTS idx_events_created_by ON public.events (created_by);
CREATE INDEX IF NOT EXISTS idx_kanban_boards_created_by ON public.kanban_boards (created_by);
CREATE INDEX IF NOT EXISTS idx_kanban_cards_created_by ON public.kanban_cards (created_by);
CREATE INDEX IF NOT EXISTS idx_kanban_checklist_items_card_id ON public.kanban_checklist_items (card_id);
CREATE INDEX IF NOT EXISTS idx_matrix_credentials_org_id ON public.matrix_credentials (org_id);
CREATE INDEX IF NOT EXISTS idx_org_contacts_created_by ON public.org_contacts (created_by);
CREATE INDEX IF NOT EXISTS idx_org_google_calendars_connected_by ON public.org_google_calendars (connected_by);
CREATE INDEX IF NOT EXISTS idx_org_invites_invited_by ON public.org_invites (invited_by);
CREATE INDEX IF NOT EXISTS idx_org_invites_role_id ON public.org_invites (role_id);
CREATE INDEX IF NOT EXISTS idx_org_members_role_id ON public.org_members (role_id);
CREATE INDEX IF NOT EXISTS idx_schedule_blocks_created_by ON public.schedule_blocks (created_by);
CREATE INDEX IF NOT EXISTS idx_sponsor_allocations_created_by ON public.sponsor_allocations (created_by);
CREATE INDEX IF NOT EXISTS idx_sponsors_created_by ON public.sponsors (created_by);


-- ============================================================
-- 6. Drop unused indexes (never been used per Supabase linter)
-- ============================================================

DROP INDEX IF EXISTS public.idx_kanban_cards_assignee;
DROP INDEX IF EXISTS public.idx_kanban_cards_due_date;
DROP INDEX IF EXISTS public.idx_calendar_events_google;
DROP INDEX IF EXISTS public.idx_activity_log_entity;
DROP INDEX IF EXISTS public.idx_team_members_team;
DROP INDEX IF EXISTS public.idx_team_members_user;
DROP INDEX IF EXISTS public.idx_kanban_boards_team;
DROP INDEX IF EXISTS public.idx_kanban_boards_personal;
DROP INDEX IF EXISTS public.idx_kanban_comments_user;
DROP INDEX IF EXISTS public.idx_kanban_comments_created;
DROP INDEX IF EXISTS public.idx_document_locks_heartbeat;
DROP INDEX IF EXISTS public.idx_event_members_user;
DROP INDEX IF EXISTS public.idx_events_org;
DROP INDEX IF EXISTS public.idx_event_tasks_assignee;
DROP INDEX IF EXISTS public.idx_dept_checklist_items_assigned;
DROP INDEX IF EXISTS public.idx_schedule_blocks_time;
DROP INDEX IF EXISTS public.idx_department_contacts_category;
DROP INDEX IF EXISTS public.idx_budget_items_category;
DROP INDEX IF EXISTS public.idx_sponsors_tier;
DROP INDEX IF EXISTS public.idx_sponsors_status;
DROP INDEX IF EXISTS public.idx_sponsor_alloc_sponsor;
DROP INDEX IF EXISTS public.idx_org_contacts_category;


-- ============================================================
-- 7. NOTE: "Leaked password protection" is an Auth dashboard setting,
--    not fixable via migration. Enable it in:
--    Supabase Dashboard → Authentication → Settings → Password Security
-- ============================================================

-- ============================================================
-- 8. NOTE: RLS initplan warnings (auth.uid() → (select auth.uid()))
--    There are ~90 policies that use auth.uid() directly instead of
--    (select auth.uid()). This causes per-row re-evaluation.
--    The fix is mechanical: wrap auth.uid() in a subselect.
--    This is addressed in the new policies above. For existing policies
--    across all tables, a comprehensive rewrite would be very large.
--    We recommend addressing these incrementally per table as needed,
--    or in a dedicated follow-up migration.
-- ============================================================
