<reasoning_content>
*   Refactor a job queue into three composable classes.
    *   File: `solution.py`.
    *   Classes to define:
        1.  `Job`: Dataclass (`id`, `data`, `priority`, `retries`).
        2.  `RetryPolicy`: Logic for retrying a function with max attempts (no actual sleep).
        3.  `JobQueue`: Management of jobs, priority logic, and processing logic using `RetryPolicy`.
    *   Constraints:
        *   Use `@dataclass`.
        *   `RetryPolicy.run` returns `(success, attempts_made)`.
        *   `JobQueue.get_next_job` returns highest priority (FIFO within same priority).
        *   No external packages (standard library only).
        *   Output: Python code only, no explanations.

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

    *   *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, attempts + 1
                    except Exception:
                        attempts += 1
                return False, attempts
        ```

    *   *JobQueue*:
        *   Need to store jobs in a way that supports priority and FIFO.
        *   Actually, `heapq` is standard for priority, but it doesn't handle FIFO naturally unless we include a counter. However, since I need to return `(id, data)`, I should probably store them in a list and sort it or use a priority queue.
        *   Requirement: "return the highest-priority job's (id, data); FIFO order within the same priority."
        *   Storage: A list of `Job` objects.
        *   `add_job`: create `Job` object, append to list.
        *   `get_next_job`: find highest priority. If ties, oldest (first in list).
        *   `process_job`: find the job, use `RetryPolicy`.

    *   *Refining `JobQueue` implementation*:
        To handle "highest priority" and "FIFO", a simple approach is to keep a list. When `get_next_job` is called, sort the list by `priority` descending and then by insertion order.
        Actually, even simpler: find the max priority, then find the first job with that priority.

    *   *Refining `RetryPolicy` implementation*:
        The prompt says: `run(self, fn: Callable, data) -> tuple[bool, int]`.
        Wait, the signature says `data` without a type hint in the prompt text, but usually, it's `dict`. I'll stick to the prompt's types.

    *   *Refining `JobQueue` - `process_job`*:
        It says `process_job(self, job_id: str, Processor: Callable) -> bool`.
        It must use `RetryPolicy`.

    *   *Imports*: `dataclass`, `Callable`, `List`, `Tuple`, `Optional`, `heapq`? Or just list manipulation?
        If I use a list and sort, I don't need `heapq`.
        Wait, "highest-priority". Usually higher number = higher priority? Or lower?
        Usually, in priority queues, lower number is higher priority, but "highest priority" usually implies the maximum value. I will assume higher integer = higher priority.

    *   Let's look at `get_next_job` again: `return the highest-priority job's (id, data); FIFO order within the same priority.`
        Since I need to remove it (implied by "get next"), I should probably pop it or delete it.

    *   *Wait*, if I use a list:
        `add_job`: append.
        `get_next_job`: Find max priority. Find first occurrence. Remove it. Return values.

    *   *Wait*, the `Job` class has a `retries` field. Does `RetryPolicy` increment it? The prompt says `RetryPolicy` takes `fn` and `data`. It doesn't take the `Job` object. This implies `JobQueue` manages the `Job` object but passes the data to the policy.

    ```python
    from dataclasses import dataclass
    from typing import Callable, Tuple, Optional, Dict, List

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

    class JobQueue:
        def __init__(self):
            self.jobs: List[Job] = []
            self.retry_policy = RetryPolicy()

        def add_job(self, job_id: str, data: dict, priority: int = 0) -> None:
            new_job = Job(id=job_id, data=data, priority=priority)
            self.jobs.append(new_job)

        def get_next_job(self) -> Optional[Tuple[str, dict]] :
            if not self.jobs:
                return None
            
            # Find the max priority
            max_prio = max(j.priority for j in self.jobs)
            # Find first job with that priority (FIFO)
            for i, job in enumerate(self.jobs):
                if job.priority == max_prio:
                    self.jobs.pop(i)
                    return job.id, job.data
            return None

        def process_job(self, job_id: str, Processor: Callable[[dict]]) -> bool:
            # Find the job object by ID
            job_idx = -1
            for i, job in enumerate(self.jobs):
                if job.id == job_id:
                    job_idx = i
                    break
            
            if job_idx == -1:
                return False
            
            job = self.jobs[job_idx]
            success, attempts = self.retry_policy.run(Processor, job.data)
            
            # Note: Prompt doesn't explicitly say to remove job on success/fail,
            # but usually process_job implies removal. 
            # However, the instructions don't specify removal here.
            # If I don't remove it, get_next_job might return it again.
            # Usually, processing consumes the job.
            # Let's check if the prompt implies consumption.
            # "orchestrates Jobs... process_job ... must USE RetryPolicy".
            # I'll remove it on successful completion or max retries.
            # Actually, if I just follow the prompt literally:
            # "process_job ... returns bool".
            # If I don't remove it, the job stays in the queue.
            # If I do remove it, I should probably remove it regardless of success.
            
            # Looking at typical patterns, process_job usually removes the job.
            # But to be safe and follow "orchestrates", let's assume 
            # process_job is an action on an existing job.
            
            # I will remove it after processing to ensure it's not processed twice.
            self.jobs.pop(job_idx)
            return success
    ```

    *   *Self-Correction on `JobQueue.get_next_job`*:
        The instruction says `get_next_job` returns the highest-priority job.
        If `process_job` is also called by ID, then `get_next_job` is likely for a consumer.
        If `process_job` is called by ID, it implies the caller knows the ID.
        I'll stick to the most literal interpretation.

    *   *One more detail*: `RetryPolicy.run` signature.
        Prompt: `run(self, fn: Callable, data) -> tuple[bool, int]`
         I will use `dict` for `data` to match `Job.data`.

    *   *Refining `process_job`*:
        If I remove the job in `get_next_job`, then `process_job` (which takes a `job_id`) won't find it if it was already "gotten".
        Usually, systems have two ways:
        1. `get_next_job` pops it, then you process it.
        2. `process_job` finds it, processes it, and removes it.
        
        The prompt has both.
        If `get_next_job` pops it, then `process_job` (searching by id) would fail.
        Therefore, `get_next_job` should probably *not* pop it? Or `process_job` shouldn't pop it?
        Actually, `get_next_job` "returns the highest-priority job's (id, data)". It doesn't say "removes".
        However, if it doesn't remove it, and `get_next_job` is called again, it returns the same one. This is common for "peek" but the name "get" usually implies removal.
        
        Let's look at `process_job(self, job_id: str, ...)`.
        If `process_job` is meant to be the primary way to execute, it must remove the job.
        If `get_next_job` is meant to be the way to fetch for a worker, it must remove the job.
        
        To satisfy both:
        If `get_next_job` removes it, then `process_job` cannot be called by ID because the ID is gone.
        If `get_next_job` does NOT remove it, then the queue stays full.
        
        Alternative: `get_next_job` returns the tuple, and the user is expected to call `process_job`. But `process_job` requires the `job_id`. This works perfectly if `get_next_job` does *not* remove the job.
        
        Wait, if `get_next_job` doesn't remove the job, and `process_job` doesn't remove the job, the queue never shrinks. That's bad.
        
        Let's look at the `Job` dataclass: `retries: int = 0`.
        This suggests that the `Job` object's `retries` count should be updated.
        The `RetryPolicy` takes `fn` and `data`. It doesn't take the `Job` object.
        This means `JobQueue` must capture the results and update the `Job` object?
        But `RetryPolicy.run` returns `(success, attempts_made)`.
        
        Let's re-read: "orchestrates Jobs using RetryPolicy".
        
        Actually, I'll make `get_next_job` remove the job. And `process_job` also remove the job. 
        Wait, if `get_next_job` removes it, `process_job` is useless.
        If `get_job_id` doesn't remove it, then `process_job` is the one that removes it.
        
        Let's try this:
        `get_next_job` -> returns (id, data) but does NOT remove. (Peek)
        `process_job` -> finds by id, uses policy, removes from list, returns bool.
        
        But "get" usually means "take".
        Let's look at the `priority` and `FIFO`.
        If I use a list and `get_next_job` pops the item, it's a standard queue.
        
        Revised Plan:
        `add_job`: append to `self.jobs`.
        `get_next_job`: find highest priority, pop it, return `(id, data)`.
        `process_job`: find by id, execute, pop it, return `bool`.
        
        Wait, if `get_next_job` pops it, `process_job` will fail to find it by ID.
        This implies the user is expected to call `get_next_job` to get the ID, then `process_job` with that ID.
        BUT if `get_next_job` pops it, `process_job` won't find it.
        
        Conclusion: `get_next_job` must NOT pop. `process_job` must pop.
        
        Wait, one more look at `Job`. It has `retries`.
        If `process_job` uses `RetryPolicy`, and `RetryPolicy` returns `attempts_made`, `JobQueue` should probably update `job.retries`.
        
        Let's write the code.

    *   *Implementation details*:
        - `RetryPolicy.run`: `fn(data)`.
        - `JobQueue.get_next_job`: Highest priority (max value), then FIFO (lowest index).
        - `JobQueue.process_job`: `RetryPolicy.run`.

    *   *Refining `get_next_job`*:
        To be FIFO, I need to ensure I pick the first one in the list that matches the max priority.

    *   *Wait*, the prompt says `RetryPolicy.run` returns `(success, attempts_made)`.
        It does NOT say to update `job.retries`. But the `Job` dataclass has a `retries` field. This strongly implies that `JobQueue` should update it.
        However, the instructions for `process_job` are: `process_job(...) -> bool`.
        If it just returns bool, it might not care about updating the object, but I'll update it just in case.

    *   *Final structure check*:
        `Job`: dataclass.
        `RetryPolicy`: `__init__`, `run`.
        `JobQueue`: `add_job`, `process_job`, `get_next_job`.

    *   *One small thing*: `Callable` requires `from typing import Callable`.
</reasoning_content>

<content>
```python
from dataclasses import dataclass
from typing import Callable, Tuple, Optional, Dict, List


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


class JobQueue:
    def __init__(self):
        self.jobs: List[Job] = []
        self.retry_policy = RetryPolicy()

    def add_job(self, job_id: str, data: dict, priority: int = 0) -> None:
        new_job = Job(id=job_id, data=data, priority=priority)
        self.jobs.append(new_job)

    def get_next_job(self) -> Optional[Tuple[str, dict]] :
        if not self.jobs:
            return None
        
        max_priority = max(job.priority for job in self.jobs)
        for i, job in enumerate(self.jobs):
            if job.priority == max_priority:
                return job.id, job.data
        return None

    def process_job(self, job_id: str, Processor: Callable[[dict]]) -> bool:
        job_index = -1
        for i, job in enumerate(self.jobs):
            if job.id == job_id:
                job_index = i
                break
        
        if job_index == -1:
            return False
        
        job = self.jobs[job_index]
        success, attempts = self.retry_policy.run(Processor, job.data)
        
        # Update retries and remove job from queue
        job.retries = attempts - 1 if not success else job.retries
        self.jobs.pop(job_index)
        
        return success
```
</content>