import logging
from datetime import timedelta

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

from basic_info.models import Employee, EmployeeStatusEnum, EmployeeType
from mission.models import Mission, MissionCategory, MissionParticipant, MissionReply
from mission.signals import (
    mission_cancelled,
    mission_completed,
    mission_created,
    mission_reopened,
    mission_replied,
    mission_reply_rejected,
)

logger = logging.getLogger(__name__)

DEFAULT_MISSION_CATEGORY_NAME = "通用"
UNSET = object()


def _send_signal_on_commit(signal, *, sender, **payload) -> None:
    def _send():
        try:
            signal.send(sender=sender, **payload)
        except Exception:
            logger.exception("[mission.services] 触发 mission signal 失败（已忽略）")

    transaction.on_commit(_send)


def _assert_employee_belongs_to_mission(employee, mission: Mission, role: str) -> None:
    if employee is None:
        raise ValueError(f"{role}不能为空")
    if employee.merchant_id != mission.merchant_id:
        raise ValueError(f"{role}不属于任务所属商户")


def _get_default_mission_category(*, merchant) -> MissionCategory:
    category, _ = MissionCategory.objects.get_or_create(
        merchant=merchant,
        name=DEFAULT_MISSION_CATEGORY_NAME,
    )
    return category


def _assert_category_belongs_to_merchant(category: MissionCategory | None, *, merchant) -> None:
    if category is None:
        return
    if category.merchant_id != merchant.id:
        raise ValueError("任务分类不属于当前商户")


def _validate_content_object_merchant(*, content_type, content_id, merchant) -> None:
    if content_type is None and content_id is None:
        return
    if content_type is None or content_id is None:
        raise ValueError("content_type 与 content_id 必须同时提供或同时为空")

    try:
        content_object = content_type.get_object_for_this_type(pk=content_id)
    except Exception as exc:
        raise ValueError("关联业务对象不存在") from exc

    content_object_merchant_id = getattr(content_object, "merchant_id", None)
    if content_object_merchant_id is not None and content_object_merchant_id != merchant.id:
        raise ValueError("关联业务对象不属于当前商户")


def _validate_unreplied_notification_config(
    *,
    notify_if_unreplied: bool,
    unreplied_notify_interval_minutes: int | None,
    unreplied_notify_max_count: int,
) -> None:
    if notify_if_unreplied and unreplied_notify_interval_minutes is None:
        raise ValueError("开启未回复提醒时必须设置提醒间隔")
    if unreplied_notify_interval_minutes is not None and unreplied_notify_interval_minutes <= 0:
        raise ValueError("未回复提醒间隔必须大于 0")
    if unreplied_notify_max_count <= 0:
        raise ValueError("未回复最大提醒次数必须大于 0")


def _reset_unreplied_notification_state(mission: Mission) -> None:
    mission.unreplied_notify_sent_count = 0
    mission.unreplied_last_notified_at = None


def _has_effective_reply(*, mission: Mission) -> bool:
    return mission.replies.filter(is_rejected=False).exists()


def _clear_unreplied_notification_state_if_active(*, mission: Mission) -> None:
    if mission.unreplied_notify_sent_count or mission.unreplied_last_notified_at is not None:
        _reset_unreplied_notification_state(mission)
        mission.save(
            update_fields=[
                "unreplied_notify_sent_count",
                "unreplied_last_notified_at",
                "updated_at",
            ]
        )


