"""Approval Engine services — business logic.

All write operations go through these functions; views must NOT call ORM
save/delete directly on ApprovalEngine models.
"""

from __future__ import annotations

from datetime import datetime

from django.db import transaction
from django.utils import timezone

from simorgh.apps.approval_engine.models import (
    ApprovalMatrix,
    ApprovalRequest,
    ApprovalStep,
    StepStatus,
    ApprovalDecision,
    ApprovalStatus,
    ApprovalType,
)


# ── Approval Request ───────────────────────────────────────────────────────

def create_approval_request(
    *,
    tenant_id: int,
    organization_node_id: int,
    title: str,
    requested_by_id: int,
    description: str = "",
    approval_type: str = "single",
    priority: str = "medium",
    due_date: datetime | None = None,
    is_confidential: bool = False,
    content_type_id: int | None = None,
    object_id: int | None = None,
    matrix_id: int | None = None,
    steps_config: list[dict] | None = None,
) -> ApprovalRequest:
    from simorgh.apps.events.bus import dispatch
    from simorgh.core.audit import record_service_event

    with transaction.atomic():
        approval_request = ApprovalRequest.objects.create(
            tenant_id=tenant_id,
            organization_node_id=organization_node_id,
            title=title.strip(),
            description=description,
            approval_type=approval_type,
            status=ApprovalStatus.PENDING,
            priority=priority,
            requested_by_id=requested_by_id,
            due_date=due_date,
            is_confidential=is_confidential,
            content_type_id=content_type_id,
            object_id=object_id,
            matrix_id=matrix_id,
        )
        if steps_config:
            _create_steps_for_request(approval_request, steps_config)
        if approval_request.steps.exists() and approval_request.steps.count() > 0:
            approval_request.status = ApprovalStatus.IN_PROGRESS
            approval_request.save(update_fields=["status"])

        if approval_type == ApprovalType.SINGLE:
            approval_request.status = ApprovalStatus.PENDING
            approval_request.save(update_fields=["status"])

    record_service_event("approval_engine.request.created", resource=approval_request, after={
        "requested_by_id": requested_by_id,
    })
    dispatch("approval.requested", {
        "approval_request_id": str(approval_request.public_id),
        "requested_by_id":     requested_by_id,
        "tenant_id":           tenant_id,
    })
    return approval_request


def _create_steps_for_request(
    approval_request: ApprovalRequest,
    steps_config: list[dict],
) -> list[ApprovalStep]:
    steps = []
    for step_data in steps_config:
        step = ApprovalStep.objects.create(
            request=approval_request,
            step_order=step_data.get("step_order", len(steps) + 1),
            approver_id=step_data.get("approver_id"),
            approver_role_id=step_data.get("approver_role_id"),
            approver_org_unit_id=step_data.get("approver_org_unit_id"),
            due_date=step_data.get("due_date"),
            is_parallel=step_data.get("is_parallel", False),
            status=StepStatus.PENDING,
        )
        steps.append(step)
    return steps


def update_approval_request(
    approval_request: ApprovalRequest,
    *,
    title: str | None = None,
    description: str | None = None,
    priority: str | None = None,
    due_date: datetime | None = ...,  # type: ignore[assignment]
    is_confidential: bool | None = None,
) -> ApprovalRequest:
    _SENTINEL = ...  # type: ignore[assignment]

    if title is not None:
        approval_request.title = title.strip()
    if description is not None:
        approval_request.description = description
    if priority is not None:
        approval_request.priority = priority
    if due_date is not _SENTINEL:
        approval_request.due_date = due_date  # type: ignore[assignment]
    if is_confidential is not None:
        approval_request.is_confidential = is_confidential

    approval_request.save()
    return approval_request


def delete_approval_request(approval_request: ApprovalRequest) -> None:
    approval_request.delete()


def cancel_approval_request(approval_request: ApprovalRequest, *, actor_id: int | None = None) -> ApprovalRequest:
    from simorgh.apps.events.bus import dispatch
    from simorgh.core.audit import record_service_event

    if approval_request.status in (ApprovalStatus.CANCELLED, ApprovalStatus.EXPIRED):
        raise ValueError("Cannot cancel an approval request that is already cancelled or expired.")

    approval_request.status = ApprovalStatus.CANCELLED
    approval_request.completed_at = timezone.now()
    approval_request.save(update_fields=["status", "completed_at", "updated_at"])

    approval_request.steps.filter(
        status__in=(StepStatus.PENDING, StepStatus.IN_PROGRESS)
    ).update(status=StepStatus.SKIPPED)

    record_service_event("approval_engine.request.cancelled", resource=approval_request)
    dispatch("approval.cancelled", {
        "approval_request_id": str(approval_request.public_id),
        "cancelled_by_id":     actor_id,
        "tenant_id":           approval_request.tenant_id,
    })
    return approval_request


# ── Approval Steps ──────────────────────────────────────────────────────────

