<reasoning_content>
*   Objective: Refactor a job queue into three specific classes: `Job`, `RetryPolicy`, and `JobQueue`.
    *   Environment: Python (standard library only).
    *   Output format: Only code, no explanation.

    *   **Class 1: `Job`**
        *   Type: `dataclass`.
        *   Fields: `id` (str), `data` (dict), `priority` (int, default 0), `retries` (int, default 0).
        *   Requirement: Use `@dataclass`.

    *   **Class 2: `RetryPolicy`**
        *   Method `__init__`: `max_attempts: int = 4`.
        *   Method `run`: `fn: Callable`, `data`.
        *   Logic: Call `fn(data)`, catch exceptions, retry until success or `max_attempts`.
        *   Return: `tuple[bool, int]` (success, attempts_made).
        *   Constraint: Do *not* sleep.

    *   **Class 3: `JobQueue`**
        *   Method `add_job`: `job_id`, `data`, `priority`.
        *   Method `process_job`: `job_id`, `Processor`. Must use `RetryPolicy`.
        *   Method `get_next_job`: Return `tuple[str, dict]` or `None`. Highest priority first, FIFO for same priority.
        *   Internal state: Needs to store jobs. A list or a heap could work. Since FIFO for same priority is required, a list sorted by priority (descending) and insertion order is standard.

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

        @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:*
        ```python
        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)
                # Sort by priority descending, maintain order for tie
                # Since we append, stable sort or just re-sorting works.
                # Actually, standard sort is stable.
                self.jobs.sort(key=lambda x: x.priority, reverse=True)

            def process_job(self, job_id: str, Processor: Callable) -> bool:
                # Find job
                job = next((j for j in self.jobs if j.id == job_id), None)
                if not job:
                    return False
                
                success, attempts = self.retry_policy.run(Processor, job.data)
                return success

            def get_next_job(self) -> tuple[str, dict] | None:
                if not self.jobs:
                    return None
                job = self.jobs[0]
                # If I remove it, I should pop. But the prompt doesn't 
                # explicitly say "remove", but usually "get_next" implies 
                # consumption in a queue context. However, it says "return". 
                # Looking at the signature: `get_next_job(self) -> tuple[str, dict] | None`. 
                # If I don't remove it, I'll keep returning the same job.
                # Usually, a queue pop. Let's check the phrasing. 
                # "return the highest-priority job's (id, data)".
                # I will pop it to behave like a queue.
                # Actually, the prompt doesn't say "pop", it says "return".
                # But it's a "queue". 
                # If I don't pop, it's more of a "peek".
                # Let's look at standard queue behavior. Usually items are consumed.
                # If I don't consume, the user can't "queue" things meaningfully.
                # I'll pop the first one.
                # Actually, let's stick to exactly what is requested.
                # "return the highest-priority job's (id, data)". 
                # I will pop it so it behaves like a Queue.
                # Wait, if I pop it, I might break `process_job` if the user 
                # calls `get_next_job` then `process_job`.
                # Let's look at the requested methods: 
                # add_job, process_job, get_next_job.
                # Usually, process_job takes a job_id. This implies the job 
                # stays in the queue until processed? No, usually it's removed.
                # If I don't remove it in `process_job`, and `get_next_job` 
                # returns the first one, `process_job` is the one doing the work.
                
                # Let's re-read: 
                # "return the highest-priority job's (id, data)". 
                # To ensure FIFO within same priority, sorting is necessary.
                # I'll pop it to make it a "Queue".
                job = self.jobs.pop(0)
                return (job.id, job.data)
        ```

    *   *Refining JobQueue sorting:*
        If I sort every time I add, it's $O(N \log N)$. If I only add, and `get_next_job` searches, it might be different.
        But `get_next_job` says "highest priority... FIFO order within same priority".
        To satisfy FIFO:
        When adding a job with priority 5, it goes after other jobs with priority 5.
        If I sort by priority descending, then it's just $O(N \log N)$ per add.

    *   *Refining `process_job`:*
        It takes `job_id`. It should find the job, use the policy, and then maybe remove it? 
        The prompt doesn't say to remove it on success/failure in `process_job`.
        However, `get_next_job` usually removes it.
        Wait, if `get_next_job` removes it, then `process_job` won't find it by ID.
        Therefore, `get_next_job` should probably NOT remove it, OR `process_job` should be the one to remove it.
        
        Let's look at the requirements again:
        1. `add_job(self, job_id: str, data: dict, priority: int = 0) -> None`
        2. `process_job(self, job_id: str, Processor: Callable) -> bool`
        3. `get_next_job(self) -> tuple[str, dict] | None`
        
        If `get_next_job` removes it, then `process_job` becomes useless because the ID is gone.
        If `get_next_job` doesn't remove it, it's a "peek".
        Given it's a "Queue", let's assume `get_next_job` is the consumer.
        If I am the consumer, I get the ID and data, then I call `process_job`? No, `process_job` is a method of the queue.
        
        Let's assume `get_next_job` returns the job but doesn't remove it, and `process_job` removes it upon completion? Or `process_job` is independent.
        
        Actually, the most standard way for these types of interview questions:
        `get_next_job` returns the next available item (pops it).
        `process_job` executes an action on a specific job.
        
        Wait, if `get_next_job` pops, then `process_job` needs the `Job` object or the ID.
        If `get_next_job` pops, the ID is gone from the queue. So `process_job(job_id)` would fail.
        
        Alternative interpretation:
        `get_next_job` returns the highest priority job's info.
        `process_job` finds the job by ID, runs it, and *removes* it.
        
        Let's look at the names again.
        `get_next_job` -> Return info.
        `process_job` -> Execute logic.
        
        I will assume `get_next_job` pops the job because it's a queue.
        If `get_next_job` pops, the user has the data. They don't need `process_job`.
        
        Let's try another interpretation:
        `process_job` finds the job, executes, and removes it.
        `get_next_job` just peeks.
        
        Let's look at the prompt's instructions for `process_job`: 
        "must USE RetryPolicy (compose, don't reimplement retry logic)".
        
        Let's look at `get_next_job` again:
        "return the highest-priority job's (id, data)".
        
        I will implement `get_next_job` to pop the item, and `process_job` to find the item by ID and execute it (maybe not pop it, or pop it if success).
        
        Wait, if `process_job` takes `job_id`, it implies the job is still in the queue.
        If `get_next_job` removes the job, `process_job` can't find it.
        
        Therefore:
        `get_next_job` = Peek.
        `process_job` = Find, execute, then potentially remove.
        
        Let's go with:
        `add_job`: adds to list.
        `get_next_job`: finds highest priority, returns tuple.
        `process_job`: finds by ID, runs via `RetryPolicy`, removes from list if successful (or always? Let's say always for simplicity or if successful).
        
        Actually, looking at common patterns: 
        `get_next_job` often implies popping.
        If it pops, `process_job` is redundant for the user unless they want to process a specific ID.
        
        I'll stick to the simplest interpretation:
        `add_job`: append and sort (stable sort to keep FIFO).
        `get_next_job`: return first element without removing.
        `process_job`: find by ID, run, and remove.
        
        Wait, if `process_job` removes it, what if it fails?
        
        Let's reconsider:
        `get_next_job`: pops the first item.
        `process_job`: takes a `job_id`, finds the job in the queue, runs it.
        
        If `get_next_job` pops, then the user shouldn't call `process_job` using the ID returned by `get_next_job`.
        
        Let's provide a clean implementation where:
        1. `add_job`: appends and maintains sorted order (descending priority).
        2. `get_next_job`: returns the first job's info (peek).
        3. `process_job`: finds job by ID, runs it using `RetryPolicy`, and removes it from the list.

    *   *Actually, let's refine `JobQueue` structure:*
        To maintain FIFO within same priority:
        Sort key: `(-job.priority, job.insertion_index)`.
        Or simply use a list and `sort(key=lambda x: x.priority, reverse=True)`. Python's `sort` is stable. If I append new jobs to the end of the list, and then sort by priority descending, the relative order of equal priorities is preserved.

    *   *Refining `RetryPolicy.run`:*
        `fn` is a `Callable`. `fn(data)` is called.
        Returns `(bool, int)`.

    *   *Implementation:*

```python
from dataclasses import dataclass
from typing import Callable, Dict, Tuple, Optional, 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, 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)
        # Python's sort is stable, so relative order for same priority is kept.
        self.jobs.sort(key=lambda x: x.priority, reverse=True)

    def process_job(self, job_id: str, Processor: Callable) -> bool:
        # Find the job index to allow removal
        index = -1
        for i, job in enumerate(self.jobs):
            if job.id == job_id:
                index = i
                break
        
        if index == -1:
            return False
        
        job = self.jobs[index]
        success, _ = self.retry_policy.run(Processor, job.data)
        
        if success:
            self.jobs.pop(index)
            
        return success

    def get_next_job(self) -> Tuple[str, dict] | None:
        if not self.jobs:
            return None
        # Peek at the highest priority job
        job = self.jobs[0]
        return (job.id, job.data)
```

    *Self-Correction during drafting:*
    Requirement: `get_next_job` -> "return the highest-priority job's (id, data)".
    If I don't remove it, it's a peek.
    If I do remove it, it's a pop.
    Usually, in a "Job Queue" context, "Get Next" implies "Give me the next one to do". 
    However, if I pop it, `process_job` becomes unreachable for that specific job unless the caller holds the job state.
    But `process_job` takes `job_id`. This strongly implies `process_job` is the primary way to complete a job.
    If `process_job` is the primary way to complete a job, `get_next_job` should probably be a peek.

    Wait, if `process_job` is used, why would someone call `get_next_job`?
    Maybe to see what's next?
    
    Let's look at the requirements again:
    `process_job` must USE `RetryPolicy`.
    
    I will implement `get_next_job` as a pop to satisfy the "Queue" behavior, but then `process_job` will fail. This is a contradiction in the API design.
    
    Let's re-examine the `process_job` signature: `process_job(self, job_id: str, Processor: Callable) -> bool`.
    This signature explicitly requires the `job_id`.
    If `get_next_job` removed the job from the queue, the `job_id` would no longer be in the queue.
    Therefore, `get_next_job` *must* be a peek (or `process_job` doesn't remove the job).
    
    Actually, I'll make `get_next_job` a peek.
    
    Let's check `JobQueue.add_job`: `priority` defaults to 0.
    `JobQueue.get_next_job`: `return the highest-priority job's (id, data)`.
    
    Wait, if I use a list and `sort`, the `process_job` logic needs to be careful about the index.
    
    One more thing: `RetryPolicy.run` return `(success, attempts_made)`.
    The prompt says "Do NOT actually sleep".

    Refined `process_job`:
    1. Find job by id.
    2. Call `retry_policy.run`.
    3. If success, remove from queue? (The prompt doesn't say, but it's cleaner).
    Actually, I won't remove it unless successful? Or just leave it?
    Most job queues remove on completion.
    
    Actually, I'll skip removal in `process_job` to be safe and just stick to the requirements. If it's not asked for, don't over-engineer.
    
    Wait, the requirement for `get_next_job` says "return the highest-priority job's (id, data)". 
    If I don't remove it, it's always the same job until someone calls `process_job`? 
    
    Let's look at the provided method names again.
    `add_job`
    `process_job`
    `get_next_job`
    
    Actually, I'll implement `get_next_job` as a pop. Most tests for "Queue" check if the item is removed.
    But if it's removed, `process_job(job_id)` will return `False`.
    
    Let's try a middle ground: `process_job` removes the job on success. `get_next_job` peeks.

    Wait, I'll look at the prompt one more time.
    "orchestrates Jobs using RetryPolicy".
    "must USE RetryPolicy (compose, don't reimplement retry logic)".
    
    Actually, the most likely intended behavior for a "Queue" class:
    `get_next_job` returns the highest priority and *removes* it.
    `process_job` executes the job.
    But `process_job` takes `job_id`.
    
    If `get_next_job` pops, `process_job` can't find the ID.
    This implies `process_job` and `get_next_job` are two different ways to interact with the queue.
    
    Maybe `process_job` is for "I want to run this specific job" and `get_next_job` is for "Give me the next one".
    
    I will implement:
    `add_job`: Append and stable sort.
    `get_next_job`: Peek (Return first item).
    `process_job`: Find by ID, execute, and if success, pop it from the list.
    
    Actually, let's check the FIFO requirement: "FIFO order within the same priority".
    If I sort by priority descending, then for items with same priority, they stay in their original insertion order (since Python sort is stable).
    
    Correct logic for `process_job`:
    Find index of job_id.
    Call retry policy.
    If success, remove at index.

    Wait, one small detail: `RetryPolicy.run` signature:
    `run(self, fn: Callable, data) -> tuple[bool, int]`
    The prompt says `data` but the type hint is missing `dict`. I'll use `dict`.

    One final check on `RetryPolicy`:
    "calls `fn(data)`, retries on any exception until success or `max_attempts` reached".
    
    ```python
    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
    ```
    This logic is correct. If `max_attempts` is 1, it tries once. If fails, returns `(False, 1)`. If success, `(True, 1)`.
    If `max_attempts` is 4. Tries, fails, tries, fails, tries, fails, tries, fails. Total 4 attempts. Returns `(False, 4)`. Correct.

    One detail: `Job` dataclass.
    `priority` default 0.
    `retries` default 0. (The prompt doesn't ask to increment `retries` in the `Job` object, just to use `RetryPolicy`. I'll stick to the prompt's structure).

    Let's verify `JobQueue.get_next_job`:
    "return the highest-priority job's (id, data)".
    Since it's a queue, I'll make it pop. If it pops, `process_job` won't work.
    Wait, I'll make it a peek. If it's a peek, it's safer for the user to call `process_job` later.

    Actually, I'll go with:
    `get_next_job` -> Peek.
    `process_job` -> Find, Run, Pop on Success.

    Wait, I just realized. If I don't remove it in `get_next_job`, but the user calls `process_job` and it succeeds, the `get_next_job` will still return the same job? 
    No, because `process_job` will remove it.
    This seems like a consistent architecture.

    Let's double check "FIFO order within the same priority".
    If I have:
    Job A (P1)
    Job B (P1)
    Job C (P2)
    Sorted result:
    Job C (P2)
    Job A (P1)
    Job B (P1)
    This is correct.

    One minor thing: `retry_policy` instantiation.
    Should I instantiate it in `JobQueue.__init__`? Yes.

    ```python
    from dataclasses import dataclass
    from typing import Callable, Dict, Tuple, Optional, 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, 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)
            self.jobs.sort(key=lambda x: x.priority, reverse=True)

        def process_job(self, job_id: str, Processor: Callable) -> bool:
            index = -1
            for i, job in enumerate(self.jobs):
                if job.id == jobid: # Typo check: job_id
                    index = i
                    break
            # ...
    ```

    Wait, `job_id` vs `jobid`. Fixed.

    Final structure check:
    `Job` class? Yes.
    `RetryPolicy` class? Yes.
    `JobQueue` class? Yes.
    `add_job`? Yes.
    `process_job`? Yes.
    `get_next_job`? Yes.

    One last look at `RetryPolicy.run`.
    `retries` field in `Job` is there but not used by `RetryPolicy`. That's fine, the instructions didn't say to use the field for anything specific, just to define the field.

    Wait, I'll use `from typing import ...` for `Callable` and `Tuple`.

    Code only. No explanation.
</reasoning_content>

<content>
```python
from dataclasses import dataclass
from typing import Callable, Dict, Tuple, Optional, 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, 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)
        # Python's sort is stable, ensuring FIFO for same priority
        self.jobs.sort(key=lambda x: x.priority, reverse=True)

    def process_job(self, job_id: str, Processor: Callable) -> bool:
        index = -1
        for i, job in enumerate(self.jobs):
            if job.id == job_id:
                index = i
                break
        
        if index == -1:
            return False
            
        job = self.jobs[index]
        success, _ = self.retry_policy.run(Processor, job.data)
        
        if success:
            self.jobs.pop(index)
            
        return success

    def get_next_job(self) -> Tuple[str, dict] | None:
        if not self.jobs:
            return None
        # Peek at the highest-priority job (FIFO for ties)
        job = self.jobs[0]
        return (job.id, job.data)
```
</content>