CREATE TABLE IF NOT EXISTS gateway_access_rule_allow_archive (
  migration_batch text NOT NULL,
  rule_id uuid NOT NULL,
  subject_type text NOT NULL,
  subject_id uuid NOT NULL,
  resource_type text NOT NULL,
  resource_id uuid NOT NULL,
  effect text NOT NULL,
  priority integer NOT NULL,
  min_permission_level integer NOT NULL,
  conditions jsonb NOT NULL,
  metadata jsonb NOT NULL,
  status text NOT NULL,
  rule_created_at timestamptz NOT NULL,
  rule_updated_at timestamptz NOT NULL,
  row_sha256 text NOT NULL CHECK (row_sha256 ~ '^[0-9a-f]{64}$'),
  archived_at timestamptz NOT NULL DEFAULT now(),
  PRIMARY KEY (migration_batch, rule_id)
);

CREATE INDEX IF NOT EXISTS idx_gateway_access_rule_allow_archive_subject
  ON gateway_access_rule_allow_archive(migration_batch, subject_type, subject_id);

CREATE TABLE IF NOT EXISTS gateway_access_rule_migration_batches (
  migration_batch text PRIMARY KEY,
  allow_count bigint NOT NULL,
  allow_sha256 text NOT NULL CHECK (allow_sha256 ~ '^[0-9a-f]{64}$'),
  deny_count bigint NOT NULL,
  deny_sha256 text NOT NULL CHECK (deny_sha256 ~ '^[0-9a-f]{64}$'),
  archived_at timestamptz NOT NULL DEFAULT now()
);

-- Reads stay available while access-rule writes are paused for the atomic
-- snapshot, archive, and cleanup sequence.
LOCK TABLE gateway_access_rules IN SHARE ROW EXCLUSIVE MODE;

INSERT INTO gateway_access_rule_migration_batches (
  migration_batch, allow_count, allow_sha256, deny_count, deny_sha256
)
SELECT
  '0102_access_rule_allow_whitelist_semantics',
  COUNT(*) FILTER (WHERE effect = 'allow'),
  encode(digest(COALESCE(string_agg(
    encode(digest(concat_ws(chr(31),
      id::text, subject_type, subject_id::text, resource_type,
      resource_id::text, effect, priority::text,
      min_permission_level::text, conditions::text, metadata::text,
      status, extract(epoch FROM created_at)::text,
      extract(epoch FROM updated_at)::text
    ), 'sha256'), 'hex'), chr(10) ORDER BY id
  ) FILTER (WHERE effect = 'allow'), ''), 'sha256'), 'hex'),
  COUNT(*) FILTER (WHERE effect = 'deny'),
  encode(digest(COALESCE(string_agg(
    encode(digest(concat_ws(chr(31),
      id::text, subject_type, subject_id::text, resource_type,
      resource_id::text, effect, priority::text,
      min_permission_level::text, conditions::text, metadata::text,
      status, extract(epoch FROM created_at)::text,
      extract(epoch FROM updated_at)::text
    ), 'sha256'), 'hex'), chr(10) ORDER BY id
  ) FILTER (WHERE effect = 'deny'), ''), 'sha256'), 'hex')
FROM gateway_access_rules
ON CONFLICT (migration_batch) DO NOTHING;

INSERT INTO gateway_access_rule_allow_archive (
  migration_batch, rule_id, subject_type, subject_id, resource_type,
  resource_id, effect, priority, min_permission_level, conditions,
  metadata, status, rule_created_at, rule_updated_at, row_sha256
)
SELECT
  '0102_access_rule_allow_whitelist_semantics', id, subject_type, subject_id,
  resource_type, resource_id, effect, priority, min_permission_level,
  conditions, metadata, status, created_at, updated_at,
  encode(digest(concat_ws(chr(31),
    id::text, subject_type, subject_id::text, resource_type,
    resource_id::text, effect, priority::text,
    min_permission_level::text, conditions::text, metadata::text,
    status, extract(epoch FROM created_at)::text,
    extract(epoch FROM updated_at)::text
  ), 'sha256'), 'hex')
FROM gateway_access_rules
WHERE effect = 'allow'
ON CONFLICT (migration_batch, rule_id) DO NOTHING;

DO $$
DECLARE
  expected_count bigint;
  expected_sha256 text;
  archived_count bigint;
  archived_sha256 text;
BEGIN
  SELECT allow_count, allow_sha256
  INTO expected_count, expected_sha256
  FROM gateway_access_rule_migration_batches
  WHERE migration_batch = '0102_access_rule_allow_whitelist_semantics';

  SELECT COUNT(*), encode(digest(COALESCE(string_agg(
    row_sha256, chr(10) ORDER BY rule_id
  ), ''), 'sha256'), 'hex')
  INTO archived_count, archived_sha256
  FROM gateway_access_rule_allow_archive
  WHERE migration_batch = '0102_access_rule_allow_whitelist_semantics';

  IF archived_count <> expected_count OR archived_sha256 <> expected_sha256 THEN
    RAISE EXCEPTION 'access-rule allow archive verification failed';
  END IF;
END
$$;

DELETE FROM gateway_access_rules
WHERE effect = 'allow';

DO $$
DECLARE
  expected_deny_count bigint;
  expected_deny_sha256 text;
  actual_allow_count bigint;
  actual_deny_count bigint;
  actual_deny_sha256 text;
BEGIN
  SELECT deny_count, deny_sha256
  INTO expected_deny_count, expected_deny_sha256
  FROM gateway_access_rule_migration_batches
  WHERE migration_batch = '0102_access_rule_allow_whitelist_semantics';

  SELECT
    COUNT(*) FILTER (WHERE effect = 'allow'),
    COUNT(*) FILTER (WHERE effect = 'deny'),
    encode(digest(COALESCE(string_agg(
      encode(digest(concat_ws(chr(31),
        id::text, subject_type, subject_id::text, resource_type,
        resource_id::text, effect, priority::text,
        min_permission_level::text, conditions::text, metadata::text,
        status, extract(epoch FROM created_at)::text,
        extract(epoch FROM updated_at)::text
      ), 'sha256'), 'hex'), chr(10) ORDER BY id
    ) FILTER (WHERE effect = 'deny'), ''), 'sha256'), 'hex')
  INTO actual_allow_count, actual_deny_count, actual_deny_sha256
  FROM gateway_access_rules;

  IF actual_allow_count <> 0 OR
     actual_deny_count <> expected_deny_count OR
     actual_deny_sha256 <> expected_deny_sha256 THEN
    RAISE EXCEPTION 'access-rule whitelist migration verification failed';
  END IF;
END
$$;