def decide_step(
    step: ApprovalStep,
    *,
    decision: str,
    decided_by_id: int,
    note: str = "",
    delegate_to_id: int | None = None,
) -> ApprovalStep:
    from simorgh.apps.events.bus import dispatch
    from simorgh.core.audit import record_service_event

    if step.status not in (StepStatus.PENDING, StepStatus.IN_PROGRESS):
        raise ValueError("Cannot decide on a step that is not pending or in progress.")

    step.status = StepStatus.PENDING  # will be updated below
    step.decision = decision
    step.decided_by_id = decided_by_id
    step.decided_at = timezone.now()
    step.note = note

    if decision == ApprovalDecision.APPROVE:
        step.status = StepStatus.APPROVED
    elif decision == ApprovalDecision.REJECT:
        step.status = StepStatus.REJECTED
    elif decision == ApprovalDecision.RETURN:
        step.status = StepStatus.RETURNED
    elif decision == ApprovalDecision.REQUEST_CHANGES:
        step.status = StepStatus.RETURNED
    elif decision == ApprovalDecision.ABSTAIN:
        step.status = StepStatus.SKIPPED
    elif decision == ApprovalDecision.DELEGATE:
        if delegate_to_id:
            step.delegated_to_id = delegate_to_id
        step.status = StepStatus.PENDING
    else:
        raise ValueError(f"Unknown decision: {decision}")

    step.save(update_fields=["status", "decision", "decided_by_id", "decided_at", "note", "updated_at"])

    _update_request_status(step.request)

    record_service_event("approval_engine.step.decided", resource=step, after={
        "decision": decision,
        "decided_by_id": decided_by_id,
    })
    dispatch("approval.step_decided", {
        "approval_request_id": str(step.request.public_id),
        "step_id":             str(step.public_id),
        "decision":            decision,
        "decided_by_id":       decided_by_id,
        "tenant_id":           step.request.tenant_id,
    })
    return step


def _update_request_status(approval_request: ApprovalRequest) -> None:
    steps = approval_request.steps.all()
    if not steps:
        return

    rejected = any(s.status == StepStatus.REJECTED for s in steps)
    returned = any(s.status == StepStatus.RETURNED for s in steps)
    all_approved = all(
        s.status in (StepStatus.APPROVED, StepStatus.SKIPPED) for s in steps
    )

    if rejected:
        new_status = ApprovalStatus.REJECTED
    elif returned:
        new_status = ApprovalStatus.RETURNED
    elif all_approved:
        new_status = ApprovalStatus.APPROVED
    else:
        new_status = ApprovalStatus.IN_PROGRESS

    update_fields = ["status"]
    if new_status in (ApprovalStatus.APPROVED, ApprovalStatus.REJECTED, ApprovalStatus.RETURNED):
        if not approval_request.completed_at:
            approval_request.completed_at = timezone.now()
            update_fields.append("completed_at")

    if approval_request.status != new_status:
        approval_request.status = new_status
        approval_request.save(update_fields=update_fields)

        if new_status == ApprovalStatus.APPROVED:
            _dispatch_approved(approval_request)
        elif new_status == ApprovalStatus.REJECTED:
            _dispatch_rejected(approval_request)
        elif new_status == ApprovalStatus.RETURNED:
            _dispatch_returned(approval_request)


def _dispatch_approved(approval_request: ApprovalRequest) -> None:
    from simorgh.apps.events.bus import dispatch
    from simorgh.core.audit import record_service_event
    record_service_event("approval_engine.request.approved", resource=approval_request)
    dispatch("approval.approved", {
        "approval_request_id": str(approval_request.public_id),
        "tenant_id":           approval_request.tenant_id,
    })


def _dispatch_rejected(approval_request: ApprovalRequest) -> None:
    from simorgh.apps.events.bus import dispatch
    from simorgh.core.audit import record_service_event
    record_service_event("approval_engine.request.rejected", resource=approval_request)
    dispatch("approval.rejected", {
        "approval_request_id": str(approval_request.public_id),
        "tenant_id":           approval_request.tenant_id,
    })


def _dispatch_returned(approval_request: ApprovalRequest) -> None:
    from simorgh.apps.events.bus import dispatch
    from simorgh.core.audit import record_service_event
    record_service_event("approval_engine.request.returned", resource=approval_request)
    dispatch("approval.returned", {
        "approval_request_id": str(approval_request.public_id),
        "tenant_id":           approval_request.tenant_id,
    })


# ── Approval Matrix ─────────────────────────────────────────────────────────

def create_approval_matrix(
    *,
    tenant_id: int,
    name: str,
    content_type_id: int,
    description: str = "",
    conditions: dict | None = None,
    is_active: bool = True,
    approver_configs: list[dict] | None = None,
) -> ApprovalMatrix:
    from simorgh.apps.approval_engine.models import ApprovalMatrix as Matrix, MatrixApproverConfig

    with transaction.atomic():
        matrix = Matrix.objects.create(
            tenant_id=tenant_id,
            name=name.strip(),
            description=description,
            content_type_id=content_type_id,
            conditions=conditions or {},
            is_active=is_active,
        )
        if approver_configs:
            for cfg in approver_configs:
                MatrixApproverConfig.objects.create(
                    matrix=matrix,
                    step_order=cfg.get("step_order", 1),
                    approver_id=cfg.get("approver_id"),
                    approver_role_id=cfg.get("approver_role_id"),
                    approver_org_unit_id=cfg.get("approver_org_unit_id"),
                    is_parallel=cfg.get("is_parallel", False),
                    is_required=cfg.get("is_required", True),
                    quorum=cfg.get("quorum", 1),
                    majority_voting=cfg.get("majority_voting", False),
                )
    return matrix


def update_approval_matrix(
    matrix: ApprovalMatrix,
    *,
    name: str | None = None,
    description: str | None = None,
    conditions: dict | None = None,
    is_active: bool | None = None,
) -> ApprovalMatrix:
    if name is not None:
        matrix.name = name.strip()
    if description is not None:
        matrix.description = description
    if conditions is not None:
        matrix.conditions = conditions
    if is_active is not None:
        matrix.is_active = is_active
    matrix.save()
    return matrix


def delete_approval_matrix(matrix: ApprovalMatrix) -> None:
    matrix.delete()
