<reasoning_content>
*   Goal: Refactor a job queue into three composable classes: `Job`, `RetryPolicy`, and `JobQueue`.
    *   Language: Python (standard library only).
    *   Class 1: `Job` (dataclass).
        *   Fields: `id: str`, `data: dict`, `priority: int = 0`, `retries: int = 0`.
    *   Class 2: `RetryPolicy`.
        *   `__init__(max_attempts: int = 4)`.
        *   `run(fn: Callable, data) -> tuple[bool, int]`.
        *   Behavior: Call `fn(data)`, retry on exception up to `max_attempts`. No sleeping.
    *   Class 3: `JobQueue`.
        *   `add_job(job_id, data, priority)`.
        *   `process_job(job_id, processor)`: MUST use `RetryPolicy`.
        *   `get_next_job()`: Highest priority first, then FIFO.

    *   `Job`:
        ```python
        from dataclasses import dataclass

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

    *   `RetryPolicy`:
        ```python
        from typing import Callable

        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:
                    attempts += 1
                    try:
                        fn(data)
                        return True, attempts
                    except Exception:
                        continue
                return False, attempts
        ```

    *   `JobQueue`:
        *   Need to store jobs. To handle priority + FIFO, a heap is good, but Python's `heapq` is a min-heap.
        *   Priority: Highest priority first. In standard priority queues, higher numbers usually mean higher priority, but `heapq` pops the smallest. So, I should store `-priority` to make it a max-heap for priority.
        *   FIFO: Use a counter to ensure stable sorting within the same priority.
        *   `add_job`: Create a `Job` object and push to a priority queue.
        *   `get_next_job`: Pop from the priority queue.
        *   `process_job`: Find the job, use `RetryPolicy.run`, and update job state (retries).

    *   *Refining `JobQueue` storage*:
        To support `process_job(job_id, ...)` efficiently, I might need a way to find a job by ID. However, the prompt says `get_next_job` returns the highest priority. `process_job` takes a `job_id`. This implies I need a mapping of `id -> Job`.
        But if I use a heap for `get_next_job`, I also need to handle the "ID-based" lookup in `process_job`.
        Wait, if `process_job` takes a `job_id`, does it mean the job is already out of the queue? Or is it in the queue?
        Usually, in these designs:
        1. `add_job` adds to the queue.
        2. `get_next_job` retrieves it.
        3. `process_job` executes it.
        If `process_job` receives a `job_id`, I need to keep track of all jobs.

        Let's look at `get_next_job` requirements: "return the highest-priority job's (id, data)".
        Let's look at `process_job` requirements: "must USE RetryPolicy".

        Standard pattern:
        `jobs_map`: `Dict[str, Job]`
        `priority_queue`: `List[tuple[int, int, str]]` (negative_priority, arrival_order, job_id)

        Wait, if `process_job` is called with a `job_id`, it implies the caller knows which job to process.

        Implementation details for `JobQueue`:
        - `self.jobs`: `dict[str, Job]`
        - `self.queue`: `heapq` containing `(-priority, counter, job_id)`
        - `self.counter`: `int` (for FIFO)
        - `self.retry_policy`: `RetryPolicy()`

        `add_job(id, data, priority)`:
        - `job = Job(id, data, priority)`
        - `self.jobs[id] = job`
        - `heapq.heappush(self.queue, (-priority, self.counter, id))`
        - `self.counter += 1`

        `get_next_job()`:
        - If queue empty, return `None`.
        - `priority_val, order, job_id = heapq.heappop(self.queue)`
        - `job = self.jobs[job_id]`
        - return `(job.id, job.data)`? No, the prompt says "return the highest-priority job's (id, data)". It doesn't say to remove it, but `get_next_job` usually implies removal or marking. If I remove it, `process_job` might be called on an ID not in the queue?
        Wait, `process_job` takes `job_id`. This implies the job is managed by the queue.

        Let's look at the logic flow:
        1. `add_job` -> adds to structure.
        2. `get_next_job` -> pops highest priority job from structure.
        3. `process_job` -> takes a `job_id`, applies `RetryPolicy`.

        If `get_next_job` pops the job, it's no longer in the priority queue. So where is it for `process_job`?
        Perhaps `JobQueue` should maintain a collection of "pending/in-progress" jobs.
        Or, perhaps `get_next_job` doesn't remove it? No, `get_next_job` usually removes.
        Actually, looking at the requirements:
        - `process_job(self, job_id: str, processor: Callable) -> bool`
        - `get_next_job(self) -> tuple[str, dict] | None`

        If `get_next_job` removes it, `process_job` needs to find it. So `self.jobs` should keep all jobs until they are fully processed/failed? Or `process_job` is called on the job that was just "gotten"?
        If I `get_next_job`, I get the ID and Data. Then I call `process_job(job_id, processor)`.

        Let's refine:
        `add_job`: Store in `self.jobs[id] = Job(...)` and push to `self.pq`.
        `get_next_job`: Pop from `self.pq`, get `job = self.jobs[id]`, return `(job.id, job.data)`.
        `process_job`:
        - `job = self.jobs[job_id]`
        - `success, attempts = self.retry_policy.run(processor, job.data)`
        - `job.retries = attempts` (or update based on logic)
        - return `success`

        Wait, if `get_next_job` pops it, and `process_job` is called, how does the `JobQueue` know if it should be re-added to the queue if it fails?
        The prompt doesn't specify re-queueing logic, just that `process_job` uses `RetryPolicy`.
        Also, the prompt says `process_job` must use `RetryPolicy`.

        Let's double check the `Job` dataclass: `retries: int = 0`.
        The `RetryPolicy.run` returns `attempts_made`.
        It's reasonable to update `job.retries` in `process_job`.

        One detail: `get_next_job` - "return the highest-priority job's (id, data)".
        If I use `heapq`, I'll use a counter to ensure FIFO for the same priority.

        `priority_queue` stores: `(-priority, entry_count, job_id)`

    *   `Job`: Dataclass, check.
    *   `RetryPolicy`: `run(fn, data)` returns `(success, attempts)`, no sleep, check.
    *   `JobQueue`:
        *   `add_job`: check.
        *   `process_job`: uses `RetryPolicy`, check.
        *   `get_next_job`: highest priority, FIFO, check.
        *   Standard library only: `dataclasses`, `heapq`, `typing`, `collections`.

    If `get_next_job` pops the job, and `process_job` is called later, `process_job` needs to access the `Job` object.
    I will keep a dictionary `self.all_jobs` to store `Job` objects by ID.

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

    @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: Any) -> tuple[bool, int]:
            attempts = 0
            while attempts < self.max_attempts:
                attempts += 1
                try:
                    fn(data)
                    return True, attempts
                except Exception:
                    continue
            return False, attempts

    class JobQueue:
        def __init__(self):
            self.jobs: dict[str, Job] = {}
            self.queue = []  # heap
            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 is a min-heap. To get max priority, use -priority.
            # To get FIFO, use a monotonic counter.
            heapq.heappush(self.queue, (-priority, self._counter, job_id))
            self._counter += 1

        def get_next_job(self) -> tuple[str, dict] | None:
            if not self.queue:
                return None
            _, _, job_id = heapq.heappop(self.queue)
            job = self.jobs[job_id]
            return (job.id, job.data)

        def process_job(self, job_id: str, processor: Callable) -> bool:
            if job_id not in self.jobs:
                return False
            job = self.jobs[job_id]
            success, attempts = self.retry_policy.run(processor, job.data)
            job.retries = attempts # The prompt implies retries tracks attempts
            return success
    ```

    Wait, `job.retries` usually means "number of times it was retried" (attempts - 1) or "total attempts". The dataclass says `retries: int = 0`. If I run it once and it succeeds, is `retries` 1 or 0? Usually, if it succeeds on the first try, retries = 0.
    However, the prompt says `RetryPolicy.run` returns `(success, attempts_made)`.
    And `Job` has `retries`. I will set `job.retries = attempts` to be safe, or `attempts - 1`?
    Actually, the prompt doesn't explicitly command how to update `retries` in `Job`, just that it's a field.
    Let's re-read: "`process_job` ... must USE `RetryPolicy`".
    If `process_job` returns `bool` (success/failure), I'll just ensure I use the policy.

    Wait, there's a potential bug in `get_next_job`. If I pop the job from the heap, but `process_job` is called, the job is still in `self.jobs`. This works.
    But what if `add_job` is called with the same `job_id`? The prompt doesn't specify. I'll assume `job_id` is unique.

    Final check on `get_next_job`: "return the highest-priority job's (id, data)".
    If I pop it, I am removing it from the "next to be processed" list. This is standard.

    One edge case: `process_job` returns `bool`.
    If `attempts_made` is 1, it succeeded immediately. `success` is True.

    One small detail: `Callable` and `Any` need to be imported from `typing`. `dataclass` from `dataclasses`. `heapq`.

    Wait, the `process_job` method: `process_job(self, job_id: str, processor: Callable) -> bool`.
    If `job_id` is not in `self.jobs`, should I return `False`? Yes.

    The prompt says: "`RetryPolicy.run(self, fn: Callable, data) -> tuple[bool, int]` ... returns `(success, attempts_made)`."
    And `Job` has `retries: int = 0`.
    In `process_job`, I'll update `job.retries` using the `attempts_made`.

    ```python
    from dataclasses import dataclass
    from typing import Callable, Any
    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: Any) -> tuple[bool, int]:
            attempts = 0
            while attempts < self.max_attempts:
                attempts += 1
                try:
                    fn(data)
                    return True, attempts
                except Exception:
                    continue
            return False, attempts

    class JobQueue:
        def __init__(self):
            self._jobs: dict[str, Job] = {}
            self._queue: list[tuple[int, int, str]] = []
            self._counter: int = 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_id))
            self._counter += 1

        def process_job(self, job_id: str, processor: Callable) -> bool:
            if job_id not in self._jobs:
                return False
            job = self._jobs[job_id]
            success, attempts = self._retry_policy.run(processor, job.data)
            job.retries = attempts
            return success

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

    Wait, one thing. If I pop the job in `get_next_job`, and `process_job` is called, but the job failed all retries, does it stay in `self._jobs`? Yes. Does it stay in the queue? No, because `get_next_job` popped it. So it won't be picked up again unless `add_job` is called for the same ID. This seems correct for a queue processing flow.

    `get_next_job` returns `tuple[str, dict] | None`.
    The return type hint `tuple[str, dict] | None` requires Python 3.10+. Since the prompt uses this syntax in the requirement, it's safe to assume 3.10+.

    Refining `RetryPolicy.run` for correctness:
    If `max_attempts` is 4:
    - `attempts = 0`
    - Loop 1: `attempts = 1`, try `fn`. If fail, continue.
    - Loop 2: `attempts = 2`, try `fn`. If fail, continue.
    - Loop 3: `attempts = 3`, try `fn`. If fail, continue.
    - Loop 4: `attempts = 4`, try `fn`. If fail, loop terminates.
    - Return `False, 4`.
    This matches the requirement.

    Final check on `JobQueue.get_next_job`:
    "return the highest-priority job's (id, data)".
    My code: `return (job.id, job.data)`. Correct.

    One detail: `Job` fields.
    `id: str`, `data: dict`, `priority: int = 0`, `retries: int = 0`.
    I'll make sure the order matches the prompt.
</reasoning_content>

<content>
```python
from dataclasses import dataclass
from typing import Callable, Any
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: Any) -> tuple[bool, int]:
        attempts = 0
        while attempts < self.max_attempts:
            attempts += 1
            try:
                fn(data)
                return True, attempts
            except Exception:
                continue
        return False, attempts


class JobQueue:
    def __init__(self):
        self._jobs: dict[str, Job] = {}
        self._priority_queue: list[tuple[int, int, str]] = []
        self._counter: int = 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 -priority for max-heap behavior with heapq (min-heap)
        # Use self._counter to ensure FIFO order for identical priorities
        heapq.heappush(self._priority_queue, (-priority, self._counter, job_id))
        self._counter += 1

    def process_job(self, job_id: str, processor: Callable) -> bool:
        if job_id not in self._jobs:
            return False
        
        job = self._jobs[job_id]
        success, attempts_made = self._retry_policy.run(processor, job.data)
        job.retries = attempts_made
        return success

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

```
</content>