"""Campaign endpoints — CRUD, test management, activation, and auto-generation.

Provides comprehensive campaign lifecycle management including test ordering,
progress tracking, and threat actor integration.
"""

# Import logging
import logging

# Import uuid
import uuid
from datetime import datetime
from typing import Optional

# Import APIRouter, Depends, Query from fastapi
from fastapi import APIRouter, Depends, Query

# Import BaseModel, Field from pydantic
from pydantic import BaseModel, Field

# Import Session from sqlalchemy.orm
from sqlalchemy.orm import Session

# Import get_db from app.database
from app.database import get_db

# Import get_current_user, require_any_role from app.dependencies.auth
from app.dependencies.auth import get_current_user, require_any_role

# Import UnitOfWork from app.domain.unit_of_work
from app.domain.unit_of_work import UnitOfWork

# Import User from app.models.user
from app.models.user import User
from app.models.campaign import Campaign, CampaignTest
from app.models.test import Test
from app.services.campaign_service import generate_campaign_from_threat_actor
from app.services.campaign_crud_service import (
    add_test_to_campaign as crud_add_test,
)

# Import  from app.services.campaign_crud_service
from app.services.campaign_crud_service import (
    complete_campaign as crud_complete,
)

# Import  from app.services.campaign_crud_service
from app.services.campaign_crud_service import (
    create_campaign as crud_create,
    delete_campaign as crud_delete,
    get_campaign_detail as crud_get_detail,
)

# Import  from app.services.campaign_crud_service
from app.services.campaign_crud_service import (
    get_campaign_history as crud_get_history,
)

from app.services.campaign_crud_service import (
    get_campaign_timeline as crud_get_timeline,
)

# Import  from app.services.campaign_crud_service
from app.services.campaign_crud_service import (
    get_campaign_progress_data as crud_get_progress,
)

# Import  from app.services.campaign_crud_service
from app.services.campaign_crud_service import (
    list_campaigns as crud_list,
)

# Import  from app.services.campaign_crud_service
from app.services.campaign_crud_service import (
    remove_test_from_campaign as crud_remove_test,
)

# Import  from app.services.campaign_crud_service
from app.services.campaign_crud_service import (
    schedule_campaign as crud_schedule,
)

# Import  from app.services.campaign_crud_service
from app.services.campaign_crud_service import (
    serialize_campaign,
)

# Import  from app.services.campaign_crud_service
from app.services.campaign_crud_service import (
    update_campaign as crud_update,
)

# Import activate_campaign from app.services.campaign_crud_service
from app.services.campaign_crud_service import (
    activate_campaign as crud_activate,
)

from app.services.campaign_crud_service import (
    submit_campaign_for_approval as crud_submit,
)
from app.services.campaign_crud_service import (
    approve_campaign as crud_approve,
)
from app.services.campaign_crud_service import (
    reject_campaign as crud_reject,
)
from app.services.campaign_crud_service import (
    create_modification_request as crud_create_mod_request,
)
from app.services.campaign_crud_service import (
    approve_modification_request as crud_approve_mod_request,
)
from app.services.campaign_crud_service import (
    reject_modification_request as crud_reject_mod_request,
)
from app.services.campaign_crud_service import (
    list_modification_requests as crud_list_mod_requests,
)
from app.services.campaign_crud_service import (
    serialize_modification_request as crud_serialize_mod_request,
)

# Import log_action from app.services.audit_service
from app.services.audit_service import log_action

# Import notify_role from app.services.notification_service
from app.services.notification_service import notify_role
from app.services.webhook_service import dispatch_webhook

# Assign logger = logging.getLogger(__name__)
logger = logging.getLogger(__name__)

# Assign router = APIRouter(prefix="/campaigns", tags=["campaigns"])
router = APIRouter(prefix="/campaigns", tags=["campaigns"])


def _create_jira_tickets_for_campaign(db: Session, campaign: Campaign, campaign_id: str, user: User) -> None:
    """Create Jira tickets for a campaign and its already-linked tests, if missing.

    Shared by both paths that bring a campaign to ``active``: the admin-only
    emergency `/activate` override and the normal manager `/approve` flow.
    Tests may already be linked to the campaign from when it was still a
    draft, i.e. before any Jira ticket existed for the campaign — so they
    need their own tickets created here too. Best-effort: failures are
    logged, not raised, since a Jira outage must not block activation.
    """
    try:
        from app.services.jira_service import (
            auto_create_campaign_issue,
            auto_create_test_issue,
            get_campaign_jira_key,
            get_test_jira_key,
        )
        campaign_jira_key = get_campaign_jira_key(db, campaign_id)
        if not campaign_jira_key:
            campaign_jira_key = auto_create_campaign_issue(db, campaign, user)
        if campaign_jira_key:
            for ct in campaign.campaign_tests:
                if ct.test and not get_test_jira_key(db, ct.test.id):
                    auto_create_test_issue(
                        db, ct.test, user,
                        parent_ticket_override=campaign_jira_key,
                        campaign_start_date=campaign.start_date,
                    )
        db.commit()
    except Exception:
        logger.exception(
            "Jira ticket creation failed while activating campaign %s",
            campaign_id,
        )


