# Hook System Integration

The django-cronjob-utils package now integrates with the `django-package-hooks` system, allowing you to inject custom logic before and after cronjob execution.

## Overview

The hook system provides two types of hooks:

- **PRE hooks**: Execute before the cronjob runs. Can reject execution based on conditions.
- **POST hooks**: Execute after the cronjob completes. Used for reactions like logging and notifications.

## Quick Start

### 1. Install Dependencies

The `django-package-hooks` dependency is automatically installed with django-cronjob-utils:

```bash
pip install django-cronjob-utils
```

### 2. Register Hooks

Create a hooks file in your Django app:

```python
# myapp/cronjob_hooks.py
from django_package_hooks import HookType, register_hook, HookRejectionError
from django_cronjob_utils.hooks import CronjobHookContext
from django_cronjob_utils.models import CronExecution
import logging

logger = logging.getLogger(__name__)


def check_dependency_completed(context: CronjobHookContext) -> bool:
    """Check if a dependent cronjob completed successfully."""
    dependency_task_code = context.options.get('dependency_task_code')
    
    if not dependency_task_code:
        return True
    
    dependency_completed = CronExecution.objects.filter(
        task_code=dependency_task_code,
        execution_date=context.execution_date,
        success=True,
        completed=True
    ).exists()
    
    if not dependency_completed:
        raise HookRejectionError(
            error_code='DEPENDENCY_NOT_COMPLETED',
            message=f'Dependency task {dependency_task_code} not completed',
            details={
                'dependency_task_code': dependency_task_code,
                'execution_date': str(context.execution_date)
            }
        )
    
    return True


def register_cronjob_hooks():
    """Register all cronjob hooks."""
    register_hook(
        name='check_dependency',
        hook_type=HookType.PRE,
        callback=check_dependency_completed,
        operation='execute',
        priority=10
    )
```

### 3. Register in AppConfig

Register your hooks in your app's `AppConfig.ready()` method:

```python
# myapp/apps.py
from django.apps import AppConfig


class MyAppConfig(AppConfig):
    name = 'myapp'
    
    def ready(self):
        from myapp.cronjob_hooks import register_cronjob_hooks
        register_cronjob_hooks()
```

## Hook Context

The `CronjobHookContext` provides information about the cronjob execution:

```python
@dataclass(frozen=True)
class CronjobHookContext(HookContext):
    operation: str           # Always 'execute' for cronjobs
    task_code: str          # Unique task code (e.g., 'A001')
    task_name: str          # Human-readable name (e.g., 'daily-report')
    execution_date: date    # Date being executed
    execution_pattern: str  # Pattern (STANDARD, ALWAYS, etc.)
    execution_id: int | None # Database ID (None in PRE hooks, set in POST hooks)
    retry_count: int        # Number of retries
    options: dict           # Task options (force, rerun, etc.)
    metadata: dict          # Mutable dict for inter-hook communication
```

**Important**: `execution_id` is `None` during PRE hooks (before DB record creation) and contains the actual database ID during POST hooks (after record creation).

## Use Cases

### Use Case 1: Dependency Checks

Ensure a cronjob only runs after another cronjob has completed successfully:

```python
@register_task('calc-commission', 'A002')
class CalcCommissionTask(CronTask):
    def __init__(self, execution_date: date, **options):
        options['dependency_task_code'] = 'A001'  # Depends on task A001
        super().__init__(execution_date, **options)
    
    def execute(self, date: date) -> dict:
        # Calculate commissions
        return {'error': False, 'message': 'Success'}
```

With the `check_dependency_completed` hook registered, task A002 will only run if task A001 has completed successfully for the same execution date.

### Use Case 2: Business Rules

Enforce business rules before execution:

```python
def check_weekday_only(context: CronjobHookContext) -> bool:
    """Only allow execution on weekdays."""
    if context.execution_date.weekday() >= 5:  # Weekend
        raise HookRejectionError(
            error_code='INVALID_EXECUTION_DAY',
            message='Task can only run on weekdays'
        )
    return True

register_hook('weekday_check', HookType.PRE, check_weekday_only, 
              operation='execute', priority=30)
```

