<reasoning_content>
*   Goal: Refactor a job queue into three specific composable classes: `Job`, `RetryPolicy`, and `JobQueue`.
    *   Language: Python.
    *   File name: `solution.py`.
    *   Constraints:
        *   Use standard library only.
        *   `Job`: Dataclass with `id`, `data`, `priority`, `retries`.
        *   `RetryPolicy`: Encapsulate retry-with-backoff. `run` method takes `fn` and `data`, retries on exception. No actual `sleep`. Return `(success, attempts_made)`.
        *   `JobQueue`: Orchestrate jobs using `RetryPolicy`. Methods: `add_job`, `process_job` (must use `RetryPolicy`), `get_next_job` (highest priority, FIFO).
    *   Output: Only the Python code.

    *   `Job` class:
        ```python
        from dataclasses import dataclass
        from typing import Callable, Dict, Tuple, Optional

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

    *   `RetryPolicy` class:
        ```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_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
        ```

    *   `JobQueue` class:
        *   Need a way to store jobs such that we can get the highest priority (highest number usually implies highest priority in these tasks, but sometimes it's lowest. Let's assume highest number = highest priority unless specified otherwise. "Highest-priority" usually maps to max value of the priority field).
        *   FIFO order within same priority.
        *   A `heapq` would be useful for priority, but `heapq` is a min-heap. To make it a max-heap, we can negate the priority.
        *   However, we also need to handle the FIFO part. `heapq` doesn't naturally handle FIFO for equal keys unless we include a counter.
        *   Alternative: A list sorted by priority (descending) and arrival time.
        *   Actually, `heapq` with `(-priority, sequence_number)` works perfectly.

        Wait, the prompt says: "highest-priority job's (id, data); FIFO order within the same priority".

        Let's look at `JobQueue` methods:
        - `add_job`: stores the job.
        - `process_job`: executes using `RetryPolicy`.
        - `get_next_job`: retrieves.

        Wait, if `process_job` takes a `job_id`, how does it know which job it is?
        The `JobQueue` needs to store the actual `Job` objects.

        Implementation details:
        - `self.jobs`: A dictionary mapping `id` to `Job` objects? Or a heap?
        - `get_next_job`: Needs to return the highest priority.
        - `process_job`: Needs to find the job by id.

        Let's refine `JobQueue`:
        ```python
        import heapq

        class JobQueue:
            def __init__(self):
                self.jobs = {} # id -> Job
                self.heap = [] # List of (-priority, sequence_number, id)
                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.heap, (-priority, self.counter, job_id))
                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.heap:
                    return None
                neg_priority, _, job_id = heapq.heappop(self.heap)
                job = self.jobs.get(job_id)
                if job:
                    return job.id, job.data
                return None
        ```

        Wait, `process_job` doesn't remove the job from the queue? The instructions don't specify removal on processing, but `get_next_job` pops from the heap.
        Actually, `get_next_job` usually implies taking it out of the queue.
        The instructions say: `process_job` must use `RetryPolicy`.

        Let's double check `RetryPolicy.run`.
        "calls `fn(data)`, retries on any exception until success or `max_attempts` reached, returns `(success, attempts_made)`."

        One minor detail: Does `RetryPolicy` need to be instantiated inside `JobQueue` or passed in?
        The prompt says: "`JobQueue` — orchestrates Jobs using RetryPolicy". Usually, this means an instance of `RetryPolicy` is used internally.

        Let's review types and signatures.
        `Job` fields: `id`, `data`, `priority`, `retries`.
        `RetryPolicy`: `__init__(self, max_attempts: int = 4)`, `run(self, fn: Callable, data) -> tuple[bool, int]`.
        `JobQueue`: `add_job`, `process_job`, `get_next_job`.

        One catch: `get_next_job` returns `tuple[str, dict] | None`.
        If `get_next_job` pops, the `job_id` is gone from the heap.

        Let's check the FIFO requirement.
        `heapq` with a counter ensures that for same `-priority`, the smaller `counter` (earlier job) comes first.

        Code structure:
        ```python
        from dataclasses import dataclass
        from typing import Callable, Dict, Tuple, Optional
        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: Dict) -> 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.jobs = {}
                self.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.heappush(self.heap, (-priority, self.counter, job_id))
                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.heap:
                    return None
                _, _, job_id = heapq.heappop(self.heap)
                job = self.jobs.get(job_id)
                if job:
                    return job.id, job.data
                return None
        ```

    *   `Job` dataclass: Correct.
    *   `RetryPolicy`: `run` returns `(success, attempts_made)`. Correct.
    *   `JobQueue`:
        *   `add_job`: Correct.
        *   `process_job`: Uses `RetryPolicy`. Correct.
        *   `get_next_job`: Highest priority (max value), FIFO for same priority. Correct (using `-priority` in min-heap).
    *   Standard library only: Yes.
    *   No explanations: Yes.

    Wait, should `process_job` remove the job from `self.jobs`? The prompt doesn't say, but usually, processing completes a job. However, `get_next_job` already pops it from the priority structure. If `process_job` is called with an arbitrary `job_id`, removing it from the dictionary might be good practice but not explicitly requested. I'll stick to the minimum required logic.

    Wait, `tuple[bool, int]` etc. are Python 3.9+ syntax for types. I'll use `Tuple` from `typing` just to be safe with older 3.x versions if not specified, but the prompt uses `tuple[...]` so I will use the built-in `tuple` and `dict` if allowed, or `typing` if needed. The prompt uses `tuple[bool, int]` so I will stick to that style (Python 3.9+).

    Wait, `RetryPolicy` `run` signature: `fn: Callable, data)`.
    The prompt has `run(self, fn: Callable, data)`. It missed the type for `data`. I'll assume `dict` based on `Job`.

    One detail: "highest-priority job's (id, data)".
    If multiple jobs have the same priority, FIFO.
    My heap approach: `(-priority, self.counter, job_id)`.
    `heapq` sorts by first element, then second.
    `self.counter` increments with every `add_job`.
    So for the same priority (same `-priority`), the smaller `counter` (earlier job) comes first. This is correct.

    Wait, one tiny detail:
    `retries: int = 0` is in `Job`. Does `RetryPolicy` need to update this?
    "retries on any exception until success or max_attempts reached".
    The `Job` class has a `retries` field, but `RetryPolicy` doesn't interact with the `Job` object, it only takes `data` and `fn`.
    Actually, the prompt says `RetryPolicy.run` takes `fn` and `data`. It does NOT take the `Job` object. So `Job.retries` might not be used by `RetryPolicy`. I will keep the dataclass as defined.

    Check `RetryPolicy.run` return value: `(success, attempts_made)`.
    If it succeeds on the first try, `attempts_made` is 1.
    If it fails 4 times, `attempts_made` is 4.

    Double check `JobQueue.process_job`:
    "must USE RetryPolicy (compose, don't reimplement retry logic)".
    My implementation:
    ```python
    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
    ```
    This uses the `RetryPolicy` instance.

    Is `data` a `dict` or `Any`? Prompt says `data: dict`.
    Is `Callable` from `typing`? I'll import it.

    One more look at `get_next_job`:
    Returns `tuple[str, dict] | None`.
    If the queue is empty, return `None`.
    My code handles this.

    Final check on `RetryPolicy.run`:
    "retries on any exception until success or `max_attempts` reached".
    If `max_attempts` is 4:
    Try 1: Fail
    Try 2: Fail
    Try 3: Fail
    Try 4: Fail
    Return `(False, 4)`.
    My code:
    `while attempts_made < self.max_attempts:`
    `attempts_made` starts at 0.
    Loop 1: `attempts_made` becomes 1.
    Loop 2: `attempts_made` becomes 2.
    Loop 3: `attempts_made` becomes 3.
    Loop 4: `attempts_made` becomes 4.
    Loop 5: `4 < 4` is false. Loop ends.
    Returns `(False, 4)`. Correct.
</reasoning_content>

<content>
```python
from dataclasses import dataclass
from typing import Callable, Dict, Tuple, Optional
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: dict) -> 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.jobs: Dict[str, Job] = {}
        self.heap: list[tuple[int, int, str]] = []
        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
        # Negate priority for max-heap behavior, use counter for FIFO
        heapq.heappush(self.heap, (-priority, self.counter, job_id))
        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.heap:
            return None
        
        _, _, job_id = heapq.heappop(self.heap)
        job = self.jobs.get(job_id)
        if job:
            return job.id, job.data
        return None
```
</content>