# ── Pydantic schemas ─────────────────────────────────────────────────

class CampaignCreate(BaseModel):
    """Payload for creating a new campaign."""

    # name: str
    name: str
    # Assign description = None
    description: Optional[str] = None
    # Assign type = "custom"
    type: str = "custom"
    # Assign threat_actor_id = None
    threat_actor_id: Optional[str] = None
    # Assign target_platform = None
    target_platform: Optional[str] = None
    # Assign tags = Field(default_factory=list)
    tags: Optional[list[str]] = Field(default_factory=list)
    # Assign scheduled_at = None
    scheduled_at: Optional[str] = None


# Define class CampaignUpdate
class CampaignUpdate(BaseModel):
    """Payload for updating an existing campaign's metadata."""

    # Assign name = None
    name: Optional[str] = None
    # Assign description = None
    description: Optional[str] = None
    # Assign type = None
    type: Optional[str] = None
    # Assign target_platform = None
    target_platform: Optional[str] = None
    # Assign tags = None
    tags: Optional[list[str]] = None
    # Assign scheduled_at = None
    scheduled_at: Optional[str] = None


# Define class AddTestPayload
class AddTestPayload(BaseModel):
    """Payload for adding a test to a campaign."""

    # test_id: str
    test_id: str
    # Assign order_index = None
    order_index: Optional[int] = None
    # Assign depends_on = None
    depends_on: Optional[str] = None
    # Assign phase = None
    phase: Optional[str] = None


# Define class SchedulePayload
class SchedulePayload(BaseModel):
    """Payload for scheduling or rescheduling a campaign run."""

    # is_recurring: bool
    is_recurring: bool
    # Assign recurrence_pattern = None  # weekly, monthly, quarterly
    recurrence_pattern: Optional[str] = None  # weekly, monthly, quarterly
    # Assign next_run_at = None
    next_run_at: Optional[str] = None


class ApprovePayload(BaseModel):
    """Payload for a manager approving a pending campaign."""

    start_date: str  # ISO date/datetime — required, campaign won't activate without it


class RejectPayload(BaseModel):
    """Payload for a manager rejecting a pending campaign."""

    reason: str


class ModificationRequestPayload(BaseModel):
    """Payload for a lead requesting to add/remove a test on an active campaign."""

    action: str  # add_test | remove_test
    test_id: str
    justification: str
    order_index: Optional[int] = None
    phase: Optional[str] = None


class RejectModificationPayload(BaseModel):
    """Payload for a manager rejecting a modification request."""

    review_notes: str


# ---------------------------------------------------------------------------
# GET /campaigns — List campaigns with filters
# ---------------------------------------------------------------------------

@router.get("")
# Define function list_campaigns
def list_campaigns(
    # Entry: type
    type: Optional[str] = Query(None),
    # Entry: status
    status: Optional[str] = Query(None),
    # Entry: threat_actor_id
    threat_actor_id: Optional[str] = Query(None),
    # Entry: search
    search: Optional[str] = Query(None),
    # Entry: offset
    offset: int = Query(0, ge=0),
    # Entry: limit
    limit: int = Query(50, ge=1, le=200),
    # Entry: db
    db: Session = Depends(get_db),
    # Entry: current_user
    current_user: User = Depends(get_current_user),
) -> dict:
    """List campaigns with optional filters and pagination.

    Args:
        type (Optional[str]): Filter by campaign type (e.g. ``custom``, ``threat_actor``).
        status (Optional[str]): Filter by campaign status (e.g. ``draft``, ``active``).
        threat_actor_id (Optional[str]): Filter campaigns linked to a specific threat actor.
        search (Optional[str]): Free-text search against campaign name.
        offset (int): Number of records to skip for pagination.
        limit (int): Maximum number of records to return.
        db (Session): SQLAlchemy database session.
        current_user (User): Authenticated user making the request.

    Returns:
        list: Serialised list of campaign summary dicts.
    """
    # Return crud_list(
    return crud_list(
        db,
        # Keyword argument: type
        type=type,
        # Keyword argument: status
        status=status,
        # Keyword argument: threat_actor_id
        threat_actor_id=threat_actor_id,
        # Keyword argument: search
        search=search,
        # Keyword argument: offset
        offset=offset,
        # Keyword argument: limit
        limit=limit,
    )


# ---------------------------------------------------------------------------
# POST /campaigns — Create campaign
# ---------------------------------------------------------------------------