@transaction.atomic
def set_mission_participants(
    *,
    mission: Mission,
    participant_ids: list[int] | None = None,
    employee_type_ids: list[int] | None = None,
) -> Mission:
    mission = Mission.objects.select_for_update().get(pk=mission.pk)
    participant_ids = list(dict.fromkeys(participant_ids or []))
    employee_type_ids = list(dict.fromkeys(employee_type_ids or []))

    if employee_type_ids:
        employee_type_count = EmployeeType.objects.filter(
            id__in=employee_type_ids,
            merchant=mission.merchant,
        ).count()
        if employee_type_count != len(employee_type_ids):
            raise ValueError("员工职位不存在或不属于任务所属商户")

        position_employee_ids = Employee.objects.filter(
            merchant=mission.merchant,
            position_id__in=employee_type_ids,
            status=EmployeeStatusEnum.ACTIVE,
        ).values_list("id", flat=True)
        participant_ids = list(dict.fromkeys([*participant_ids, *position_employee_ids]))

    employees = list(Employee.objects.filter(id__in=participant_ids, merchant=mission.merchant))
    if len(employees) != len(participant_ids):
        raise ValueError("参与者不存在或不属于任务所属商户")

    MissionParticipant.objects.filter(mission=mission).exclude(employee_id__in=participant_ids).delete()
    existing_employee_ids = set(
        MissionParticipant.objects.filter(mission=mission, employee_id__in=participant_ids)
        .values_list("employee_id", flat=True)
    )
    MissionParticipant.objects.bulk_create(
        [
            MissionParticipant(merchant=mission.merchant, mission=mission, employee=employee)
            for employee in employees
            if employee.id not in existing_employee_ids
        ]
    )
    return mission


@transaction.atomic
def create_mission(
    *,
    creator,
    description: str,
    category: MissionCategory | None = None,
    content_type=None,
    content_id: int | None = None,
    participant_ids: list[int] | None = None,
    employee_type_ids: list[int] | None = None,
    notify_if_unreplied: bool = False,
    unreplied_notify_interval_minutes: int | None = None,
    unreplied_notify_max_count: int = 5,
    extra=None,
) -> Mission:
    if creator is None:
        raise ValueError("任务创建者不能为空")
    merchant = creator.merchant
    _validate_content_object_merchant(content_type=content_type, content_id=content_id, merchant=merchant)
    _assert_category_belongs_to_merchant(category, merchant=merchant)
    _validate_unreplied_notification_config(
        notify_if_unreplied=notify_if_unreplied,
        unreplied_notify_interval_minutes=unreplied_notify_interval_minutes,
        unreplied_notify_max_count=unreplied_notify_max_count,
    )

    mission = Mission.objects.create(
        merchant=merchant,
        creator=creator,
        description=description,
        category=category or _get_default_mission_category(merchant=merchant),
        content_type=content_type,
        content_id=content_id,
        extra=extra,
        notify_if_unreplied=notify_if_unreplied,
        unreplied_notify_interval_minutes=unreplied_notify_interval_minutes,
        unreplied_notify_max_count=unreplied_notify_max_count,
    )
    if participant_ids is not None or employee_type_ids is not None:
        set_mission_participants(
            mission=mission,
            participant_ids=participant_ids,
            employee_type_ids=employee_type_ids,
        )
    _send_signal_on_commit(
        mission_created,
        sender=Mission,
        instance=mission,
        created_by=creator,
    )
    return mission


