"""Helpdesk service layer — all business logic lives here.

Rules
-----
* Views MUST NOT implement business logic; they call services.
* Services MUST NOT directly render HTTP responses.
* Services emit domain events via the event bus, never Django signals.
* SLA timers are always started / paused / resolved through ``TicketService``.
* Workflow automations are executed via the platform ``ProcessEngine``.
"""

from __future__ import annotations

from datetime import timedelta
from typing import Any

import structlog
from django.db import transaction
from django.utils import timezone

_log = structlog.get_logger("simorgh.helpdesk")

# ---------------------------------------------------------------------------
# TicketService
# ---------------------------------------------------------------------------


class TicketService:
    """Central service for Ticket lifecycle operations."""

    # ------------------------------------------------------------------
    # Creation
    # ------------------------------------------------------------------

    @staticmethod
    @transaction.atomic
    def create(
        *,
        tenant_id: int,
        organization_node_id: int,
        queue_id: int,
        subject: str,
        description: str = "",
        priority: str = "normal",
        channel: str = "web",
        category_id: int | None = None,
        requester_id: int | None = None,
        requester_email: str = "",
        requester_name: str = "",
        created_by_id: int | None = None,
        custom_fields: dict | None = None,
        tag_ids: list[int] | None = None,
        actor=None,
    ) -> Ticket:  # noqa: F821
        from simorgh.apps.app_settings.services import resolve as resolve_setting
        from simorgh.apps.events.bus import dispatch
        from simorgh.apps.helpdesk.models import Ticket, TicketStatus, TicketTag
        from simorgh.apps.platform_core.services import (
            PlatformCoreError,
            get_next_reference,
            record_activity,
        )

        # Quota check: helpdesk.max_tickets (0 = unlimited)
        max_tickets = resolve_setting("helpdesk.max_tickets", tenant_id=tenant_id)
        if max_tickets and max_tickets > 0:
            current_count = Ticket.objects.filter(
                tenant_id=tenant_id,
                is_deleted=False,
            ).exclude(status__in=(TicketStatus.SOLVED, TicketStatus.CLOSED)).count()
            if current_count >= max_tickets:
                _log.warning(
                    "helpdesk.services.create.quota_exceeded",
                    tenant_id=tenant_id,
                    current_count=current_count,
                    max_tickets=max_tickets,
                )
                raise ValueError(
                    f"Ticket quota reached ({current_count}/{max_tickets}). "
                    "Contact your administrator to increase the limit."
                )

        ticket = Ticket.objects.create(
            tenant_id=tenant_id,
            organization_node_id=organization_node_id,
            queue_id=queue_id,
            subject=subject,
            description=description,
            priority=priority,
            channel=channel,
            category_id=category_id,
            requester_id=requester_id,
            requester_email=requester_email,
            requester_name=requester_name,
            created_by_id=created_by_id,
            custom_fields=custom_fields or {},
        )

        # Auto-assign reference number if a sequence is configured.
        try:
            ref = get_next_reference(tenant_id, "helpdesk.ticket")
            ticket.reference_number = ref
            ticket.save(update_fields=["reference_number"])
        except PlatformCoreError:
            pass  # Silently skip if no sequence is configured

        if tag_ids:
            TicketTag.objects.bulk_create([
                TicketTag(ticket=ticket, tag_id=tid, added_by_id=created_by_id)
                for tid in tag_ids
            ], ignore_conflicts=True)

        # Inherit SLA from queue → policy.
        TicketService._assign_sla(ticket)

        record_activity(
            verb="created",
            entity=ticket,
            actor=actor,
            extra={"subject": subject, "priority": priority, "queue_id": queue_id},
        )

        dispatch("helpdesk.ticket.created", {
            "ticket_id": ticket.pk,
            "tenant_id": tenant_id,
            "org_node_id": organization_node_id,
            "queue_id": queue_id,
            "created_by": created_by_id,
        })

        _log.info("helpdesk.ticket.created", ticket_id=ticket.pk, tenant_id=tenant_id)
        return ticket

    # ------------------------------------------------------------------
    # Update
    # ------------------------------------------------------------------

    @staticmethod
    @transaction.atomic
    def update(
        ticket: Ticket,  # noqa: F821
        *,
        actor_id: int | None = None,
        actor=None,
        **fields: Any,
    ) -> Ticket:  # noqa: F821
        from simorgh.apps.events.bus import dispatch
        from simorgh.apps.platform_core.services import record_activity

        allowed = {
            "subject", "description", "priority", "category_id",
            "queue_id", "custom_fields",
        }
        changed: dict[str, Any] = {}
        for key, value in fields.items():
            if key not in allowed:
                continue
            if getattr(ticket, key) != value:
                changed[key] = value
                setattr(ticket, key, value)

        if changed:
            ticket.save(update_fields=[*list(changed.keys()), "updated_at"])
            record_activity(
                verb="updated",
                entity=ticket,
                actor=actor,
                extra={"changed_fields": list(changed.keys())},
            )
            dispatch("helpdesk.ticket.updated", {
                "ticket_id": ticket.pk,
                "tenant_id": ticket.tenant_id,
                "changed_fields": list(changed.keys()),
                "actor_id": actor_id,
            })

        return ticket

    # ------------------------------------------------------------------
    # Status transition
    # ------------------------------------------------------------------

    @staticmethod
    @transaction.atomic
    def transition(
        ticket: Ticket,  # noqa: F821
        *,
        new_status: str,
        actor_id: int | None = None,
        actor=None,
        note: str = "",
    ) -> Ticket:  # noqa: F821
        from simorgh.apps.events.bus import dispatch
        from simorgh.apps.helpdesk.models import TicketStatus
        from simorgh.apps.platform_core.services import record_activity

        old_status = ticket.status
        if old_status == new_status:
            return ticket

        now = timezone.now()
        update_fields = ["status", "updated_at"]

        if new_status in (TicketStatus.SOLVED, TicketStatus.CLOSED):
            ticket.resolved_at = ticket.resolved_at or now
            update_fields.append("resolved_at")
            if new_status == TicketStatus.CLOSED:
                ticket.closed_at = now
                update_fields.append("closed_at")

        ticket.status = new_status
        ticket.save(update_fields=update_fields)

        record_activity(
            verb="status_changed",
            entity=ticket,
            actor=actor,
            extra={"from_status": old_status, "to_status": new_status, "note": note},
        )

        dispatch("helpdesk.ticket.status_changed", {
            "ticket_id": ticket.pk,
            "tenant_id": ticket.tenant_id,
            "from_status": old_status,
            "to_status": new_status,
            "actor_id": actor_id,
        })

        if new_status in (TicketStatus.SOLVED, TicketStatus.CLOSED):
            dispatch("helpdesk.ticket.solved", {
                "ticket_id": ticket.pk,
                "tenant_id": ticket.tenant_id,
                "resolved_at": (ticket.resolved_at or now).isoformat(),
                "actor_id": actor_id,
            })

        _log.info(
            "helpdesk.ticket.status_changed",
            ticket_id=ticket.pk,
            from_status=old_status,
            to_status=new_status,
        )
        return ticket

    # ------------------------------------------------------------------
    # Assignment
    # ------------------------------------------------------------------

    @staticmethod
    @transaction.atomic
    def assign(
        ticket: Ticket,  # noqa: F821
        *,
        assigned_to_id: int | None,
        assigned_by_id: int | None = None,
        actor=None,
    ) -> Ticket:  # noqa: F821
        from simorgh.apps.events.bus import dispatch
        from simorgh.apps.platform_core.services import record_activity

        ticket.assigned_to_id = assigned_to_id
        ticket.assigned_at = timezone.now() if assigned_to_id else None
        ticket.save(update_fields=["assigned_to_id", "assigned_at", "updated_at"])

        record_activity(
            verb="assigned",
            entity=ticket,
            actor=actor,
            extra={"assigned_to_id": assigned_to_id, "assigned_by_id": assigned_by_id},
        )

        dispatch("helpdesk.ticket.assigned", {
            "ticket_id": ticket.pk,
            "tenant_id": ticket.tenant_id,
            "assigned_to_id": assigned_to_id,
            "assigned_by_id": assigned_by_id,
        })
        return ticket

    # ------------------------------------------------------------------
    # Escalation
    # ------------------------------------------------------------------

    @staticmethod
    @transaction.atomic
    def escalate(
        ticket: Ticket,  # noqa: F821
        *,
        escalated_to_id: int,
        reason: str = "",
        actor_id: int | None = None,
        actor=None,
    ) -> Ticket:  # noqa: F821
        from simorgh.apps.events.bus import dispatch
        from simorgh.apps.platform_core.services import record_activity

        ticket.escalated_to_id = escalated_to_id
        ticket.escalated_at = timezone.now()
        ticket.escalation_reason = reason
        ticket.save(update_fields=[
            "escalated_to_id", "escalated_at", "escalation_reason", "updated_at",
        ])

        record_activity(
            verb="escalated",
            entity=ticket,
            actor=actor,
            extra={"escalated_to_id": escalated_to_id, "reason": reason},
        )

        dispatch("helpdesk.ticket.escalated", {
            "ticket_id": ticket.pk,
            "tenant_id": ticket.tenant_id,
            "escalated_to_id": escalated_to_id,
            "reason": reason,
            "actor_id": actor_id,
        })
        return ticket

    # ------------------------------------------------------------------
    # Reply / internal note
    # ------------------------------------------------------------------

    @staticmethod
    @transaction.atomic
    def add_reply(
        ticket: Ticket,  # noqa: F821
        *,
        body: str,
        is_internal: bool = False,
        author_id: int | None = None,
        author_email: str = "",
        author_name: str = "",
        channel: str = "web",
        actor=None,
    ) -> TicketReply:  # noqa: F821
        from simorgh.apps.events.bus import dispatch
        from simorgh.apps.helpdesk.models import TicketReply
        from simorgh.apps.platform_core.services import record_activity

        reply = TicketReply.objects.create(
            ticket=ticket,
            body=body,
            is_internal=is_internal,
            author_id=author_id,
            author_email=author_email,
            author_name=author_name,
            channel=channel,
        )

        record_activity(
            verb="note_added" if is_internal else "replied",
            entity=ticket,
            actor=actor,
            extra={"reply_id": reply.pk, "is_internal": is_internal},
        )

        # Move ticket to open if it was new/pending (requester replied).
        if not is_internal and ticket.status in ("new", "pending", "on_hold"):
            TicketService.transition(ticket, new_status="open", actor_id=author_id, actor=actor)

        dispatch("helpdesk.reply.added", {
            "ticket_id": ticket.pk,
            "reply_id": reply.pk,
            "tenant_id": ticket.tenant_id,
            "author_id": author_id,
            "is_internal": is_internal,
        })

        return reply

    # ------------------------------------------------------------------
    # SLA helpers
    # ------------------------------------------------------------------

    @staticmethod
    def _assign_sla(ticket: Ticket) -> None:  # noqa: F821
        """Attach the queue's default SLA policy to the ticket (if any)."""
        if ticket.sla_policy_id:
            return
        queue = ticket.queue
        if queue.default_sla_policy_id:
            ticket.sla_policy_id = queue.default_sla_policy_id
            ticket.save(update_fields=["sla_policy_id"])

    @staticmethod
    def start_sla_timers(*, ticket_id: int) -> None:
        from simorgh.apps.helpdesk.models import SLABreachType, SLATimer, Ticket

        try:
            ticket = Ticket.objects.select_related("sla_policy").get(pk=ticket_id)
        except Ticket.DoesNotExist:
            return

        policy = ticket.sla_policy
        if not policy:
            return

        now = timezone.now()
        first_due = now + timedelta(hours=policy.first_response_hours)
        res_due = now + timedelta(hours=policy.resolution_hours)

        ticket.first_response_due_at = first_due
        ticket.resolution_due_at = res_due
        ticket.save(update_fields=["first_response_due_at", "resolution_due_at"])

        SLATimer.objects.get_or_create(
            ticket_id=ticket_id,
            breach_type=SLABreachType.FIRST_RESPONSE,
            defaults={"policy": policy, "due_at": first_due},
        )
        SLATimer.objects.get_or_create(
            ticket_id=ticket_id,
            breach_type=SLABreachType.RESOLUTION,
            defaults={"policy": policy, "due_at": res_due},
        )

    @staticmethod
    def mark_first_response(*, ticket_id: int) -> None:
        from simorgh.apps.helpdesk.models import SLABreachType, SLATimer, Ticket

        now = timezone.now()
        SLATimer.objects.filter(
            ticket_id=ticket_id,
            breach_type=SLABreachType.FIRST_RESPONSE,
            is_resolved=False,
        ).update(is_resolved=True, resolved_at=now)

        Ticket.objects.filter(pk=ticket_id, first_responded_at__isnull=True).update(
            first_responded_at=now
        )

    @staticmethod
    def handle_sla_status_change(*, ticket_id: int, new_status: str) -> None:
        """Pause SLA timers when ticket is on_hold; resume when re-opened."""
        from simorgh.apps.helpdesk.models import SLATimer

        now = timezone.now()
        if new_status == "on_hold":
            SLATimer.objects.filter(
                ticket_id=ticket_id, is_paused=False, is_resolved=False
            ).update(is_paused=True, paused_at=now)
        elif new_status == "open":
            # Resume: add elapsed pause seconds to the timer.
            timers = SLATimer.objects.filter(
                ticket_id=ticket_id, is_paused=True, is_resolved=False
            )
            for timer in timers:
                if timer.paused_at:
                    elapsed = int((now - timer.paused_at).total_seconds())
                    timer.paused_seconds = (timer.paused_seconds or 0) + elapsed
                    timer.due_at = timer.due_at + timedelta(seconds=elapsed)
                    timer.is_paused = False
                    timer.paused_at = None
                    timer.save(update_fields=[
                        "paused_seconds", "due_at", "is_paused", "paused_at"
                    ])


# ---------------------------------------------------------------------------
# QueueService
# ---------------------------------------------------------------------------


class QueueService:
    @staticmethod
    @transaction.atomic
    def create(
        *,
        tenant_id: int,
        organization_node_id: int,
        name: str,
        description: str = "",
        email_address: str = "",
        is_default: bool = False,
        default_sla_policy_id: int | None = None,
        created_by_id: int | None = None,
    ) -> Queue:  # noqa: F821
        from simorgh.apps.helpdesk.models import Queue

        if is_default:
            Queue.objects.filter(tenant_id=tenant_id, is_default=True).update(is_default=False)

        return Queue.objects.create(
            tenant_id=tenant_id,
            organization_node_id=organization_node_id,
            name=name,
            description=description,
            email_address=email_address,
            is_default=is_default,
            default_sla_policy_id=default_sla_policy_id,
            created_by_id=created_by_id,
        )