@router.post("", status_code=201)
# Define function create_campaign
def create_campaign(
    # Entry: payload
    payload: CampaignCreate,
    # Entry: db
    db: Session = Depends(get_db),
    # Entry: current_user
    current_user: User = Depends(require_any_role("red_lead", "blue_lead")),
) -> dict:
    """Create a new campaign.

    Args:
        payload (CampaignCreate): Fields for the new campaign (name, type, threat actor, etc.).
        db (Session): SQLAlchemy database session.
        current_user (User): Authenticated red_lead or blue_lead creating the campaign.

    Returns:
        dict: Serialised representation of the newly created campaign.
    """
    # Open context manager
    with UnitOfWork(db) as uow:
        # Assign result = crud_create(
        result = crud_create(
            db,
            # Keyword argument: creator_id
            creator_id=current_user.id,
            # Keyword argument: name
            name=payload.name,
            # Keyword argument: description
            description=payload.description,
            # Keyword argument: type
            type=payload.type,
            # Keyword argument: threat_actor_id
            threat_actor_id=payload.threat_actor_id,
            # Keyword argument: target_platform
            target_platform=payload.target_platform,
            # Keyword argument: tags
            tags=payload.tags,
            # Keyword argument: scheduled_at
            scheduled_at=payload.scheduled_at,
        )
        campaign_id = result["id"]
        log_action(
            db,
            # Keyword argument: user_id
            user_id=current_user.id,
            # Keyword argument: action
            action="create_campaign",
            # Keyword argument: entity_type
            entity_type="campaign",
            entity_id=campaign_id,
            details={"name": payload.name, "type": payload.type},
        )
        # Call uow.commit()
        uow.commit()

    # Return result
    return result


# ---------------------------------------------------------------------------
# GET /campaigns/{id} — Detail with tests and progress
# ---------------------------------------------------------------------------

@router.get("/{campaign_id}")
# Define function get_campaign
def get_campaign(
    # Entry: campaign_id
    campaign_id: str,
    # Entry: db
    db: Session = Depends(get_db),
    # Entry: current_user
    current_user: User = Depends(get_current_user),
) -> dict:
    """Get detailed campaign info including tests and progress.

    Args:
        campaign_id (str): UUID string of the campaign to retrieve.
        db (Session): SQLAlchemy database session.
        current_user (User): Authenticated user making the request.

    Returns:
        dict: Campaign detail including associated tests and progress metrics.
    """
    # Return crud_get_detail(db, campaign_id)
    return crud_get_detail(db, campaign_id)


# ---------------------------------------------------------------------------
# PATCH /campaigns/{id} — Update campaign
# ---------------------------------------------------------------------------

@router.patch("/{campaign_id}")
# Define function update_campaign
def update_campaign(
    # Entry: campaign_id
    campaign_id: str,
    # Entry: payload
    payload: CampaignUpdate,
    # Entry: db
    db: Session = Depends(get_db),
    # Entry: current_user
    current_user: User = Depends(require_any_role("red_lead", "blue_lead")),
) -> dict:
    """Update a campaign. Only allowed in draft or active state.

    Args:
        campaign_id (str): UUID string of the campaign to update.
        payload (CampaignUpdate): Partial update payload; only set fields are applied.
        db (Session): SQLAlchemy database session.
        current_user (User): Authenticated red_lead or blue_lead performing the update.

    Returns:
        dict: Serialised representation of the updated campaign.
    """
    # Assign update_data = payload.model_dump(exclude_unset=True)
    update_data = payload.model_dump(exclude_unset=True)
    # Open context manager
    with UnitOfWork(db) as uow:
        # Assign result = crud_update(
        result = crud_update(
            db,
            campaign_id,
            # Keyword argument: updater_id
            updater_id=current_user.id,
            # Keyword argument: updater_role
            updater_role=current_user.role,
            **update_data,
        )
        # Call log_action()
        log_action(
            db,
            # Keyword argument: user_id
            user_id=current_user.id,
            # Keyword argument: action
            action="update_campaign",
            # Keyword argument: entity_type
            entity_type="campaign",
            # Keyword argument: entity_id
            entity_id=campaign_id,
            # Keyword argument: details
            details={"updated_fields": list(update_data.keys())},
        )
        # Call uow.commit()
        uow.commit()

    # Return result
    return result


# ---------------------------------------------------------------------------
# POST /campaigns/{id}/submit — Submit draft for manager approval
# ---------------------------------------------------------------------------