@transaction.atomic
def update_mission(
    *,
    mission: Mission,
    updated_by,
    description: str | None = None,
    category: MissionCategory | None = None,
    content_type=None,
    content_id: int | None = None,
    update_content_object: bool = False,
    participant_ids: list[int] | None = None,
    employee_type_ids: list[int] | None = None,
    notify_if_unreplied=UNSET,
    unreplied_notify_interval_minutes=UNSET,
    unreplied_notify_max_count=UNSET,
    extra=UNSET,
) -> Mission:
    mission = Mission.objects.select_for_update().get(pk=mission.pk)
    _assert_employee_belongs_to_mission(updated_by, mission, "更新任务的员工")

    update_fields = []
    if description is not None:
        mission.description = description
        update_fields.append("description")
    if category is not None:
        _assert_category_belongs_to_merchant(category, merchant=mission.merchant)
        mission.category = category
        update_fields.append("category")
    if update_content_object:
        _validate_content_object_merchant(
            content_type=content_type,
            content_id=content_id,
            merchant=mission.merchant,
        )
        mission.content_type = content_type
        mission.content_id = content_id
        update_fields.extend(["content_type", "content_id"])
    if extra is not UNSET:
        mission.extra = extra
        update_fields.append("extra")

    notify_changed = notify_if_unreplied is not UNSET
    interval_changed = unreplied_notify_interval_minutes is not UNSET
    max_count_changed = unreplied_notify_max_count is not UNSET
    if notify_changed or interval_changed or max_count_changed:
        final_notify_if_unreplied = (
            notify_if_unreplied
            if notify_changed
            else mission.notify_if_unreplied
        )
        final_interval = (
            unreplied_notify_interval_minutes
            if interval_changed
            else mission.unreplied_notify_interval_minutes
        )
        final_max_count = (
            unreplied_notify_max_count
            if max_count_changed
            else mission.unreplied_notify_max_count
        )
        _validate_unreplied_notification_config(
            notify_if_unreplied=final_notify_if_unreplied,
            unreplied_notify_interval_minutes=final_interval,
            unreplied_notify_max_count=final_max_count,
        )
        if notify_changed:
            mission.notify_if_unreplied = final_notify_if_unreplied
            update_fields.append("notify_if_unreplied")
        if interval_changed:
            mission.unreplied_notify_interval_minutes = final_interval
            update_fields.append("unreplied_notify_interval_minutes")
        if max_count_changed:
            mission.unreplied_notify_max_count = final_max_count
            update_fields.append("unreplied_notify_max_count")

        if not final_notify_if_unreplied:
            _reset_unreplied_notification_state(mission)
            update_fields.extend([
                "unreplied_notify_sent_count",
                "unreplied_last_notified_at",
            ])
        elif notify_changed or interval_changed:
            _reset_unreplied_notification_state(mission)
            update_fields.extend([
                "unreplied_notify_sent_count",
                "unreplied_last_notified_at",
            ])

    if update_fields:
        mission.save(update_fields=[*dict.fromkeys(update_fields), "updated_at"])
    if participant_ids is not None or employee_type_ids is not None:
        set_mission_participants(
            mission=mission,
            participant_ids=participant_ids,
            employee_type_ids=employee_type_ids,
        )
    return mission


@transaction.atomic
def create_mission_reply(
    *,
    mission: Mission,
    responder,
    content: str,
    ends_task: bool = False,
    extra=None,
) -> MissionReply:
    mission = Mission.objects.select_for_update().get(pk=mission.pk)
    _assert_employee_belongs_to_mission(responder, mission, "回应者")
    if not mission.can_reply:
        raise ValueError("任务当前不允许继续回应")

    reply = MissionReply.objects.create(
        merchant=mission.merchant,
        mission=mission,
        responder=responder,
        content=content,
        ends_task=ends_task,
        extra=extra,
    )
    if ends_task and not mission.is_completed:
        mission.is_completed = True
        _reset_unreplied_notification_state(mission)
        mission.save(
            update_fields=[
                "is_completed",
                "unreplied_notify_sent_count",
                "unreplied_last_notified_at",
                "updated_at",
            ]
        )
        _send_signal_on_commit(
            mission_completed,
            sender=Mission,
            instance=mission,
            completed_by=responder,
            reply=reply,
        )
    else:
        _clear_unreplied_notification_state_if_active(mission=mission)
    _send_signal_on_commit(
        mission_replied,
        sender=MissionReply,
        instance=reply,
        mission=mission,
        responder=responder,
    )
    return reply


@transaction.atomic
def reopen_mission(*, mission: Mission, reopened_by) -> Mission:
    mission = Mission.objects.select_for_update().get(pk=mission.pk)
    _assert_employee_belongs_to_mission(reopened_by, mission, "重新打开任务的员工")
    ending_replies = mission.replies.select_for_update().filter(ends_task=True, is_rejected=False)
    if mission.is_cancelled:
        raise ValueError("已取消任务不能重新打开")
    if not mission.is_completed:
        raise ValueError("未完成任务不能重新打开")
    if not ending_replies.exists():
        raise ValueError("任务没有可撤销的结束回应")

    rejected_reply_ids = list(ending_replies.values_list("id", flat=True))
    now = timezone.now()
    ending_replies.update(ends_task=False, is_rejected=True, rejected_by=reopened_by, rejected_at=now)
    mission.is_completed = False
    if mission.notify_if_unreplied and not _has_effective_reply(mission=mission):
        _reset_unreplied_notification_state(mission)
        mission.save(
            update_fields=[
                "is_completed",
                "unreplied_notify_sent_count",
                "unreplied_last_notified_at",
                "updated_at",
            ]
        )
    else:
        mission.save(update_fields=["is_completed", "updated_at"])
    for reply in MissionReply.objects.filter(id__in=rejected_reply_ids):
        _send_signal_on_commit(
            mission_reply_rejected,
            sender=MissionReply,
            instance=reply,
            mission=mission,
            rejected_by=reopened_by,
            reason="reopen",
        )
    _send_signal_on_commit(
        mission_reopened,
        sender=Mission,
        instance=mission,
        reopened_by=reopened_by,
        rejected_reply_ids=rejected_reply_ids,
    )
    return mission