### Use Case 3: Custom Logging

Add custom logging after execution:

```python
def log_to_monitoring_service(context: CronjobHookContext) -> None:
    """Send execution data to monitoring service."""
    if context.execution_id:
        execution = CronExecution.objects.get(pk=context.execution_id)
        
        monitoring_service.send({
            'task': context.task_code,
            'success': execution.success,
            'duration': execution.duration,
            'date': str(context.execution_date)
        })

register_hook('monitoring', HookType.POST, log_to_monitoring_service,
              operation='execute', priority=200)
```

## Disabling Hooks

You can disable hooks for specific task executions:

```python
task = MyTask(date.today(), enable_hooks=False)
task.run()
```

Or from the command line:

```bash
python manage.py run_cron_task my-task 2024-01-15 --disable-hooks
```

## Hook Priority Guidelines

Lower numbers = higher priority (execute first):

| Priority Range | Use For |
|---------------|---------|
| 0-10 | Critical security and dependency checks |
| 10-50 | Authorization and validation |
| 50-100 | Business rules |
| 100-200 | Logging |
| 200-500 | Notifications |
| 500+ | Analytics and cleanup |

## Example Hooks

See `examples/cronjob_hooks.py` for complete working examples including:

- ✅ Dependency checking
- ✅ Execution time window validation
- ✅ Date range validation
- ✅ Audit logging
- ✅ Custom notifications
- ✅ Statistics tracking

## Testing Hooks

In your tests, you can clear and re-register hooks:

```python
from django_cronjob_utils import clear_cronjob_hooks
from django.test import TestCase

class CronTaskTestCase(TestCase):
    def setUp(self):
        clear_cronjob_hooks()
        # Register test-specific hooks if needed
    
    def tearDown(self):
        clear_cronjob_hooks()
```

## Best Practices

1. **Use PRE hooks for validation only** - Don't modify state in PRE hooks
2. **Use POST hooks for side effects** - Logging, notifications, cleanup
3. **Provide clear error codes** - Use ALL_CAPS format for error codes
4. **Set appropriate priorities** - Ensure hooks run in the correct order
5. **Test hooks in isolation** - Write unit tests for individual hooks
6. **Document hook dependencies** - If hooks depend on each other, document it

## Hook Execution Flow

```
1. Task.run() called
2. Validation
3. Duplicate check
4. ✨ PRE HOOKS EXECUTED ✨
   - If any hook rejects: Return failed result, NO DB RECORD CREATED
   - Hooks receive context with execution_id=None
5. Lock acquired (only if PRE hooks pass)
6. Execution record created
7. Task.execute() called
8. Update execution record
9. ✨ POST HOOKS EXECUTED ✨
   - Errors captured but don't affect success
   - Hooks receive context with execution_id set
10. Notifications sent (if failed)
11. Retry scheduled (if needed)
```

**Key Points:**
- PRE hooks run **before** lock acquisition to avoid creating DB records for rejected tasks
- PRE hooks receive `context.execution_id = None` (no record yet)
- POST hooks receive `context.execution_id` with the actual database ID
- If PRE hooks reject, no execution record is created in the database
```

## API Reference

### CronjobHookContext

The context object passed to all hooks.

### get_global_cronjob_hook_manager()

Get the global hook manager for cronjob operations.

```python
from django_cronjob_utils.hooks import get_global_cronjob_hook_manager

manager = get_global_cronjob_hook_manager()
```

### clear_cronjob_hooks()

Clear all registered cronjob hooks (useful for testing).

```python
from django_cronjob_utils import clear_cronjob_hooks

clear_cronjob_hooks()
```

## Further Reading

- [django-package-hooks README](https://github.com/yourusername/django-package-hooks) - Complete hook system documentation
- [Hook Examples](../examples/cronjob_hooks.py) - Working examples
- [Architecture](./architecture.md) - Package architecture details