@router.post("/{campaign_id}/submit")
def submit_campaign(
    campaign_id: str,
    db: Session = Depends(get_db),
    current_user: User = Depends(require_any_role("red_lead", "blue_lead")),
) -> dict:
    """Submit a draft campaign into the manager's approval queue."""
    with UnitOfWork(db) as uow:
        campaign = crud_submit(
            db, campaign_id,
            submitter_id=current_user.id,
            submitter_role=current_user.role,
        )
        log_action(
            db,
            user_id=current_user.id,
            action="submit_campaign_for_approval",
            entity_type="campaign",
            entity_id=campaign.id,
            details={"name": campaign.name},
        )
        uow.commit()
    db.refresh(campaign)
    return serialize_campaign(db, campaign)


# ---------------------------------------------------------------------------
# POST /campaigns/{id}/approve — Manager approves a pending campaign
# ---------------------------------------------------------------------------

@router.post("/{campaign_id}/approve")
def approve_campaign_endpoint(
    campaign_id: str,
    payload: ApprovePayload,
    db: Session = Depends(get_db),
    # admin passes automatically via require_any_role's built-in bypass — do not add "admin" here
    current_user: User = Depends(require_any_role("manager")),
) -> dict:
    """Manager approves a pending campaign, fixing its start date and activating it."""
    with UnitOfWork(db) as uow:
        campaign = crud_approve(
            db, campaign_id,
            approver_id=current_user.id,
            start_date=payload.start_date,
        )
        log_action(
            db,
            user_id=current_user.id,
            action="approve_campaign",
            entity_type="campaign",
            entity_id=campaign.id,
            details={"start_date": payload.start_date},
        )
        uow.commit()
    db.refresh(campaign)

    # Create Jira tickets for campaign and its already-linked tests (non-fatal).
    # Mirrors the admin-only /activate override — this is the normal path.
    _create_jira_tickets_for_campaign(db, campaign, campaign_id, current_user)

    return serialize_campaign(db, campaign)


# ---------------------------------------------------------------------------
# POST /campaigns/{id}/reject — Manager rejects a pending campaign
# ---------------------------------------------------------------------------

@router.post("/{campaign_id}/reject")
def reject_campaign_endpoint(
    campaign_id: str,
    payload: RejectPayload,
    db: Session = Depends(get_db),
    # admin passes automatically via require_any_role's built-in bypass — do not add "admin" here
    current_user: User = Depends(require_any_role("manager")),
) -> dict:
    """Manager rejects a pending campaign, returning it to draft with a reason."""
    with UnitOfWork(db) as uow:
        campaign = crud_reject(
            db, campaign_id,
            rejecter_id=current_user.id,
            reason=payload.reason,
        )
        log_action(
            db,
            user_id=current_user.id,
            action="reject_campaign",
            entity_type="campaign",
            entity_id=campaign.id,
            details={"reason": payload.reason},
        )
        uow.commit()
    db.refresh(campaign)
    return serialize_campaign(db, campaign)


# ---------------------------------------------------------------------------
# POST /campaigns/{id}/modification-requests — Request a test add/remove
# ---------------------------------------------------------------------------

@router.post("/{campaign_id}/modification-requests", status_code=201)
def create_modification_request_endpoint(
    campaign_id: str,
    payload: ModificationRequestPayload,
    db: Session = Depends(get_db),
    current_user: User = Depends(require_any_role("red_lead", "blue_lead")),
) -> dict:
    """File a request to add/remove a test on an active campaign — needs manager approval."""
    with UnitOfWork(db) as uow:
        request = crud_create_mod_request(
            db, campaign_id,
            requester_id=current_user.id,
            action=payload.action,
            test_id=payload.test_id,
            justification=payload.justification,
            order_index=payload.order_index,
            phase=payload.phase,
        )
        log_action(
            db,
            user_id=current_user.id,
            action="request_campaign_modification",
            entity_type="campaign",
            entity_id=campaign_id,
            details={
                "action": payload.action,
                "test_id": payload.test_id,
                "justification": payload.justification,
            },
        )
        uow.commit()
    return crud_serialize_mod_request(db, request)


# ---------------------------------------------------------------------------
# GET /campaigns/{id}/modification-requests — List requests for one campaign
# ---------------------------------------------------------------------------

@router.get("/{campaign_id}/modification-requests")
def list_campaign_modification_requests_endpoint(
    campaign_id: str,
    db: Session = Depends(get_db),
    current_user: User = Depends(get_current_user),
) -> list:
    """List modification requests filed against a specific campaign."""
    return crud_list_mod_requests(db, campaign_id=campaign_id)


# ---------------------------------------------------------------------------
# GET /campaigns/modification-requests/pending — Manager's global queue
# ---------------------------------------------------------------------------

@router.get("/modification-requests/pending")
def list_pending_modification_requests_endpoint(
    db: Session = Depends(get_db),
    # admin passes automatically via require_any_role's built-in bypass — do not add "admin" here
    current_user: User = Depends(require_any_role("manager")),
) -> list:
    """List all modification requests awaiting manager review, across all campaigns."""
    return crud_list_mod_requests(db, status="pending")


