-- 0008_mai_projects_sync.sql
-- Bidirectional sync between projax.items and mai.projects so projax becomes
-- the single source of truth for project identity/metadata while mai keeps a
-- FK-compatible table for its workers/tasks/sessions/messages/metrics.
--
-- Manual prereq (operator runs once on msupabase as superuser, NOT in a
-- migration — RLS+grant ownership lives outside projax_admin's reach):
--
--   GRANT INSERT, UPDATE, DELETE, TRIGGER ON mai.projects TO projax_admin;
--   DROP POLICY IF EXISTS projax_write ON mai.projects;
--   CREATE POLICY projax_write ON mai.projects FOR ALL TO projax_admin
--     USING (true) WITH CHECK (true);
--
-- Trust model: projax_admin is intentionally widened on mai.projects only
-- (single-tenant fleet, m only). Other mai.* tables remain off-limits.
--
-- Cycle prevention is twofold:
--   * pg_trigger_depth() > 1 skip — handles the natural recursion case where
--     the forward trigger's UPDATE on mai.projects fires the reverse trigger
--     within the same call stack.
--   * projax.in_sync GUC — belt-and-braces; set on entry, cleared on exit.

-- --------------------------------------------------------------------------
-- Forward: projax.items -> mai.projects (runs as caller, projax_admin)
-- --------------------------------------------------------------------------
CREATE OR REPLACE FUNCTION projax.sync_to_mai()
RETURNS trigger
LANGUAGE plpgsql
SET search_path = projax, mai, public
AS $$
DECLARE
  was_mai bool;
  is_mai  bool;
  mai_id  text;
  mapped_status text;
BEGIN
  IF pg_trigger_depth() > 1 THEN RETURN COALESCE(NEW, OLD); END IF;
  IF current_setting('projax.in_sync', true) = 'on' THEN RETURN COALESCE(NEW, OLD); END IF;
  PERFORM set_config('projax.in_sync', 'on', true);

  IF TG_OP = 'INSERT' THEN
    IF 'mai' = ANY(NEW.management) AND NEW.deleted_at IS NULL THEN
      mapped_status := CASE NEW.status WHEN 'done' THEN 'archived' ELSE NEW.status END;
      INSERT INTO mai.projects (id, name, goal, repo, path, memory_group, status, metadata)
      VALUES (
        NEW.slug,
        NEW.title,
        NEW.content_md,
        COALESCE(NEW.metadata->>'repo', ''),
        COALESCE(NEW.metadata->>'path', ''),
        COALESCE(NEW.metadata->>'memory_group', ''),
        mapped_status,
        COALESCE(NEW.metadata, '{}'::jsonb)
      )
      ON CONFLICT (id) DO UPDATE SET
        name         = EXCLUDED.name,
        goal         = EXCLUDED.goal,
        repo         = EXCLUDED.repo,
        path         = EXCLUDED.path,
        memory_group = EXCLUDED.memory_group,
        status       = EXCLUDED.status,
        metadata     = EXCLUDED.metadata,
        updated_at   = now();
      INSERT INTO projax.item_links (item_id, ref_type, ref_id, rel)
        VALUES (NEW.id, 'mai-project', NEW.slug, 'derived-from')
        ON CONFLICT (item_id, ref_type, ref_id, rel) DO NOTHING;
    END IF;

  ELSIF TG_OP = 'UPDATE' THEN
    was_mai := 'mai' = ANY(OLD.management) AND OLD.deleted_at IS NULL;
    is_mai  := 'mai' = ANY(NEW.management) AND NEW.deleted_at IS NULL;

    SELECT ref_id INTO mai_id
      FROM projax.item_links
      WHERE item_id = NEW.id AND ref_type = 'mai-project'
      LIMIT 1;

    IF is_mai AND NOT was_mai THEN
      mapped_status := CASE NEW.status WHEN 'done' THEN 'archived' ELSE NEW.status END;
      INSERT INTO mai.projects (id, name, goal, repo, path, memory_group, status, metadata)
      VALUES (
        COALESCE(mai_id, NEW.slug),
        NEW.title,
        NEW.content_md,
        COALESCE(NEW.metadata->>'repo', ''),
        COALESCE(NEW.metadata->>'path', ''),
        COALESCE(NEW.metadata->>'memory_group', ''),
        mapped_status,
        COALESCE(NEW.metadata, '{}'::jsonb)
      )
      ON CONFLICT (id) DO UPDATE SET
        name         = EXCLUDED.name,
        goal         = EXCLUDED.goal,
        repo         = EXCLUDED.repo,
        path         = EXCLUDED.path,
        memory_group = EXCLUDED.memory_group,
        status       = EXCLUDED.status,
        metadata     = EXCLUDED.metadata,
        updated_at   = now();
      INSERT INTO projax.item_links (item_id, ref_type, ref_id, rel)
        VALUES (NEW.id, 'mai-project', COALESCE(mai_id, NEW.slug), 'derived-from')
        ON CONFLICT (item_id, ref_type, ref_id, rel) DO NOTHING;

    ELSIF was_mai AND NOT is_mai AND mai_id IS NOT NULL THEN
      -- mai management dropped → delete mai row + the pointer. FK-bound deletes
      -- (workers/tasks/sessions/messages/metrics) will fail and roll back the
      -- projax UPDATE — that is the intended safety net.
      DELETE FROM mai.projects WHERE id = mai_id;
      DELETE FROM projax.item_links
        WHERE item_id = NEW.id AND ref_type = 'mai-project';

    ELSIF is_mai AND mai_id IS NOT NULL THEN
      -- Both states managed by mai → propagate editable fields. We DO NOT
      -- change mai.projects.id (FK targets cannot be renamed), so projax slug
      -- and mai.id may drift after a rename — tolerated because the pointer
      -- in item_links remains stable.
      mapped_status := CASE NEW.status WHEN 'done' THEN 'archived' ELSE NEW.status END;
      UPDATE mai.projects SET
        name         = NEW.title,
        goal         = NEW.content_md,
        repo         = COALESCE(NEW.metadata->>'repo', ''),
        path         = COALESCE(NEW.metadata->>'path', ''),
        memory_group = COALESCE(NEW.metadata->>'memory_group', ''),
        status       = mapped_status,
        metadata     = COALESCE(NEW.metadata, '{}'::jsonb),
        updated_at   = now()
      WHERE id = mai_id;
    END IF;

  ELSIF TG_OP = 'DELETE' THEN
    IF 'mai' = ANY(OLD.management) THEN
      SELECT ref_id INTO mai_id
        FROM projax.item_links
        WHERE item_id = OLD.id AND ref_type = 'mai-project'
        LIMIT 1;
      IF mai_id IS NULL THEN
        mai_id := OLD.slug;
      END IF;
      DELETE FROM mai.projects WHERE id = mai_id;
    END IF;
  END IF;

  PERFORM set_config('projax.in_sync', 'off', true);
  RETURN COALESCE(NEW, OLD);
EXCEPTION WHEN OTHERS THEN
  PERFORM set_config('projax.in_sync', 'off', true);
  RAISE;
END;
$$;

DROP TRIGGER IF EXISTS items_sync_to_mai ON projax.items;
CREATE TRIGGER items_sync_to_mai
  AFTER INSERT OR UPDATE OR DELETE ON projax.items
  FOR EACH ROW EXECUTE FUNCTION projax.sync_to_mai();

-- --------------------------------------------------------------------------
-- Reverse: mai.projects -> projax.items (SECURITY DEFINER so writes by the
-- `mai` role can fan out into projax.items, which projax_admin owns)
-- --------------------------------------------------------------------------
CREATE OR REPLACE FUNCTION projax.sync_from_mai()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = projax, mai, public
AS $$
DECLARE
  pid uuid;
  mapped_status text;
BEGIN
  IF pg_trigger_depth() > 1 THEN RETURN COALESCE(NEW, OLD); END IF;
  IF current_setting('projax.in_sync', true) = 'on' THEN RETURN COALESCE(NEW, OLD); END IF;
  PERFORM set_config('projax.in_sync', 'on', true);

  IF TG_OP = 'INSERT' THEN
    SELECT i.id INTO pid
      FROM projax.items i
      JOIN projax.item_links l ON l.item_id = i.id
      WHERE l.ref_type = 'mai-project' AND l.ref_id = NEW.id
      LIMIT 1;
    IF pid IS NULL THEN
      mapped_status := CASE NEW.status WHEN 'sleeping' THEN 'archived' ELSE NEW.status END;
      INSERT INTO projax.items
        (kind, title, slug, parent_ids, content_md, metadata, status, management)
      VALUES (
        ARRAY['project']::text[],
        NEW.name,
        NEW.id,
        '{}'::uuid[],   -- root; /admin/classify will surface it for re-parenting
        COALESCE(NEW.goal, ''),
        jsonb_build_object(
          'repo',         COALESCE(NEW.repo, ''),
          'path',         COALESCE(NEW.path, ''),
          'memory_group', COALESCE(NEW.memory_group, '')
        ) || COALESCE(NEW.metadata, '{}'::jsonb),
        mapped_status,
        ARRAY['mai']::text[]
      )
      RETURNING id INTO pid;
      INSERT INTO projax.item_links (item_id, ref_type, ref_id, rel)
        VALUES (pid, 'mai-project', NEW.id, 'derived-from');
    END IF;

  ELSIF TG_OP = 'UPDATE' THEN
    mapped_status := CASE NEW.status WHEN 'sleeping' THEN 'archived' ELSE NEW.status END;
    UPDATE projax.items i SET
      title      = NEW.name,
      content_md = COALESCE(NEW.goal, ''),
      status     = mapped_status,
      metadata   = jsonb_build_object(
        'repo',         COALESCE(NEW.repo, ''),
        'path',         COALESCE(NEW.path, ''),
        'memory_group', COALESCE(NEW.memory_group, '')
      ) || COALESCE(NEW.metadata, '{}'::jsonb)
      FROM projax.item_links l
      WHERE l.item_id = i.id
        AND l.ref_type = 'mai-project'
        AND l.ref_id = NEW.id;

  ELSIF TG_OP = 'DELETE' THEN
    UPDATE projax.items i SET deleted_at = now()
      FROM projax.item_links l
      WHERE l.item_id = i.id
        AND l.ref_type = 'mai-project'
        AND l.ref_id = OLD.id
        AND i.deleted_at IS NULL;
  END IF;

  PERFORM set_config('projax.in_sync', 'off', true);
  RETURN COALESCE(NEW, OLD);
EXCEPTION WHEN OTHERS THEN
  PERFORM set_config('projax.in_sync', 'off', true);
  RAISE;
END;
$$;

DROP TRIGGER IF EXISTS mai_projects_sync_to_projax ON mai.projects;
CREATE TRIGGER mai_projects_sync_to_projax
  AFTER INSERT OR UPDATE OR DELETE ON mai.projects
  FOR EACH ROW EXECUTE FUNCTION projax.sync_from_mai();
