"""工单业务：创建 / 升级 / 客服处理 / 提现拦截 / 列表范围。"""
from __future__ import annotations

import logging
import random
import time

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

from gongdan.constants import (
    TYPE_ORDER, TYPE_RECHARGE, TYPE_PENALTY, TYPE_GUANSHI, TYPE_FRAUD, TYPE_ZUZHANG,
    TYPE_LABELS, STATUS_PROCESSING, STATUS_DONE, STATUS_DISSATISFIED, STATUS_CLOSED,
    STATUS_NEGOTIATING, STATUS_LABELS, OPEN_STATUSES, MAX_IMAGES, MAX_SHUOMING_LEN,
    MAX_SHENGJI_TIMES, TARGET_ROLE_GUANSHI, TARGET_ROLE_ZUZHANG, TARGET_ROLE_LABELS,
    TARGET_ROLE_DASHOU, TARGET_ROLE_PLATFORM,
)
from gongdan.models import Gongdan, GongdanImage, GongdanShengji, ClubGongdanConfig

logger = logging.getLogger(__name__)


class GongdanError(Exception):
    def __init__(self, message, code='error'):
        super().__init__(message)
        self.code = code


def _gen_gongdan_id():
    ts = int(time.time() * 1000)
    ns = int(time.time_ns() // 1000) % 1000000
    rnd = random.randint(0, 99)
    return f'GD{ts:011d}{ns:06d}{rnd:02d}'


def _trim(text, max_len=MAX_SHUOMING_LEN):
    return (text or '').strip()[:max_len]


def is_complaint_withdraw_block_enabled(club_id: str) -> bool:
    cid = (club_id or '').strip()
    if not cid:
        return False
    row = ClubGongdanConfig.query.filter(club_id=cid).first()
    return bool(row and row.complaint_withdraw_block)


def ensure_club_gongdan_config(club_id: str, *, withdraw_block: bool = False) -> ClubGongdanConfig:
    cid = (club_id or '').strip()
    row = ClubGongdanConfig.query.filter(club_id=cid).first()
    if not row:
        row = ClubGongdanConfig(club_id=cid, complaint_withdraw_block=bool(withdraw_block))
        row.save()
    return row


def get_club_gongdan_config(club_id: str) -> dict:
    cid = (club_id or '').strip()
    if not cid:
        return {
            'club_id': '',
            'complaint_withdraw_block': False,
            'human_kefu_wechat': '',
            'human_kefu_link': '',
            'human_kefu_tip': '',
            'human_kefu_available': False,
        }
    row = ClubGongdanConfig.query.filter(club_id=cid).first()
    wechat = ((row.human_kefu_wechat if row else '') or '').strip()
    link = ((row.human_kefu_link if row else '') or '').strip()
    tip = ((row.human_kefu_tip if row else '') or '').strip()
    return {
        'club_id': cid,
        'complaint_withdraw_block': bool(row and row.complaint_withdraw_block),
        'human_kefu_wechat': wechat,
        'human_kefu_link': link,
        'human_kefu_tip': tip or ('添加人工客服微信咨询' if wechat else ('点击联系人工客服' if link else '')),
        'human_kefu_available': bool(wechat or link),
    }


def set_complaint_withdraw_block(club_id: str, enabled: bool) -> dict:
    cid = (club_id or '').strip()
    if not cid:
        raise GongdanError('俱乐部不能为空', 'need_club')
    row = ensure_club_gongdan_config(cid, withdraw_block=bool(enabled))
    enabled = bool(enabled)
    if row.complaint_withdraw_block != enabled:
        row.complaint_withdraw_block = enabled
        row.save(update_fields=['complaint_withdraw_block', 'UpdateTime'])
    return get_club_gongdan_config(cid)


def update_club_gongdan_config(club_id: str, **fields) -> dict:
    """更新俱乐部工单配置（提现拦截 / 人工客服联系方式）。"""
    cid = (club_id or '').strip()
    if not cid:
        raise GongdanError('俱乐部不能为空', 'need_club')
    row = ensure_club_gongdan_config(cid)
    dirty = []
    if 'complaint_withdraw_block' in fields and fields['complaint_withdraw_block'] is not None:
        val = bool(fields['complaint_withdraw_block'])
        if row.complaint_withdraw_block != val:
            row.complaint_withdraw_block = val
            dirty.append('complaint_withdraw_block')
    if 'human_kefu_wechat' in fields and fields['human_kefu_wechat'] is not None:
        val = str(fields['human_kefu_wechat'] or '').strip()[:64]
        if (row.human_kefu_wechat or '') != val:
            row.human_kefu_wechat = val
            dirty.append('human_kefu_wechat')
    if 'human_kefu_link' in fields and fields['human_kefu_link'] is not None:
        val = str(fields['human_kefu_link'] or '').strip()[:512]
        if (row.human_kefu_link or '') != val:
            row.human_kefu_link = val
            dirty.append('human_kefu_link')
    if 'human_kefu_tip' in fields and fields['human_kefu_tip'] is not None:
        val = str(fields['human_kefu_tip'] or '').strip()[:120]
        if (row.human_kefu_tip or '') != val:
            row.human_kefu_tip = val
            dirty.append('human_kefu_tip')
    if dirty:
        dirty.append('UpdateTime')
        row.save(update_fields=dirty)
    return get_club_gongdan_config(cid)


def resolve_inviter_complaint_target(yonghuid: str, leixing: int) -> dict:
    """
    管事/组长投诉只能针对本人邀请人，禁止搜索乱选：
    - 管事投诉：打手的 yaoqingren，且对方须是管事
    - 组长投诉：管事的 yaoqingren，且对方须是组长
    无邀请人则不可投诉。
    """
    uid = (yonghuid or '').strip()
    if not uid:
        raise GongdanError('请先登录', 'need_login')

    from users.models import UserDashou, UserGuanshi, UserZuzhang

    if leixing == TYPE_GUANSHI:
        ds = UserDashou.query.filter(user__UserUID=uid).select_related('user').first()
        if not ds:
            raise GongdanError('仅打手可投诉自己的管事', 'need_dashou')
        inviter = (ds.yaoqingren or '').strip()
        if not inviter:
            raise GongdanError('你没有邀请人，无法投诉管事', 'no_inviter')
        gs = UserGuanshi.query.filter(user__UserUID=inviter, zhuangtai=1).select_related('user').first()
        if not gs:
            raise GongdanError('你的邀请人不是有效管事，无法投诉', 'inviter_not_guanshi')
        return {
            'role': TARGET_ROLE_GUANSHI,
            'role_label': TARGET_ROLE_LABELS[TARGET_ROLE_GUANSHI],
            'user_id': inviter,
            'nicheng': _user_nicheng(inviter),
            'can_complain': True,
        }

    if leixing == TYPE_ZUZHANG:
        gs = UserGuanshi.query.filter(user__UserUID=uid).select_related('user').first()
        if not gs:
            raise GongdanError('仅管事可投诉自己的组长', 'need_guanshi')
        inviter = (gs.yaoqingren or '').strip()
        if not inviter:
            raise GongdanError('你没有邀请人，无法投诉组长', 'no_inviter')
        zz = UserZuzhang.query.filter(user__UserUID=inviter, zhuangtai=1).select_related('user').first()
        if not zz:
            raise GongdanError('你的邀请人不是有效组长，无法投诉', 'inviter_not_zuzhang')
        return {
            'role': TARGET_ROLE_ZUZHANG,
            'role_label': TARGET_ROLE_LABELS[TARGET_ROLE_ZUZHANG],
            'user_id': inviter,
            'nicheng': _user_nicheng(inviter),
            'can_complain': True,
        }

    raise GongdanError('仅管事/组长投诉需要邀请人', 'bad_type')


def peek_inviter_complaint_target(yonghuid: str, leixing: int) -> dict:
    """前端展示用：失败时返回 can_complain=False + 原因，不抛错。"""
    try:
        return resolve_inviter_complaint_target(yonghuid, leixing)
    except GongdanError as e:
        role = TARGET_ROLE_GUANSHI if leixing == TYPE_GUANSHI else TARGET_ROLE_ZUZHANG
        return {
            'role': role,
            'role_label': TARGET_ROLE_LABELS.get(role, ''),
            'user_id': '',
            'nicheng': '',
            'can_complain': False,
            'reason': str(e),
        }


def _user_nicheng(uid: str) -> str:
    """展示昵称：打手 nicheng → 老板 nickname → 用户{UID}。不把 UID 当昵称展示。"""
    uid = (uid or '').strip()
    if not uid:
        return ''
    try:
        from users.models import UserDashou, UserBoss
        from users.business_models import User

        ds = UserDashou.query.filter(user__UserUID=uid).select_related('user').first()
        if ds and (ds.nicheng or '').strip():
            return (ds.nicheng or '').strip()[:32]
        boss = UserBoss.query.filter(user__UserUID=uid).select_related('user').first()
        if boss and (boss.nickname or '').strip():
            return (boss.nickname or '').strip()[:32]
        u = User.query.filter(UserUID=uid).first() or User.objects.filter(UserUID=uid).first()
        if u and (getattr(u, 'Phone', None) or '').strip():
            phone = str(u.Phone).strip()
            if len(phone) >= 4:
                return f'用户*{phone[-4:]}'
        return '用户'
    except Exception:
        return '用户'


def _resolve_target(leixing, guanshi_id=None, target_role=None, target_user_id=None, *, yonghuid=None):
    """归一化被投诉对象。管事/组长投诉强制本人邀请人，忽略前端传入的选人。"""
    role = (target_role or '').strip().lower()
    tid = (target_user_id or '').strip()
    gid = (guanshi_id or '').strip() or None

    if leixing == TYPE_GUANSHI:
        info = resolve_inviter_complaint_target(yonghuid, TYPE_GUANSHI)
        tid = info['user_id']
        return TARGET_ROLE_GUANSHI, tid, tid

    if leixing == TYPE_ZUZHANG:
        info = resolve_inviter_complaint_target(yonghuid, TYPE_ZUZHANG)
        tid = info['user_id']
        return TARGET_ROLE_ZUZHANG, tid, None

    if role and tid:
        if role not in TARGET_ROLE_LABELS and role not in ('order', 'recharge', 'penalty'):
            raise GongdanError('无效的被投诉角色', 'bad_target_role')
        return role, tid, gid if role == TARGET_ROLE_GUANSHI else None

    return role or '', tid or '', gid


def validate_type_refs(leixing, order_id=None, chongzhi_id=None, fadan_id=None, guanshi_id=None):
    order_id = (order_id or '').strip() or None
    chongzhi_id = (chongzhi_id or '').strip() or None
    fadan_id = (fadan_id or '').strip() or None
    guanshi_id = (guanshi_id or '').strip() or None

    if leixing == TYPE_ORDER:
        if not order_id:
            raise GongdanError('订单投诉请填写订单ID', 'need_order_id')
        from orders.models import Order
        if not Order.query.filter(OrderID=order_id).exists():
            raise GongdanError('订单不存在，请核对订单ID', 'order_not_found')
        return order_id, None, None, None

    if leixing == TYPE_RECHARGE:
        if not chongzhi_id:
            raise GongdanError('充值投诉请填写充值记录ID', 'need_chongzhi_id')
        from products.models import Czjilu
        from products.czjilu_types import CZJILU_PAID_STATUS
        cz = Czjilu.query.filter(dingdan_id=chongzhi_id).first()
        if not cz:
            raise GongdanError('充值记录不存在，请核对充值ID', 'chongzhi_not_found')
        try:
            z = int(cz.zhuangtai) if cz.zhuangtai is not None else None
        except (TypeError, ValueError):
            z = None
        if z != CZJILU_PAID_STATUS:
            raise GongdanError('仅已支付的充值单可投诉', 'chongzhi_not_paid')
        return None, chongzhi_id, None, None

    if leixing == TYPE_PENALTY:
        if not fadan_id:
            raise GongdanError('罚款投诉请填写罚单ID', 'need_fadan_id')
        from orders.models import Penalty
        try:
            fid = int(fadan_id)
        except (TypeError, ValueError):
            raise GongdanError('罚单ID格式不正确', 'bad_fadan_id')
        if not Penalty.query.filter(id=fid).exists():
            raise GongdanError('罚单不存在，请核对罚单ID', 'fadan_not_found')
        return None, None, str(fid), None

    if leixing == TYPE_GUANSHI:
        return None, None, None, guanshi_id

    if leixing in (TYPE_FRAUD, TYPE_ZUZHANG):
        return None, None, None, None

    raise GongdanError('不支持的投诉类型', 'bad_type')


def _effective_target_uid(gd) -> str:
    return (gd.target_user_id or gd.guanshi_id or '').strip()


def _effective_target_role(gd) -> str:
    role = (gd.target_role or '').strip()
    if role:
        return role
    if gd.leixing == TYPE_GUANSHI or gd.guanshi_id:
        return TARGET_ROLE_GUANSHI
    if gd.leixing == TYPE_ZUZHANG:
        return TARGET_ROLE_ZUZHANG
    return ''


def serialize_gongdan(gd, include_images=True, include_shengji=False, *, viewer_uid=None, for_admin=False):
    """
    序列化。默认脱敏：不把申请人/被投诉人/客服 UID 作为展示字段下发。
    for_admin=True 时仍不主动展示 UID 列，只多给内部审计需要的标记。
    contact_peer_uid 仅当 viewer 是当事方时返回，供 IM 打开（前端勿渲染）。
    """
    t_role = _effective_target_role(gd)
    t_uid = _effective_target_uid(gd)
    relation = ''
    if viewer_uid:
        if gd.shenqingren_id == viewer_uid:
            relation = 'created'
        elif t_uid and t_uid == viewer_uid:
            relation = 'received'
        else:
            relation = 'linked' if _viewer_is_linked_zuzhang(viewer_uid, gd) else ''

    data = {
        'gongdan_id': gd.gongdan_id,
        'leixing': gd.leixing,
        'leixing_label': TYPE_LABELS.get(gd.leixing, str(gd.leixing)),
        'order_id': gd.order_id or '',
        'chongzhi_id': gd.chongzhi_id or '',
        'fadan_id': gd.fadan_id or '',
        'shuoming': gd.shuoming or '',
        'contact_qq': getattr(gd, 'contact_qq', '') or '',
        'contact_wechat': getattr(gd, 'contact_wechat', '') or '',
        'contact_phone': getattr(gd, 'contact_phone', '') or '',
        'club_id': gd.club_id or '',
        'zhuangtai': gd.zhuangtai,
        'zhuangtai_label': STATUS_LABELS.get(gd.zhuangtai, str(gd.zhuangtai)),
        'chuli_jieguo': gd.chuli_jieguo or '',
        # 对外：只显示「平台已介入」，不返回客服 UID
        'platform_intervened': bool(gd.platform_intervened),
        'intervene_at': gd.intervene_at.isoformat() if gd.intervene_at else '',
        'intervene_reason': gd.intervene_reason or '',
        'chuli_time': gd.chuli_time.isoformat() if gd.chuli_time else '',
        'shengji_cishu': gd.shengji_cishu or 0,
        'CreateTime': gd.CreateTime.isoformat() if gd.CreateTime else '',
        'UpdateTime': gd.UpdateTime.isoformat() if gd.UpdateTime else '',
        'target_role': t_role,
        'target_role_label': TARGET_ROLE_LABELS.get(t_role, t_role or ''),
        'target_nicheng': _user_nicheng(t_uid),
        'complainant_nicheng': _user_nicheng(gd.shenqingren_id),
        'relation': relation,
        'is_open': gd.zhuangtai in OPEN_STATUSES,
        'can_contact': False,
        'contact_peer_uid': '',
        'contact_peer_nicheng': '',
        'can_contact_complainant': False,
        'contact_complainant_uid': '',
        'contact_complainant_nicheng': '',
    }
    if viewer_uid and relation in ('created', 'received'):
        peer = t_uid if relation == 'created' else gd.shenqingren_id
        if peer and peer != viewer_uid:
            data['can_contact'] = True
            data['contact_peer_uid'] = peer
            data['contact_peer_nicheng'] = _user_nicheng(peer)
    elif viewer_uid and relation == 'linked':
        # 牵连组长：默认可联系主被投诉管事；另可联系投诉人
        if t_uid and t_uid != viewer_uid:
            data['can_contact'] = True
            data['contact_peer_uid'] = t_uid
            data['contact_peer_nicheng'] = _user_nicheng(t_uid)
        if gd.shenqingren_id and gd.shenqingren_id != viewer_uid:
            data['can_contact_complainant'] = True
            data['contact_complainant_uid'] = gd.shenqingren_id
            data['contact_complainant_nicheng'] = _user_nicheng(gd.shenqingren_id)
    if for_admin:
        # 客服需要 UID 做内部处理；用户端绝不下发
        data['shenqingren_id'] = gd.shenqingren_id or ''
        data['target_user_id'] = t_uid or ''
        data['guanshi_id'] = gd.guanshi_id or ''
        data['chuliren_id'] = gd.chuliren_id or ''
        data['chuliren_name'] = gd.chuliren_name or ('平台客服' if gd.platform_intervened or gd.chuliren_id else '')
    else:
        data['chuliren_name'] = '平台客服' if (gd.platform_intervened or gd.chuliren_name) else ''

    if include_images:
        data['images'] = [
            {'url': img.url, 'paixu': img.paixu}
            for img in gd.images.all().order_by('paixu', 'id')
        ]
    if include_shengji:
        data['shengji_list'] = [
            {
                'cishu': s.cishu,
                'shuoming': s.shuoming,
                'CreateTime': s.CreateTime.isoformat() if s.CreateTime else '',
            }
            for s in gd.shengji_list.all().order_by('cishu', 'id')
        ]
    return data


def _viewer_is_linked_zuzhang(viewer_uid: str, gd) -> bool:
    """下级管事被投诉时，其上级组长视为牵连方。"""
    t_uid = _effective_target_uid(gd)
    t_role = _effective_target_role(gd)
    if not t_uid or t_role != TARGET_ROLE_GUANSHI:
        return False
    try:
        from users.models import UserGuanshi
        gs = UserGuanshi.objects.filter(user__UserUID=t_uid).first()
        if not gs:
            return False
        return (gs.yaoqingren or '').strip() == str(viewer_uid)
    except Exception:
        return False


def _sub_guanshi_uids(zuzhang_uid: str):
    from users.models import UserGuanshi
    rows = UserGuanshi.objects.filter(yaoqingren=str(zuzhang_uid)).select_related('user')
    out = []
    for r in rows:
        uid = getattr(getattr(r, 'user', None), 'UserUID', None)
        if uid:
            out.append(str(uid))
    return out


def open_complaint_q_for_target_uids(uids):
    """未结清且指向这些 UID 的工单条件。"""
    uids = [str(u) for u in (uids or []) if u]
    if not uids:
        return Q(pk__in=[])
    return (
        Q(zhuangtai__in=OPEN_STATUSES)
        & (Q(target_user_id__in=uids) | Q(guanshi_id__in=uids))
    )


def check_complaint_blocks_withdraw(user_main, club_id=None):
    """
    提现拦截。
    返回 (ok, msg, extra)
    extra: reason=received|linked, tab, count
    """
    try:
        from jituan.services.club_user import get_user_club_id
        cid = (club_id or get_user_club_id(user_main) or '').strip()
    except Exception:
        cid = (club_id or '').strip()

    if not is_complaint_withdraw_block_enabled(cid):
        return True, '', {}

    uid = str(user_main.UserUID)
    base = Gongdan.query.filter(club_id=cid) if cid else Gongdan.query.all()

    # 1) 自己是主被投诉人
    qs_recv = base.filter(open_complaint_q_for_target_uids([uid]))
    cnt = qs_recv.count()
    if cnt:
        return False, '你有未处理完的投诉，请处理后再提现', {
            'reason': 'received',
            'tab': 'received',
            'count': cnt,
            'code': 'COMPLAINT_BLOCK_WITHDRAW',
        }

    # 2) 自己是组长，下级管事被投诉 → 牵连
    try:
        from users.models import UserZuzhang
        is_zz = UserZuzhang.query.filter(user=user_main).exists()
    except Exception:
        is_zz = False
    if is_zz:
        subs = _sub_guanshi_uids(uid)
        if subs:
            qs_link = base.filter(open_complaint_q_for_target_uids(subs)).filter(
                Q(target_role=TARGET_ROLE_GUANSHI) | Q(target_role='') | Q(leixing=TYPE_GUANSHI)
            )
            cnt2 = qs_link.count()
            if cnt2:
                return False, '下级管事有未处理投诉，暂不可提现', {
                    'reason': 'linked',
                    'tab': 'linked',
                    'count': cnt2,
                    'code': 'COMPLAINT_BLOCK_WITHDRAW',
                }

    return True, '', {}


def search_complaint_targets(*, role, keyword='', club_id=None, page=1, page_size=20, yonghuid=None):
    """已废弃搜索选人：仅返回本人邀请人（防乱投诉）。keyword/club 忽略。"""
    role = (role or '').strip().lower()
    if role == TARGET_ROLE_GUANSHI:
        leixing = TYPE_GUANSHI
    elif role == TARGET_ROLE_ZUZHANG:
        leixing = TYPE_ZUZHANG
    else:
        raise GongdanError('role 须为 guanshi 或 zuzhang', 'bad_role')
    info = peek_inviter_complaint_target(yonghuid, leixing)
    rows = []
    if info.get('can_complain') and info.get('user_id'):
        rows.append({
            'user_id': info['user_id'],
            'nicheng': info.get('nicheng') or '',
            'role': info.get('role') or role,
            'role_label': info.get('role_label') or '',
        })
    return {
        'list': rows,
        'total': len(rows),
        'page': 1,
        'page_size': 1,
        'role': role,
        'can_complain': bool(info.get('can_complain')),
        'reason': info.get('reason') or '',
        'invite_only': True,
    }


def list_gongdan_for_user(yonghuid, *, scope='created', club_id=None, page=1, page_size=20):
    """
    scope:
      created  — 我发起的
      received — 我被投诉的
      linked   — 下级牵连（组长）
    """
    scope = (scope or 'created').strip().lower()
    page = max(int(page or 1), 1)
    page_size = min(max(int(page_size or 20), 1), 50)
    uid = str(yonghuid)
    qs = Gongdan.query.all()
    if club_id:
        qs = qs.filter(club_id=club_id)

    if scope == 'received':
        qs = qs.filter(Q(target_user_id=uid) | Q(guanshi_id=uid)).order_by('-CreateTime')
    elif scope == 'linked':
        subs = _sub_guanshi_uids(uid)
        if not subs:
            qs = qs.none()
        else:
            qs = qs.filter(Q(target_user_id__in=subs) | Q(guanshi_id__in=subs)).filter(
                Q(target_role=TARGET_ROLE_GUANSHI) | Q(target_role='') | Q(leixing=TYPE_GUANSHI)
            ).order_by('-CreateTime')
    else:
        qs = qs.filter(shenqingren_id=uid).order_by('-CreateTime')

    total = qs.count()
    start = (page - 1) * page_size
    rows = list(qs[start:start + page_size])
    return {
        'list': [serialize_gongdan(g, include_images=False, viewer_uid=uid) for g in rows],
        'total': total,
        'page': page,
        'page_size': page_size,
        'scope': scope,
    }


def user_can_view_gongdan(gd, yonghuid) -> bool:
    uid = str(yonghuid)
    if gd.shenqingren_id == uid:
        return True
    if _effective_target_uid(gd) == uid:
        return True
    if _viewer_is_linked_zuzhang(uid, gd):
        return True
    return False


def _replace_images(gd, image_urls):
    urls = [u.strip() for u in (image_urls or []) if u and str(u).strip()]
    if len(urls) > MAX_IMAGES:
        raise GongdanError(f'最多上传{MAX_IMAGES}张图片', 'too_many_images')
    GongdanImage.objects.filter(gongdan=gd).delete()
    for i, url in enumerate(urls):
        GongdanImage.objects.create(
            gongdan=gd,
            gongdan_hao=gd.gongdan_id,
            url=url[:512],
            paixu=i,
        )


@transaction.atomic
def create_gongdan(
    *,
    yonghuid,
    leixing,
    shuoming,
    club_id=None,
    order_id=None,
    chongzhi_id=None,
    fadan_id=None,
    guanshi_id=None,
    target_role=None,
    target_user_id=None,
    image_urls=None,
    contact_qq=None,
    contact_wechat=None,
    contact_phone=None,
):
    try:
        leixing = int(leixing)
    except (TypeError, ValueError):
        raise GongdanError('投诉类型无效', 'bad_type')
    if leixing not in TYPE_LABELS:
        raise GongdanError('投诉类型无效', 'bad_type')

    text = _trim(shuoming)
    if not text:
        raise GongdanError('请填写投诉说明', 'need_shuoming')

    order_id, chongzhi_id, fadan_id, guanshi_id = validate_type_refs(
        leixing, order_id, chongzhi_id, fadan_id, guanshi_id or target_user_id,
    )
    t_role, t_uid, guanshi_id = _resolve_target(
        leixing,
        guanshi_id=guanshi_id,
        target_role=target_role,
        target_user_id=target_user_id or guanshi_id,
        yonghuid=yonghuid,
    )

    gid = _gen_gongdan_id()
    for _ in range(5):
        if not Gongdan.query.filter(gongdan_id=gid).exists():
            break
        gid = _gen_gongdan_id()

    gd = Gongdan.objects.create(
        gongdan_id=gid,
        leixing=leixing,
        order_id=order_id,
        chongzhi_id=chongzhi_id,
        fadan_id=fadan_id,
        guanshi_id=guanshi_id,
        target_role=t_role or '',
        target_user_id=t_uid or '',
        shuoming=text,
        contact_qq=_trim(contact_qq or '', 32),
        contact_wechat=_trim(contact_wechat or '', 64),
        contact_phone=_trim(contact_phone or '', 20),
        shenqingren_id=yonghuid,
        club_id=club_id or None,
        zhuangtai=STATUS_PROCESSING,
        shengji_cishu=0,
    )
    _replace_images(gd, image_urls)
    logger.info('创建工单 %s type=%s user=%s target=%s/%s', gid, leixing, yonghuid, t_role, t_uid)
    return gd


@transaction.atomic
def escalate_gongdan(*, gongdan_id, yonghuid, shuoming, image_urls=None):
    gd = Gongdan.query.select_for_update().filter(gongdan_id=gongdan_id).first()
    if not gd:
        raise GongdanError('工单不存在', 'not_found')
    if gd.shenqingren_id != yonghuid:
        raise GongdanError('无权操作此工单', 'forbidden')
    if gd.zhuangtai not in (STATUS_DONE, STATUS_DISSATISFIED):
        raise GongdanError('当前状态不可继续投诉，请等待客服处理', 'bad_status')
    if (gd.shengji_cishu or 0) >= MAX_SHENGJI_TIMES:
        raise GongdanError(f'最多升级{MAX_SHENGJI_TIMES}次，请联系客服', 'too_many_shengji')

    text = _trim(shuoming)
    if not text:
        raise GongdanError('请填写补充说明', 'need_shuoming')

    next_cishu = (gd.shengji_cishu or 0) + 1
    GongdanShengji.objects.create(
        gongdan=gd,
        gongdan_hao=gd.gongdan_id,
        cishu=next_cishu,
        shuoming=text,
        shenqingren_id=yonghuid,
    )
    gd.shengji_cishu = next_cishu
    gd.zhuangtai = STATUS_PROCESSING
    gd.chuli_jieguo = ''
    gd.chuli_time = None
    gd.save(update_fields=[
        'shengji_cishu', 'zhuangtai', 'chuli_jieguo', 'chuli_time', 'UpdateTime',
    ])
    if image_urls is not None:
        _replace_images(gd, image_urls)
    return gd


@transaction.atomic
def handle_gongdan(
    *,
    gongdan_id,
    handler_uid,
    handler_name,
    action,
    chuli_jieguo='',
    intervene_reason='',
):
    """
    action:
      done/finish/resolve → 已处理
      close → 关闭
      processing/reopen → 处理中
      dissatisfied → 不满意
      intervene → 标记平台介入（不改结案状态）
      negotiate → 协商中（客服 sparingly）
    """
    gd = Gongdan.query.select_for_update().filter(gongdan_id=gongdan_id).first()
    if not gd:
        raise GongdanError('工单不存在', 'not_found')

    action = (action or '').strip().lower()
    jieguo = _trim(chuli_jieguo)

    if action in ('intervene', 'platform_intervene'):
        gd.platform_intervened = True
        gd.intervene_at = timezone.now()
        gd.intervene_by = (handler_uid or '')[:32]
        gd.intervene_reason = (intervene_reason or 'kefu')[:32]
        if gd.zhuangtai not in OPEN_STATUSES:
            gd.zhuangtai = STATUS_PROCESSING
        gd.save()
        logger.info('工单平台介入 %s by=%s', gongdan_id, handler_uid)
        return gd

    if action in ('done', 'finish', 'resolve'):
        if not jieguo:
            raise GongdanError('请填写处理结果', 'need_jieguo')
        gd.zhuangtai = STATUS_DONE
        gd.chuli_jieguo = jieguo
        gd.chuliren_id = handler_uid
        gd.chuliren_name = (handler_name or '平台客服')[:64]
        gd.chuli_time = timezone.now()
        if not gd.platform_intervened:
            gd.platform_intervened = True
            gd.intervene_at = gd.intervene_at or timezone.now()
            gd.intervene_by = (handler_uid or '')[:32]
            gd.intervene_reason = gd.intervene_reason or 'kefu_done'
    elif action in ('close',):
        gd.zhuangtai = STATUS_CLOSED
        gd.chuli_jieguo = jieguo or gd.chuli_jieguo or '已关闭'
        gd.chuliren_id = handler_uid
        gd.chuliren_name = (handler_name or '平台客服')[:64]
        gd.chuli_time = timezone.now()
    elif action in ('processing', 'reopen'):
        gd.zhuangtai = STATUS_PROCESSING
    elif action in ('negotiate',):
        gd.zhuangtai = STATUS_NEGOTIATING
    elif action in ('dissatisfied',):
        gd.zhuangtai = STATUS_DISSATISFIED
    else:
        raise GongdanError('未知处理动作', 'bad_action')

    gd.save()
    logger.info('工单处理 %s action=%s by=%s', gongdan_id, action, handler_uid)
    return gd


@transaction.atomic
def user_mark_dissatisfied(*, gongdan_id, yonghuid):
    gd = Gongdan.query.select_for_update().filter(gongdan_id=gongdan_id).first()
    if not gd:
        raise GongdanError('工单不存在', 'not_found')
    if gd.shenqingren_id != yonghuid:
        raise GongdanError('无权操作此工单', 'forbidden')
    if gd.zhuangtai != STATUS_DONE:
        raise GongdanError('仅已处理的工单可标记不满意', 'bad_status')
    gd.zhuangtai = STATUS_DISSATISFIED
    gd.save(update_fields=['zhuangtai', 'UpdateTime'])
    return gd


@transaction.atomic
def user_resolve_gongdan(*, gongdan_id, yonghuid):
    """投诉人确认已解决（私了结清）。"""
    gd = Gongdan.query.select_for_update().filter(gongdan_id=gongdan_id).first()
    if not gd:
        raise GongdanError('工单不存在', 'not_found')
    if gd.shenqingren_id != yonghuid:
        raise GongdanError('仅投诉人可确认解决', 'forbidden')
    if gd.zhuangtai not in OPEN_STATUSES:
        raise GongdanError('当前状态无需确认解决', 'bad_status')
    gd.zhuangtai = STATUS_DONE
    gd.chuli_jieguo = gd.chuli_jieguo or '双方协商已解决'
    gd.chuli_time = timezone.now()
    gd.chuliren_name = gd.chuliren_name or '双方协商'
    gd.save()
    return gd


@transaction.atomic
def user_start_negotiate(*, gongdan_id, yonghuid):
    gd = Gongdan.query.select_for_update().filter(gongdan_id=gongdan_id).first()
    if not gd:
        raise GongdanError('工单不存在', 'not_found')
    if not user_can_view_gongdan(gd, yonghuid):
        raise GongdanError('无权操作此工单', 'forbidden')
    if gd.zhuangtai not in (STATUS_PROCESSING, STATUS_DISSATISFIED, STATUS_NEGOTIATING):
        raise GongdanError('当前状态不可转协商', 'bad_status')
    gd.zhuangtai = STATUS_NEGOTIATING
    gd.save(update_fields=['zhuangtai', 'UpdateTime'])
    return gd


@transaction.atomic
def user_apply_intervene(*, gongdan_id, yonghuid):
    """用户申请平台介入：打标，仍保持未结清。"""
    gd = Gongdan.query.select_for_update().filter(gongdan_id=gongdan_id).first()
    if not gd:
        raise GongdanError('工单不存在', 'not_found')
    if gd.shenqingren_id != yonghuid and _effective_target_uid(gd) != yonghuid:
        raise GongdanError('无权申请介入', 'forbidden')
    if gd.zhuangtai not in OPEN_STATUSES:
        raise GongdanError('当前状态不可申请介入', 'bad_status')
    gd.platform_intervened = True
    if not gd.intervene_at:
        gd.intervene_at = timezone.now()
    gd.intervene_reason = gd.intervene_reason or 'user_apply'
    if gd.zhuangtai == STATUS_NEGOTIATING:
        gd.zhuangtai = STATUS_PROCESSING
    gd.save()
    return gd