# ---------------------------------------------------------------------------
# POST /campaigns/modification-requests/{id}/approve — Apply the requested change
# ---------------------------------------------------------------------------

@router.post("/modification-requests/{request_id}/approve")
def approve_modification_request_endpoint(
    request_id: str,
    db: Session = Depends(get_db),
    # admin passes automatically via require_any_role's built-in bypass — do not add "admin" here
    current_user: User = Depends(require_any_role("manager")),
) -> dict:
    """Manager approves a modification request — the test change is applied now."""
    with UnitOfWork(db) as uow:
        request = crud_approve_mod_request(db, request_id, reviewer_id=current_user.id)
        log_action(
            db,
            user_id=current_user.id,
            action="approve_campaign_modification",
            entity_type="campaign",
            entity_id=str(request.campaign_id),
            details={
                "request_id": request_id,
                "action": request.action,
                "test_id": str(request.test_id) if request.test_id else None,
            },
        )
        uow.commit()
    return crud_serialize_mod_request(db, request)


# ---------------------------------------------------------------------------
# POST /campaigns/modification-requests/{id}/reject — Deny the requested change
# ---------------------------------------------------------------------------

@router.post("/modification-requests/{request_id}/reject")
def reject_modification_request_endpoint(
    request_id: str,
    payload: RejectModificationPayload,
    db: Session = Depends(get_db),
    # admin passes automatically via require_any_role's built-in bypass — do not add "admin" here
    current_user: User = Depends(require_any_role("manager")),
) -> dict:
    """Manager rejects a modification request. No change is applied."""
    with UnitOfWork(db) as uow:
        request = crud_reject_mod_request(
            db, request_id,
            reviewer_id=current_user.id,
            review_notes=payload.review_notes,
        )
        log_action(
            db,
            user_id=current_user.id,
            action="reject_campaign_modification",
            entity_type="campaign",
            entity_id=str(request.campaign_id),
            details={"request_id": request_id, "review_notes": payload.review_notes},
        )
        uow.commit()
    return crud_serialize_mod_request(db, request)


# ---------------------------------------------------------------------------
# DELETE /campaigns/{id} — Delete campaign
# ---------------------------------------------------------------------------

@router.delete("/{campaign_id}", status_code=204)
def delete_campaign(
    campaign_id: str,
    delete_tests: bool = Query(False, description="Also delete associated tests"),
    db: Session = Depends(get_db),
    current_user: User = Depends(get_current_user),
):
    """Delete a campaign. Only draft campaigns can be deleted (admins can delete any)."""
    with UnitOfWork(db) as uow:
        crud_delete(
            db,
            campaign_id,
            deleter_id=current_user.id,
            deleter_role=current_user.role,
            delete_tests=delete_tests,
        )
        log_action(
            db,
            user_id=current_user.id,
            action="delete_campaign",
            entity_type="campaign",
            entity_id=campaign_id,
            details={"delete_tests": delete_tests},
        )
        uow.commit()


# ---------------------------------------------------------------------------
# POST /campaigns/{id}/tests — Add test to campaign
# ---------------------------------------------------------------------------

@router.post("/{campaign_id}/tests")
# Define function add_test_to_campaign
def add_test_to_campaign(
    # Entry: campaign_id
    campaign_id: str,
    # Entry: payload
    payload: AddTestPayload,
    # Entry: db
    db: Session = Depends(get_db),
    # Entry: current_user
    current_user: User = Depends(require_any_role("red_lead", "blue_lead")),
) -> dict:
    """Add a test to a campaign with optional ordering and dependency.

    Args:
        campaign_id (str): UUID string of the target campaign.
        payload (AddTestPayload): Test ID plus optional order index, dependency, and phase.
        db (Session): SQLAlchemy database session.
        current_user (User): Authenticated red_lead or blue_lead adding the test.

    Returns:
        dict: The created campaign-test association record.
    """
    # Open context manager
    with UnitOfWork(db) as uow:
        # Assign result = crud_add_test(
        result = crud_add_test(
            db,
            campaign_id,
            # Keyword argument: test_id
            test_id=payload.test_id,
            # Keyword argument: order_index
            order_index=payload.order_index,
            # Keyword argument: depends_on
            depends_on=payload.depends_on,
            # Keyword argument: phase
            phase=payload.phase,
        )
        # Call uow.commit()
        uow.commit()

    return result


# ---------------------------------------------------------------------------
# DELETE /campaigns/{id}/tests/{campaign_test_id} — Remove test from campaign
# ---------------------------------------------------------------------------