@transaction.atomic
def reject_reply(*, reply: MissionReply, rejected_by) -> MissionReply:
    reply = MissionReply.objects.select_for_update().select_related("mission").get(pk=reply.pk)
    mission = Mission.objects.select_for_update().get(pk=reply.mission_id)
    _assert_employee_belongs_to_mission(rejected_by, mission, "撤销回应的员工")

    if mission.is_cancelled:
        raise ValueError("已取消任务不能撤销回应")
    if reply.is_rejected:
        raise ValueError("回应已被撤销")

    was_ending_reply = reply.ends_task
    now = timezone.now()
    reply.ends_task = False
    reply.is_rejected = True
    reply.rejected_by = rejected_by
    reply.rejected_at = now
    reply.save(update_fields=["ends_task", "is_rejected", "rejected_by", "rejected_at", "updated_at"])

    has_other_ending_reply = mission.replies.exclude(pk=reply.pk).filter(ends_task=True, is_rejected=False).exists()
    has_effective_reply = mission.replies.exclude(pk=reply.pk).filter(is_rejected=False).exists()
    if was_ending_reply and not has_other_ending_reply and mission.is_completed:
        mission.is_completed = False
        if mission.notify_if_unreplied and not has_effective_reply:
            _reset_unreplied_notification_state(mission)
            mission.save(
                update_fields=[
                    "is_completed",
                    "unreplied_notify_sent_count",
                    "unreplied_last_notified_at",
                    "updated_at",
                ]
            )
        else:
            mission.save(update_fields=["is_completed", "updated_at"])
    elif mission.notify_if_unreplied and not has_effective_reply:
        _clear_unreplied_notification_state_if_active(mission=mission)
    _send_signal_on_commit(
        mission_reply_rejected,
        sender=MissionReply,
        instance=reply,
        mission=mission,
        rejected_by=rejected_by,
        reason="reject_reply",
    )
    return reply


@transaction.atomic
def cancel_mission(*, mission: Mission, cancelled_by) -> Mission:
    mission = Mission.objects.select_for_update().get(pk=mission.pk)
    _assert_employee_belongs_to_mission(cancelled_by, mission, "取消任务的员工")

    if mission.is_cancelled:
        raise ValueError("任务已取消")

    mission.is_cancelled = True
    mission.cancelled_by = cancelled_by
    mission.cancelled_at = timezone.now()
    _reset_unreplied_notification_state(mission)
    mission.save(
        update_fields=[
            "is_cancelled",
            "cancelled_by",
            "cancelled_at",
            "unreplied_notify_sent_count",
            "unreplied_last_notified_at",
            "updated_at",
        ]
    )
    _send_signal_on_commit(
        mission_cancelled,
        sender=Mission,
        instance=mission,
        cancelled_by=cancelled_by,
    )
    return mission


@transaction.atomic
def set_mission_urgent(*, mission: Mission, updated_by, is_urgent: bool) -> Mission:
    mission = Mission.objects.select_for_update().get(pk=mission.pk)
    _assert_employee_belongs_to_mission(updated_by, mission, "更新任务紧急状态的员工")

    mission.is_urgent = is_urgent
    mission.save(update_fields=["is_urgent", "updated_at"])
    return mission


def get_mission_unreplied_due_at(*, mission: Mission):
    if mission.unreplied_notify_interval_minutes is None:
        return None
    base_time = mission.unreplied_last_notified_at or mission.created_at
    return base_time + timedelta(minutes=mission.unreplied_notify_interval_minutes)
