<reasoning_content>
*   Objective: Refactor a job queue into three specific classes: `Job`, `RetryPolicy`, and `JobQueue`.
    *   Constraint 1: Use `@dataclass` for `Job`.
    *   Constraint 2: `RetryPolicy` handles retries with backoff logic (but no actual sleeping).
    *   Constraint 3: `JobQueue` orchestrates `Job` and uses `RetryPolicy`.
    *    Constraint 4: `JobQueue` must maintain priority (highest first) and FIFO within priority.
    *    Constraint 5: Standard library only.
    *    Constraint 6: Write only code, no explanations.

    *   `Job` dataclass:
        *   `id: str`
        *   `data: dict`
        *   `priority: int = 0`
        *   `retries: int = 0`

    *   `RetryPolicy` class:
        *   `__init__(self, max_attempts: int = 4)`
        *   `run(self, fn: Callable, data) -> tuple[bool, int]`:
            *   Call `fn(data)`.
            *   Retry on exception.
            *   Return `(success, attempts_made)`.
            *   No `time.sleep`.

    *   `JobQueue` class:
        *   `add_job(self, job_id: str, data: dict, priority: int = 0) -> None`:
            *   Add a `Job` object to an internal storage.
        *   `process_job(self, job_id: str, processor: Callable) -> bool`:
            *   Must use `RetryPolicy`.
        *   `get_next_job(self) -> tuple[str, dict] | None`:
            *   Highest priority first.
            *   FIFO for same priority.
            *   Return `(id, data)` or `None`.

    *   To maintain priority and FIFO, a `heapq` is the standard way to do a priority queue in Python.
    *   Wait, `heapq` is a min-heap. For "highest priority first", I should negate the priority or use a custom comparison.
    *   For FIFO within same priority, I need a counter/sequence number because `Job` objects might not be comparable if only priority is used.
    *   Actually, since `Job` is a dataclass, if I put it in a heap, it might try to compare fields.
    *   Let's use a list and `heapq.heappush`. To ensure FIFO, I'll use an incrementing counter.

    *   `RetryPolicy.run` takes a `Callable` and `data`.
    *   It needs to handle any exception.
    *   It returns `(success, attempts_made)`.

    *   `JobQueue` needs to find the job by ID for `process_job`? Or does `process_job` imply it already knows which one?
    *   The prompt says: `process_job(self, job_id: str, processor: Callable) -> bool`.
    *   This implies searching the queue for the specific ID.

    *   Refining `JobQueue` internal structure:
        *   A list of jobs for `heapq`.
        *   A dictionary to quickly find jobs by ID (to satisfy `process_job`'s requirement for `job_id`).
        *   Actually, `process_job` should probably remove/mark the job? The prompt doesn't specify if it removes it, but usually, processing a job completes it. I'll keep it simple: find the job, run via policy, return result.

    *   *Correction on Priority*: "Highest-priority job's (id, data)". Usually, higher numbers = higher priority, but `heapq` is min-heap. I'll store priority as negative numbers.

    *   *Refining `RetryPolicy`*:
        ```python
        def run(self, fn, data):
            attempts = 0
            while attemps < self.max_attempts:
                try:
                    fn(data)
                    return True, attemps + 1
                except Exception:
                    attemps += 1
            return False, attemps
        ```

    *   *Refining `JobQueue`*:
        *   `self._queue`: list of `(priority, entry_count, job_object)`
        *   `self._jobs`: dict of `{job_id: job_object}`
        *   `self._counter`: integer.

    Wait, the prompt says: `process_job(self, job_id: str, processor: Callable) -> bool`.
    If I use a heap, finding a specific ID is $O(N)$. If I use a dict, it's $O(1)$.
    The prompt doesn't specify the performance requirement, but $O(N)$ is fine for small queues.

    *Refining `Job`*:
    ```python
    @dataclass
    class Job:
        id: str
        data: dict
        priority: int = 0
        retries: int = 0
    ```

    *Refining `RetryPolicy`*:
    ```python
    class RetryPolicy:
        def __init__(self, max_attempts: int = 4):
            self.max_attempts = max_attempts
        def run(self, fn: Callable, data: dict) -> tuple[bool, int]:
            attempts = 0
            while attempts < self.max_attempts:
                try:
                    fn(data)
                    return True, attemps + 1
                except Exception:
                    attempts += 1
            return False, attemps
    ```

    *Refining `JobQueue`*:
    ```python
    import heapq
    from dataclasses import dataclass
    from typing import Callable

    class JobQueue:
        def __init__(self):
            self._queue = []
            self._jobs = {}
            self._counter = 0
            self._retry_policy = RetryPolicy()

        def add_job(self, job_id: str, data: dict, priority: int = 0):
            job = Job(id=job_id, data=data, priority=priority)
            self._jobs[job_id] = job
            # Use -priority for max-heap behavior, counter for FIFO
            heapq.heappush(self._queue, (-priority, self._counter, job))
            self._counter += 1

        def process_job(self, job_id: str, processor: Callable) -> bool:
            job = self._jobs.get(job_id)
            if not job:
                return False
            success, _ = self._retry_policy.run(processor, job.data)
            return success

        def get_next_job(self) -> tuple[str, dict] | None:
            if not self._queue:
                return None
            _, _, job = heapq.heappop(self._queue)
            # Note: Removing from dict isn't explicitly asked for, 
            # but usually happens after pop. I'll stick to instructions.
            return (job.id, job.data)
    ```

    Wait, `process_job` takes `job_id`. Does it remove it? The prompt doesn't say. If I `heappop` in `get_next_job`, it is removed from the queue. If I call `process_job` with an ID that was already popped, it will still be in `self._jobs` unless I remove it.
    Actually, `get_next_job` returns the highest priority.
    The most logical flow is:
    1. `add_job`
    2. `get_next_job` -> returns highest priority job.
    3. `process_job` -> uses `RetryPolicy`.

    If `get_next_job` removes it from the queue, then `process_job` should probably look at the current state.
    If `process_job` is called directly, it doesn't remove it from the heap? That seems wrong for a queue.

    Let's re-read: `process_job` must USE `RetryPolicy`.
    It takes `job_id`.
    If I just search the `_jobs` dictionary, I don't need to worry about the heap.
    However, if `get_next_job` pops it, `process_job` might still find it in the dict.
    Let's ensure `get_next_job` removes it from the dict too.

    *Self-Correction*: The prompt says `get_next_job` returns the highest-priority job's (id, data). It doesn't explicitly say "remove", but "get next" usually implies removal in queue patterns. I will remove it from the internal storage to be safe and clean.

    *Wait*, `RetryPolicy.run` signature: `run(self, fn: Callable, data) -> tuple[bool, int]`.
    The prompt says `data`, not `data: dict`. I'll match the prompt's signature exactly.

    *One more detail*: `RetryPolicy.run` retries on "any exception".

    *Refining `JobQueue` storage*:
    I need a way to handle `process_job` if it's called on an ID that's still in the queue but not "next".
    If I use a dict to store everything, `process_job` is $O(1)$.
    If I use a heap for `get_next_job`, it's $O(\log N)$.
    If `get_next_job` is called, I pop from heap and delete from dict.

    ```python
    import heapq
    from dataclasses import dataclass
    from typing import Callable

    @dataclass
    class Job:
        id: str
        data: dict
        priority: int = 0
        retries: int = 0

    class RetryPolicy:
        def __init__(self, max_attempts: int = 4):
            self.max_attempts = max_attempts

        def run(self, fn: Callable, data) -> tuple[bool, int]:
            attempts = 0
            while attempts < self.max_attempts:
                try:
                    fn(data)
                    return True, attemps + 1
                except Exception:
                    attempts += 1
            return False, attemps

    class JobQueue:
        def __init__(self):
            self._queue = [] # List for heap
            self._jobs = {}  # Dict for ID lookup
            self._counter = 0
            self._retry_policy = RetryPolicy()

        def add_job(self, job_id: str, data: dict, priority: int = 0) -> None:
            job = Job(id=job_id, data=data, priority=priority)
            self._jobs[job_id] = job
            heapq.heappush(self._queue, (-priority, self._counter, job))
            self._counter += 1

        def process_job(self, job_id: str, processor: Callable) -> bool:
            job = self._jobs.get(job_id)
            if not job:
                return False
            success, _ = self._retry_policy.run(processor, job.data)
            return success

        def get_next_job(self) -> tuple[str, dict] | None:
            if not self._queue:
                return None
            _, _, job = heapq.heappop(self._queue)
            del self._jobs[job.id]
            return (job.id, job.data)
    ```

    Checking constraints:
    1. `Job` dataclass with fields? Yes.
    2. `RetryPolicy` init, run (no sleep), returns tuple? Yes.
    3. `JobQueue` add_job, process_job (uses RetryPolicy), get_next_job (highest priority, FIFO)? Yes.
    4. Standard library only? Yes.
    5. Python code only? Yes.

    One tiny detail: `RetryPolicy.run` signature is `run(self, fn: Callable, data)`.
    The `data` parameter doesn't have a type hint in the prompt's text for the signature but `data: dict` is mentioned in the class description. I'll omit the hint on `data` in the method signature to match the prompt exactly.

    Actually, looking at the prompt:
    `run(self, fn: Callable, data) -> tuple[bool, int]`
    I will follow that exactly.

    Wait, `process_job` usage: `must USE RetryPolicy (compose, don't reimplement retry logic)`.
    My `process_job` calls `self._retry_policy.run(...)`. This is composition.

    Wait, the `RetryPolicy.run` prompt says `attempts_made`.
    In my logic:
    Attempt 1: `fn(data)` succeeds -> `attempts = 0`, `return True, 1`.
    Attempt 1: `fn(data)` fails -> `attempts = 1`.
    Attempt 2: `fn(data)` succeeds -> `return True, 2`.
    Attempt 2: `fn(data)` fails -> `attempts = 2`.
    ...
    Attempt 4: `fn(data)` fails -> `return False, 4`.
    This correctly tracks `attempts_made`.

    Wait, FIFO order.
    `heapq` is a min-heap.
    To get highest priority first: Use `-priority`.
    To get FIFO within same priority: Use an incrementing counter.
    Example:
    Job A: Priority 10, Counter 0 -> `(-10, 0)`
    Job B: Priority 10, Counter 1 -> `(-10, 1)`
    `heapq` will return `(-10, 0)` first. Correct.

    Final check on `get_next_job` return type: `tuple[str, dict] | None`.
    Correct.
</reasoning_content>

<content>
```python
from dataclasses import dataclass
from typing import Callable
import heapq

@dataclass
class Job:
    id: str
    data: dict
    priority: int = 0
    retries: int = 0

class RetryPolicy:
    def __init__(self, max_attempts: int = 4):
        self.max_attempts = max_attempts

    def run(self, fn: Callable, data) -> tuple[bool, int]:
        attempts_made = 0
        while attempts_made < self.max_attempts:
            try:
                fn(data)
                return True, attempts_made + 1
            except Exception:
                attempts_made += 1
        return False, attempts_made

class JobQueue:
    def __init__(self):
        self._queue = []
        self._jobs = {}
        self._counter = 0
        self._retry_policy = RetryPolicy()

    def add_job(self, job_id: str, data: dict, priority: int = 0) -> None:
        job = Job(id=job_id, data=data, priority=priority)
        self._jobs[job_id] = job
        # Use negative priority for max-heap behavior and counter for FIFO
        heapq.heappush(self._queue, (-priority, self._counter, job))
        self._counter += 1

    def process_job(self, job_id: str, processor: Callable) -> bool:
        job = self._jobs.get(job_id)
        if not job:
            return False
        
        success, _ = self._retry_policy.run(processor, job.data)
        return success

    def get_next_job(self) -> tuple[str, dict] | None:
        if not self._queue:
            return None
        
        _, _, job = heapq.heappop(self._queue)
        if job.id in self._jobs:
            del self._jobs[job.id]
            
        return (job.id, job.data)
```
</content>