@router.delete("/{campaign_id}/tests/{campaign_test_id}")
# Define function remove_test_from_campaign
def remove_test_from_campaign(
    # Entry: campaign_id
    campaign_id: str,
    # Entry: campaign_test_id
    campaign_test_id: str,
    # Entry: db
    db: Session = Depends(get_db),
    # Entry: current_user
    current_user: User = Depends(require_any_role("red_lead", "blue_lead")),
) -> dict:
    """Remove a test from a campaign.

    Args:
        campaign_id (str): UUID string of the campaign.
        campaign_test_id (str): UUID string of the campaign-test association to remove.
        db (Session): SQLAlchemy database session.
        current_user (User): Authenticated red_lead or blue_lead removing the test.

    Returns:
        dict: Confirmation message with key ``detail``.
    """
    # Open context manager
    with UnitOfWork(db) as uow:
        # Call crud_remove_test()
        crud_remove_test(db, campaign_id, campaign_test_id)
        # Call uow.commit()
        uow.commit()
    # Return {"detail": "Test removed from campaign"}
    return {"detail": "Test removed from campaign"}


# ---------------------------------------------------------------------------
# POST /campaigns/{id}/activate — Activate campaign
# ---------------------------------------------------------------------------

@router.post("/{campaign_id}/activate")
# Define function activate_campaign
def activate_campaign(
    # Entry: campaign_id
    campaign_id: str,
    db: Session = Depends(get_db),
    # admin passes automatically via require_any_role's built-in bypass — do not add "admin" here
    current_user: User = Depends(require_any_role("admin")),
):
    """Admin-only emergency override: activate a draft campaign directly, bypassing the approval queue.

    Every other path to 'active' goes through /submit -> /approve, where the
    manager sets start_date. A draft campaign never has a start_date set
    (only /approve sets it, in the same call that moves the campaign to
    'active'), so there is no "scheduled for the future" case to guard
    against here anymore.
    """
    with UnitOfWork(db) as uow:
        # Assign campaign = crud_activate(db, campaign_id)
        campaign = crud_activate(db, campaign_id)
        # Call notify_role()
        notify_role(
            db,
            # Keyword argument: role
            role="red_tech",
            # Keyword argument: type
            type="campaign_activated",
            # Keyword argument: title
            title="Campaign activated",
            # Keyword argument: message
            message=f'Campaign "{campaign.name}" has been activated.',
            # Keyword argument: entity_type
            entity_type="campaign",
            # Keyword argument: entity_id
            entity_id=campaign.id,
        )
        # Call log_action()
        log_action(
            db,
            # Keyword argument: user_id
            user_id=current_user.id,
            # Keyword argument: action
            action="activate_campaign",
            # Keyword argument: entity_type
            entity_type="campaign",
            # Keyword argument: entity_id
            entity_id=campaign.id,
            # Keyword argument: details
            details={"name": campaign.name},
        )
        # Call uow.commit()
        uow.commit()
    # Reload ORM object attributes from the database
    db.refresh(campaign)

    # Create Jira tickets for campaign and tests at activation time (non-fatal).
    # Campaign ticket is created here if it doesn't already exist (deferred from creation).
    _create_jira_tickets_for_campaign(db, campaign, campaign_id, current_user)

    return serialize_campaign(db, campaign)


# ---------------------------------------------------------------------------
# POST /campaigns/{id}/complete — Mark campaign as completed
# ---------------------------------------------------------------------------

@router.post("/{campaign_id}/complete")
# Define function complete_campaign
def complete_campaign(
    # Entry: campaign_id
    campaign_id: str,
    # Entry: db
    db: Session = Depends(get_db),
    # Entry: current_user
    current_user: User = Depends(require_any_role("red_lead", "admin")),
) -> dict:
    """Mark a campaign as completed.

    Args:
        campaign_id (str): UUID string of the campaign to complete.
        db (Session): SQLAlchemy database session.
        current_user (User): Authenticated red_lead or admin completing the campaign.

    Returns:
        dict: Serialised representation of the completed campaign.
    """
    # Open context manager
    with UnitOfWork(db) as uow:
        # Assign campaign = crud_complete(db, campaign_id)
        campaign = crud_complete(db, campaign_id)
        # Call log_action()
        log_action(
            db,
            # Keyword argument: user_id
            user_id=current_user.id,
            # Keyword argument: action
            action="complete_campaign",
            # Keyword argument: entity_type
            entity_type="campaign",
            # Keyword argument: entity_id
            entity_id=campaign.id,
            # Keyword argument: details
            details={"name": campaign.name},
        )
        # Call uow.commit()
        uow.commit()
    # Reload ORM object attributes from the database
    db.refresh(campaign)
    dispatch_webhook("campaign.completed", {"campaign_id": str(campaign.id), "name": campaign.name})

    # Return serialize_campaign(db, campaign)
    return serialize_campaign(db, campaign)


# ---------------------------------------------------------------------------
# GET /campaigns/{id}/progress — Campaign progress
# ---------------------------------------------------------------------------

