-- =====================================================
-- Member Audit Log
--
-- Full change history for compliance: who changed what
-- field, old value→new value, when. Plus activity timeline.
-- =====================================================

-- 1. Audit log table
CREATE TABLE IF NOT EXISTS public.member_audit_log (
  id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  member_id uuid NOT NULL REFERENCES public.members(id) ON DELETE CASCADE,
  account_id uuid NOT NULL REFERENCES public.accounts(id) ON DELETE CASCADE,
  user_id uuid REFERENCES auth.users(id) ON DELETE SET NULL,
  action text NOT NULL CHECK (action IN (
    'created', 'updated', 'status_changed', 'archived', 'unarchived',
    'department_assigned', 'department_removed',
    'role_assigned', 'role_removed',
    'honor_awarded', 'honor_removed',
    'mandate_created', 'mandate_updated', 'mandate_revoked',
    'transferred', 'merged',
    'application_approved', 'application_rejected',
    'portal_invited', 'portal_linked',
    'card_generated',
    'imported', 'exported',
    'gdpr_consent_changed', 'gdpr_anonymized',
    'tag_added', 'tag_removed',
    'communication_logged', 'note_added',
    'bulk_status_changed', 'bulk_archived'
  )),
  changes jsonb NOT NULL DEFAULT '{}',
  metadata jsonb NOT NULL DEFAULT '{}',
  created_at timestamptz NOT NULL DEFAULT now()
);

COMMENT ON TABLE public.member_audit_log IS
  'Immutable audit trail for all member lifecycle events';

CREATE INDEX ix_member_audit_member
  ON public.member_audit_log(member_id, created_at DESC);
CREATE INDEX ix_member_audit_account
  ON public.member_audit_log(account_id, created_at DESC);
CREATE INDEX ix_member_audit_user
  ON public.member_audit_log(user_id)
  WHERE user_id IS NOT NULL;
CREATE INDEX ix_member_audit_action
  ON public.member_audit_log(account_id, action);

ALTER TABLE public.member_audit_log ENABLE ROW LEVEL SECURITY;
REVOKE ALL ON public.member_audit_log FROM authenticated, service_role;
GRANT SELECT ON public.member_audit_log TO authenticated;
GRANT ALL ON public.member_audit_log TO service_role;

-- Read access: must have role on the account
CREATE POLICY member_audit_log_select
  ON public.member_audit_log FOR SELECT TO authenticated
  USING (public.has_role_on_account(account_id));

-- No direct insert/update/delete for authenticated — only via SECURITY DEFINER functions

-- 2. Auto-audit trigger on members UPDATE
-- Computes field-by-field diff and classifies the action type.
CREATE OR REPLACE FUNCTION public.trg_member_audit_on_update()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = ''
AS $$
DECLARE
  v_changes jsonb := '{}'::jsonb;
  v_user_id uuid;
  v_action text;
  v_old jsonb;
  v_new jsonb;
  v_key text;
BEGIN
  v_user_id := nullif(current_setting('app.current_user_id', true), '')::uuid;
  v_old := to_jsonb(OLD);
  v_new := to_jsonb(NEW);

  -- Compare each field, skip meta columns
  FOR v_key IN
    SELECT jsonb_object_keys(v_new)
    EXCEPT
    SELECT unnest(ARRAY['updated_at', 'updated_by', 'version'])
  LOOP
    IF (v_old -> v_key) IS DISTINCT FROM (v_new -> v_key) THEN
      v_changes := v_changes || jsonb_build_object(
        v_key, jsonb_build_object('old', v_old -> v_key, 'new', v_new -> v_key)
      );
    END IF;
  END LOOP;

  -- Skip if nothing actually changed
  IF v_changes = '{}'::jsonb THEN
    RETURN NEW;
  END IF;

  -- Classify the action
  IF (v_old ->> 'status') IS DISTINCT FROM (v_new ->> 'status') THEN
    v_action := 'status_changed';
  ELSIF (v_old ->> 'is_archived') IS DISTINCT FROM (v_new ->> 'is_archived')
    AND COALESCE((v_new ->> 'is_archived'), 'false') = 'true' THEN
    v_action := 'archived';
  ELSIF (v_old ->> 'is_archived') IS DISTINCT FROM (v_new ->> 'is_archived') THEN
    v_action := 'unarchived';
  ELSE
    v_action := 'updated';
  END IF;

  INSERT INTO public.member_audit_log (member_id, account_id, user_id, action, changes)
  VALUES (NEW.id, NEW.account_id, v_user_id, v_action, v_changes);

  RETURN NEW;
END;
$$;

CREATE TRIGGER trg_members_audit_on_update
  AFTER UPDATE ON public.members
  FOR EACH ROW
  EXECUTE FUNCTION public.trg_member_audit_on_update();

-- 3. Auto-audit trigger on members INSERT
CREATE OR REPLACE FUNCTION public.trg_member_audit_on_insert()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = ''
AS $$
DECLARE
  v_user_id uuid;
BEGIN
  v_user_id := COALESCE(
    nullif(current_setting('app.current_user_id', true), '')::uuid,
    NEW.created_by
  );

  INSERT INTO public.member_audit_log (member_id, account_id, user_id, action, metadata)
  VALUES (
    NEW.id, NEW.account_id, v_user_id, 'created',
    jsonb_build_object(
      'member_number', NEW.member_number,
      'first_name', NEW.first_name,
      'last_name', NEW.last_name,
      'status', NEW.status
    )
  );

  RETURN NEW;
END;
$$;

CREATE TRIGGER trg_members_audit_on_insert
  AFTER INSERT ON public.members
  FOR EACH ROW
  EXECUTE FUNCTION public.trg_member_audit_on_insert();

-- 4. Helper function to log explicit audit events (for related tables)
CREATE OR REPLACE FUNCTION public.log_member_audit_event(
  p_member_id uuid,
  p_account_id uuid,
  p_action text,
  p_changes jsonb DEFAULT '{}',
  p_metadata jsonb DEFAULT '{}'
)
RETURNS void
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = ''
AS $$
BEGIN
  -- Verify caller has access to the account
  IF NOT public.has_role_on_account(p_account_id) THEN
    RAISE EXCEPTION 'Access denied' USING ERRCODE = '42501';
  END IF;

  -- Force user_id to be the actual caller
  INSERT INTO public.member_audit_log (member_id, account_id, user_id, action, changes, metadata)
  VALUES (p_member_id, p_account_id, auth.uid(), p_action, p_changes, p_metadata);
END;
$$;

GRANT EXECUTE ON FUNCTION public.log_member_audit_event(uuid, uuid, text, jsonb, jsonb)
  TO authenticated, service_role;

-- 5. Activity timeline RPC (read layer on audit log)
CREATE OR REPLACE FUNCTION public.get_member_timeline(
  p_member_id uuid,
  p_page int DEFAULT 1,
  p_page_size int DEFAULT 50,
  p_action_filter text DEFAULT NULL
)
RETURNS TABLE (
  id bigint,
  action text,
  changes jsonb,
  metadata jsonb,
  user_id uuid,
  user_display_name text,
  created_at timestamptz,
  total_count bigint
)
LANGUAGE plpgsql
STABLE
SECURITY DEFINER
SET search_path = ''
AS $$
DECLARE
  v_account_id uuid;
  v_total bigint;
  v_offset int;
BEGIN
  -- Get member's account for access check
  SELECT m.account_id INTO v_account_id
  FROM public.members m WHERE m.id = p_member_id;

  IF v_account_id IS NULL THEN
    RAISE EXCEPTION 'Member not found';
  END IF;

  IF NOT public.has_role_on_account(v_account_id) THEN
    RAISE EXCEPTION 'Access denied';
  END IF;

  -- Clamp page size to prevent unbounded queries
  p_page_size := LEAST(GREATEST(p_page_size, 1), 200);
  v_offset := GREATEST(0, (p_page - 1)) * p_page_size;

  -- Get total count
  SELECT count(*) INTO v_total
  FROM public.member_audit_log al
  WHERE al.member_id = p_member_id
    AND (p_action_filter IS NULL OR al.action = p_action_filter);

  -- Return paginated results with user names
  RETURN QUERY
  SELECT
    al.id,
    al.action,
    al.changes,
    al.metadata,
    al.user_id,
    COALESCE(
      u.raw_user_meta_data ->> 'display_name',
      u.email,
      al.user_id::text
    ) AS user_display_name,
    al.created_at,
    v_total AS total_count
  FROM public.member_audit_log al
  LEFT JOIN auth.users u ON u.id = al.user_id
  WHERE al.member_id = p_member_id
    AND (p_action_filter IS NULL OR al.action = p_action_filter)
  ORDER BY al.created_at DESC
  OFFSET v_offset
  LIMIT p_page_size;
END;
$$;

GRANT EXECUTE ON FUNCTION public.get_member_timeline(uuid, int, int, text)
  TO authenticated;