@router.get("/{campaign_id}/progress")
# Define function get_campaign_progress_endpoint
def get_campaign_progress_endpoint(
    # Entry: campaign_id
    campaign_id: str,
    # Entry: db
    db: Session = Depends(get_db),
    # Entry: current_user
    current_user: User = Depends(get_current_user),
) -> dict:
    """Get progress statistics for a campaign.

    Args:
        campaign_id (str): UUID string of the campaign.
        db (Session): SQLAlchemy database session.
        current_user (User): Authenticated user making the request.

    Returns:
        dict: Progress breakdown including counts by test state and overall percentage.
    """
    # Return crud_get_progress(db, campaign_id)
    return crud_get_progress(db, campaign_id)


# ---------------------------------------------------------------------------
# POST /campaigns/from-threat-actor/{actor_id} — Auto-generate campaign
# ---------------------------------------------------------------------------

class GenerateFromActorPayload(BaseModel):
    start_date: Optional[datetime] = None


@router.post("/from-threat-actor/{actor_id}", status_code=201)
# Define function generate_campaign_from_actor
def generate_campaign_from_actor(
    # Entry: actor_id
    actor_id: str,
    payload: GenerateFromActorPayload = GenerateFromActorPayload(),
    db: Session = Depends(get_db),
    # Entry: current_user
    current_user: User = Depends(require_any_role("red_lead", "blue_lead")),
) -> dict:
    """Auto-generate a campaign from a threat actor's uncovered techniques.

    Creates tests from the best available templates and orders them
    by kill chain phase.

    Args:
        actor_id (str): UUID string of the threat actor to generate a campaign for.
        db (Session): SQLAlchemy database session.
        current_user (User): Authenticated red_lead or blue_lead requesting the generation.

    Returns:
        dict: Serialised representation of the newly generated campaign.
    """
    campaign = generate_campaign_from_threat_actor(
        db,
        uuid.UUID(actor_id),
        current_user,
        start_date=payload.start_date,
    )

    # Open context manager
    with UnitOfWork(db) as uow:
        # Call log_action()
        log_action(
            db,
            # Keyword argument: user_id
            user_id=current_user.id,
            # Keyword argument: action
            action="generate_campaign",
            # Keyword argument: entity_type
            entity_type="campaign",
            # Keyword argument: entity_id
            entity_id=campaign.id,
            # Keyword argument: details
            details={"actor_id": actor_id, "campaign_name": campaign.name},
        )
        # Call uow.commit()
        uow.commit()

    # Return serialize_campaign(db, campaign)
    return serialize_campaign(db, campaign)


# ---------------------------------------------------------------------------
# PATCH /campaigns/{id}/schedule — Configure recurrence
# ---------------------------------------------------------------------------

@router.patch("/{campaign_id}/schedule")
# Define function schedule_campaign
def schedule_campaign(
    # Entry: campaign_id
    campaign_id: str,
    # Entry: payload
    payload: SchedulePayload,
    # Entry: db
    db: Session = Depends(get_db),
    # Entry: current_user
    current_user: User = Depends(require_any_role("red_lead", "blue_lead")),
) -> dict:
    """Configure or update the recurrence schedule for a campaign.

    Only the campaign creator or admin can change scheduling.

    Args:
        campaign_id (str): UUID string of the campaign to schedule.
        payload (SchedulePayload): Recurrence flag, pattern, and next run timestamp.
        db (Session): SQLAlchemy database session.
        current_user (User): Authenticated red_lead or blue_lead (must be owner or admin).

    Returns:
        dict: Serialised representation of the campaign with updated schedule fields.
    """
    # Open context manager
    with UnitOfWork(db) as uow:
        # Assign campaign = crud_schedule(
        campaign = crud_schedule(
            db,
            campaign_id,
            # Keyword argument: owner_id
            owner_id=current_user.id,
            # Keyword argument: owner_role
            owner_role=current_user.role,
            # Keyword argument: is_recurring
            is_recurring=payload.is_recurring,
            # Keyword argument: recurrence_pattern
            recurrence_pattern=payload.recurrence_pattern,
            # Keyword argument: next_run_at
            next_run_at=payload.next_run_at,
        )
        # Call log_action()
        log_action(
            db,
            # Keyword argument: user_id
            user_id=current_user.id,
            # Keyword argument: action
            action="schedule_campaign",
            # Keyword argument: entity_type
            entity_type="campaign",
            # Keyword argument: entity_id
            entity_id=campaign.id,
            # Keyword argument: details
            details={
                # Literal argument value
                "is_recurring": campaign.is_recurring,
                # Literal argument value
                "recurrence_pattern": campaign.recurrence_pattern,
                # Literal argument value
                "next_run_at": campaign.next_run_at.isoformat() if campaign.next_run_at else None,
            },
        )
        # Call uow.commit()
        uow.commit()
    # Reload ORM object attributes from the database
    db.refresh(campaign)

    # Return serialize_campaign(db, campaign)
    return serialize_campaign(db, campaign)


# ---------------------------------------------------------------------------
# GET /campaigns/{id}/history — Execution history (child campaigns)
# ---------------------------------------------------------------------------

@router.get("/{campaign_id}/history")
# Define function get_campaign_history
def get_campaign_history(
    # Entry: campaign_id
    campaign_id: str,
    # Entry: db
    db: Session = Depends(get_db),
    # Entry: current_user
    current_user: User = Depends(get_current_user),
) -> list:
    """List all child campaigns (execution history) of a recurring campaign.

    Args:
        campaign_id (str): UUID string of the parent recurring campaign.
        db (Session): SQLAlchemy database session.
        current_user (User): Authenticated user making the request.

    Returns:
        list: Serialised list of child campaign dicts ordered by creation date.
    """
    # Return crud_get_history(db, campaign_id)
    return crud_get_history(db, campaign_id)


# ---------------------------------------------------------------------------
# GET /campaigns/{id}/timeline — Audit-log history for this campaign
# ---------------------------------------------------------------------------

@router.get("/{campaign_id}/timeline")
def get_campaign_timeline_endpoint(
    campaign_id: str,
    db: Session = Depends(get_db),
    current_user: User = Depends(get_current_user),
) -> list:
    """Return the chronological audit-log history for a campaign."""
    return crud_get_timeline(db, campaign_id)


# ---------------------------------------------------------------------------
# GET /campaigns/{id}/timing-summary — Aggregated timing across campaign tests
# ---------------------------------------------------------------------------


def _seconds_between(start: datetime | None, end: datetime | None) -> int:
    """Return elapsed seconds between two datetimes; 0 if either is None."""
    if not start or not end:
        return 0
    diff = (end - start).total_seconds()
    return max(0, int(diff))


@router.get("/{campaign_id}/timing-summary")
def get_campaign_timing_summary(
    campaign_id: str,
    db: Session = Depends(get_db),
    current_user: User = Depends(get_current_user),
):
    """Return aggregated Red/Blue timing metrics for all tests in a campaign.

    For each test we calculate:
    - red_execution_secs  : red_started_at → blue_started_at (minus red_paused_seconds)
    - blue_queue_secs     : blue_started_at → blue_work_started_at (waiting for Blue pick-up)
    - blue_evaluation_secs: blue_work_started_at → first validation timestamp (minus blue_paused_seconds)
    - total_secs          : sum of the three phases

    Returns totals + per-test breakdown.
    """
    # Load campaign
    campaign = db.query(Campaign).filter(Campaign.id == campaign_id).first()
    if not campaign:
        from fastapi import HTTPException
        raise HTTPException(status_code=404, detail="Campaign not found")

    # Load all tests for this campaign
    test_ids = [
        ct.test_id
        for ct in db.query(CampaignTest).filter(CampaignTest.campaign_id == campaign.id).all()
    ]
    tests = db.query(Test).filter(Test.id.in_(test_ids)).all() if test_ids else []

    breakdown = []
    total_red = 0
    total_queue = 0
    total_blue = 0

    for t in tests:
        # Red execution: from start-execution to submit-to-blue, minus paused time
        red_secs = max(
            0,
            _seconds_between(t.red_started_at, t.blue_started_at) - (t.red_paused_seconds or 0),
        )

        # Blue queue: from receiving the test to actually starting evaluation
        queue_secs = _seconds_between(t.blue_started_at, t.blue_work_started_at)

        # Blue evaluation: from starting evaluation to first validation, minus paused time
        eval_end = t.red_validated_at or t.blue_validated_at
        blue_secs = max(
            0,
            _seconds_between(t.blue_work_started_at, eval_end) - (t.blue_paused_seconds or 0),
        )

        total_red += red_secs
        total_queue += queue_secs
        total_blue += blue_secs

        breakdown.append({
            "test_id": str(t.id),
            "test_name": t.name,
            "state": t.state.value if t.state else None,
            "red_execution_secs": red_secs,
            "blue_queue_secs": queue_secs,
            "blue_evaluation_secs": blue_secs,
            "total_secs": red_secs + queue_secs + blue_secs,
            "has_timing": bool(t.red_started_at),
        })

    total_secs = total_red + total_queue + total_blue

    return {
        "campaign_id": campaign_id,
        "campaign_name": campaign.name,
        "tests_total": len(tests),
        "tests_with_timing": sum(1 for b in breakdown if b["has_timing"]),
        "red_execution_secs": total_red,
        "blue_queue_secs": total_queue,
        "blue_evaluation_secs": total_blue,
        "total_secs": total_secs,
        "breakdown": sorted(breakdown, key=lambda x: -(x["total_secs"])),
    }
